Muy bien, encontré una solución que funciona para mí. El mayor problema con la solución es que el complemento XML no es ... bastante inestable, pero está mal documentado y tiene errores o está mal documentado de forma incorrecta.
TLDR
Línea de comando bash:
gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf
Configuración de 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 {
...
}
Detallado
Mi solución funciona porque al menos hasta el entry
nivel, mi entrada XML es muy uniforme y, por lo tanto, puede manejarse mediante algún tipo de coincidencia de patrones.
Dado que la exportación es básicamente una línea muy larga de XML, y el complemento logstash xml funciona esencialmente solo con campos (léase: columnas en líneas) que contienen datos XML, tuve que cambiar los datos a un formato más útil.
Shell: preparar el archivo
gzcat -d file.xml.gz |
: Era demasiada información, obviamente puedes omitir eso
tr -d "\n\r" |
: Eliminar saltos de línea dentro de elementos XML: algunos de los elementos pueden contener saltos de línea como datos de caracteres. El siguiente paso requiere que se eliminen o se codifiquen de alguna manera. Aunque se supone que en este punto tiene todo el código XML en una línea masiva, no importa si este comando elimina cualquier espacio en blanco entre los elementos
xmllint --format - |
: Formatee el XML con xmllint (viene con libxml)
Aquí la única línea de espagueti enorme de XML ( <root><entry><fieldx>...</fieldx></entry></root>
) tiene el formato correcto:
<root>
<entry>
<fieldx>...</fieldx>
<fieldy>...</fieldy>
<fieldz>...</fieldz>
<fieldarray>
<fielda>...</fielda>
<fieldb>...</fieldb>
...
</fieldarray>
</entry>
<entry>
...
</entry>
...
</root>
Logstash
logstash -f logstash-csv.conf
(Consulte el contenido completo del .conf
archivo en la sección TL; DR).
Aquí, el multiline
filtro hace el truco. Puede combinar varias líneas en un solo mensaje de registro. Y es por eso que xmllint
fue necesario formatear con :
filter {
# add all lines that have more indentation than double-space to the previous line
multiline {
pattern => "^\s\s(\s\s|\<\/entry\>)"
what => previous
}
}
Básicamente, esto dice que cada línea con sangría que tiene más de dos espacios (o es </entry>
/ xmllint hace sangría con dos espacios por defecto) pertenece a una línea anterior. Esto también significa que los datos de los caracteres no deben contener líneas nuevas (despojadas con tr
shell) y que el xml debe estar normalizado (xmllint)