Bene, ho trovato una soluzione che funziona per me. Il problema più grande con la soluzione è che il plug-in XML è ... non del tutto instabile, ma scarsamente documentato e difettoso o scarsamente e erroneamente documentato.
TLDR
Riga di comando Bash:
gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf
Config 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 {
...
}
dettagliata
La mia soluzione funziona perché almeno fino al entry
livello, il mio input XML è molto uniforme e quindi può essere gestito da una specie di pattern matching.
Poiché l'esportazione è fondamentalmente una lunga riga di XML e il plug-in xml logstash funziona essenzialmente solo con campi (leggi: colonne in righe) che contengono dati XML, ho dovuto cambiare i dati in un formato più utile.
Shell: preparazione del file
gzcat -d file.xml.gz |
: Erano troppi dati - ovviamente puoi saltarli
tr -d "\n\r" |
: Rimuove le interruzioni di riga all'interno di elementi XML: alcuni elementi possono contenere interruzioni di riga come dati carattere. Il passaggio successivo richiede che vengano rimossi o codificati in qualche modo. Anche se si presuppone che a questo punto si disponga di tutto il codice XML in un'unica riga, non importa se questo comando rimuove lo spazio bianco tra gli elementi
xmllint --format - |
: Formatta l'XML con xmllint (viene fornito con libxml)
Qui la singola enorme linea di spaghetti di XML ( <root><entry><fieldx>...</fieldx></entry></root>
) è formattata correttamente:
<root>
<entry>
<fieldx>...</fieldx>
<fieldy>...</fieldy>
<fieldz>...</fieldz>
<fieldarray>
<fielda>...</fielda>
<fieldb>...</fieldb>
...
</fieldarray>
</entry>
<entry>
...
</entry>
...
</root>
Logstash
logstash -f logstash-csv.conf
(Vedi il contenuto completo del .conf
file nella sezione TL; DR.)
Qui, il multiline
filtro fa il trucco. Può unire più righe in un singolo messaggio di registro. Ecco perché xmllint
era necessaria la formattazione :
filter {
# add all lines that have more indentation than double-space to the previous line
multiline {
pattern => "^\s\s(\s\s|\<\/entry\>)"
what => previous
}
}
Questo in sostanza dice che ogni riga con rientro che è più di due spazi (o che è </entry>
/ xmllint fa rientro con due spazi di default) appartiene a una riga precedente. Questo significa anche che i dati dei caratteri non devono contenere newline (rimossi con tr
in shell) e che l'xml deve essere normalizzato (xmllint)