Tudo bem, encontrei uma solução que funcione para mim. O maior problema com a solução é que o plugin XML é ... não instável, mas mal documentado e com erros ou mal e incorretamente documentado.
TLDR
Linha de comando do Bash:
gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf
Configuração do 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 {
...
}
Detalhado
Minha solução funciona porque, pelo menos até o nível entry
, minha entrada XML é muito uniforme e, portanto, pode ser manipulada por algum tipo de correspondência de padrões.
Como a exportação é basicamente uma linha muito longa de XML, e o plug-in logstash xml essencialmente funciona apenas com campos (leia-se: colunas em linhas) que contêm dados XML, tive que alterar os dados para um formato mais útil. / p>
Shell: Preparando o arquivo
-
gzcat -d file.xml.gz |
: Foram dados demais - obviamente você pode pular isso -
tr -d "\n\r" |
: remova as quebras de linha nos elementos XML: Alguns dos elementos podem conter quebras de linha como dados de caractere. O próximo passo requer que eles sejam removidos ou codificados de alguma forma. Mesmo assumindo que neste momento você tem todo o código XML em uma linha massiva, não importa se este comando remove qualquer espaço em branco entre os elementos -
xmllint --format - |
: Formate o XML com xmllint (vem com libxml)Aqui, a única linha de spaghetti com XML (
<root><entry><fieldx>...</fieldx></entry></root>
) está formatada corretamente:<root> <entry> <fieldx>...</fieldx> <fieldy>...</fieldy> <fieldz>...</fieldz> <fieldarray> <fielda>...</fielda> <fieldb>...</fieldb> ... </fieldarray> </entry> <entry> ... </entry> ... </root>
Logstash
logstash -f logstash-csv.conf
(Veja o conteúdo completo do arquivo .conf
na seção TL; DR.)
Aqui, o filtro multiline
faz o truque. Pode mesclar várias linhas em uma única mensagem de log. E é por isso que a formatação com xmllint
foi necessária:
filter {
# add all lines that have more indentation than double-space to the previous line
multiline {
pattern => "^\s\s(\s\s|\<\/entry\>)"
what => previous
}
}
Isso basicamente diz que toda linha com recuo com mais de dois espaços (ou é </entry>
/ xmllint faz recuo com dois espaços por padrão) pertence a uma linha anterior. Isso também significa que os dados de caractere não devem conter novas linhas (removidos com tr
no shell) e que o xml deve ser normalizado (xmllint)