Хорошо, я нашел решение, которое работает для меня. Самая большая проблема с решением заключается в том, что плагин 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)