Анализ XML-документа Logstash, содержащий несколько записей журнала

8

В настоящее время я оцениваю, полезны ли logstash иasticsearch для нашего варианта использования. У меня есть файл журнала, содержащий несколько записей, который имеет форму

<root>
    <entry>
        <fieldx>...</fieldx>
        <fieldy>...</fieldy>
        <fieldz>...</fieldz>
        ...
        <fieldarray>
            <fielda>...</fielda>
            <fielda>...</fielda>
            ...
        </fieldarray>
    </entry>
    <entry>
    ...
    </entry>
    ...
<root>

Каждый entryэлемент будет содержать одно событие журнала. (Если вам интересно, файл на самом деле представляет собой экспорт рабочего журнала Tempo Times (плагин Atlassian JIRA).)

Можно ли преобразовать такой файл в несколько событий журнала без написания собственного кодека?

dualed
источник

Ответы:

11

Хорошо, я нашел решение, которое работает для меня. Самая большая проблема с решением заключается в том, что плагин XML ... не совсем нестабилен, но либо плохо документирован и содержит ошибки, либо плохо и неправильно документирован.

TLDR

Командная строка Bash:

gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf

Конфигурация Logstash:

input {
    stdin {}
}

filter {
    # add all lines that have more indentation than double-space to the previous line
    multiline {
        pattern => "^\s\s(\s\s|\<\/entry\>)"
        what => previous
    }
    # multiline filter adds the tag "multiline" only to lines spanning multiple lines
    # We _only_ want those here.
    if "multiline" in [tags] {
        # Add the encoding line here. Could in theory extract this from the
        # first line with a clever filter. Not worth the effort at the moment.
        mutate {
            replace => ["message",'<?xml version="1.0" encoding="UTF-8" ?>%{message}']
        }
        # This filter exports the hierarchy into the field "entry". This will
        # create a very deep structure that elasticsearch does not really like.
        # Which is why I used add_field to flatten it.
        xml {
            target => entry
            source => message
            add_field => {
                fieldx         => "%{[entry][fieldx]}"
                fieldy         => "%{[entry][fieldy]}"
                fieldz         => "%{[entry][fieldz]}"
                # With deeper nested fields, the xml converter actually creates
                # an array containing hashes, which is why you need the [0]
                # -- took me ages to find out.
                fielda         => "%{[entry][fieldarray][0][fielda]}"
                fieldb         => "%{[entry][fieldarray][0][fieldb]}"
                fieldc         => "%{[entry][fieldarray][0][fieldc]}"
            }
        }
        # Remove the intermediate fields before output. "message" contains the
        # original message (XML). You may or may-not want to keep that.
        mutate {
            remove_field => ["message"]
            remove_field => ["entry"]
        }
    }
}

output {
    ...
}

Детальнее

Мое решение работает, потому что, по крайней мере, до entryуровня, мой ввод XML очень однороден и, следовательно, может быть обработан с помощью какого-либо сопоставления с образцом.

Поскольку экспорт представляет собой одну действительно длинную строку XML, а подключаемый модуль logstash xml по существу работает только с полями (читай: столбцы в строках), которые содержат данные XML, мне пришлось преобразовать данные в более полезный формат.

Оболочка: подготовка файла

  • gzcat -d file.xml.gz |: Было слишком много данных - очевидно, вы можете пропустить это
  • tr -d "\n\r" |: Удалить разрывы строк внутри элементов XML: Некоторые элементы могут содержать разрывы строк в виде символьных данных. Следующий шаг требует, чтобы они были удалены или закодированы каким-либо образом. Несмотря на то, что предполагается, что на данный момент у вас есть весь XML-код в одной массивной строке, не имеет значения, удаляет ли эта команда пробелы между элементами

  • xmllint --format - |: Отформатируйте XML с помощью xmllint (поставляется с libxml)

    Здесь единственная огромная строка XML ( <root><entry><fieldx>...</fieldx></entry></root>) спагетти правильно отформатирована:

    <root>
      <entry>
        <fieldx>...</fieldx>
        <fieldy>...</fieldy>
        <fieldz>...</fieldz>
        <fieldarray>
          <fielda>...</fielda>
          <fieldb>...</fieldb>
          ...
        </fieldarray>
      </entry>
      <entry>
        ...
      </entry>
      ...
    </root>
    

Logstash

logstash -f logstash-csv.conf

(См. Полное содержание .confфайла в разделе TL; DR.)

Здесь multilineфильтр делает свое дело. Он может объединить несколько строк в одно сообщение журнала. И именно поэтому xmllintбыло необходимо форматирование с :

filter {
    # add all lines that have more indentation than double-space to the previous line
    multiline {
        pattern => "^\s\s(\s\s|\<\/entry\>)"
        what => previous
    }
}

По сути, это говорит о том, что каждая строка с отступом более двух пробелов (или is </entry>/ xmllint делает отступ с двумя пробелами по умолчанию) принадлежит предыдущей строке. Это также означает, что символьные данные не должны содержать символов новой строки (с trразделителями в оболочке) и что XML должен быть нормализован (xmllint)

dualed
источник
Привет, тебе удалось сделать эту работу? Мне любопытно, поскольку у меня такая же потребность, и многострочное решение вместе с разделением не сработало для меня. Спасибо за ваш отзыв
есть
@viz Это сработало, но мы никогда не использовали его в производстве. Multiline работает только в том случае, если у вас очень правильная структура XML и вы отформатировали ее сначала с отступом (см. Ответ, раздел «подготовка файла»)
12
1

У меня был похожий случай. Для анализа этого XML:

<ROOT number="34">
  <EVENTLIST>
    <EVENT name="hey"/>
    <EVENT name="you"/>
  </EVENTLIST>
</ROOT>

Я использую эту конфигурацию для logstash:

input {
  file {
    path => "/path/events.xml"
    start_position => "beginning"
    sincedb_path => "/dev/null"
    codec => multiline {
      pattern => "<ROOT"
      negate => "true"
      what => "previous"
      auto_flush_interval => 1
    }
  }
}
filter {
  xml {
    source => "message"
    target => "xml_content"
  }
  split {
    field => "xml_content[EVENTLIST]"
  }
  split {
    field => "xml_content[EVENTLIST][EVENT]"
  }
  mutate {
    add_field => { "number" => "%{xml_content[number]}" }
    add_field => { "name" => "%{xml_content[EVENTLIST][EVENT][name]}" }
    remove_field => ['xml_content', 'message', 'path']
  }
}
output {
  stdout {
    codec => rubydebug
  }
}

Я надеюсь, что это может кому-то помочь. Мне нужно было много времени, чтобы получить это.

drinor
источник