D'accord, j'ai trouvé une solution qui fonctionne pour moi. Le plus gros problème avec la solution est que le plugin XML n'est ... pas tout à fait instable, mais soit mal documenté et bogué, soit mal et mal documenté.
TLDR
Ligne de commande Bash:
gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf
Configuration 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 {
...
}
Détaillé
Ma solution fonctionne car au moins jusqu'au entry
niveau, mon entrée XML est très uniforme et peut donc être gérée par une sorte de correspondance de modèle.
Étant donné que l'exportation est essentiellement une très longue ligne de XML et que le plug-in logstash xml ne fonctionne essentiellement qu'avec des champs (lire: colonnes en lignes) qui contiennent des données XML, j'ai dû changer les données dans un format plus utile.
Shell: préparation du fichier
gzcat -d file.xml.gz |
: Était juste trop de données - évidemment, vous pouvez ignorer cela
tr -d "\n\r" |
: Supprimer les sauts de ligne dans les éléments XML: Certains éléments peuvent contenir des sauts de ligne en tant que données de caractère. L'étape suivante nécessite que ceux-ci soient supprimés ou encodés d'une manière ou d'une autre. Même si elle supposait qu'à ce stade, vous avez tout le code XML sur une seule ligne massive, peu importe si cette commande supprime tout espace blanc entre les éléments
xmllint --format - |
: Formater le XML avec xmllint (livré avec libxml)
Ici, la seule énorme ligne de spaghetti de XML ( <root><entry><fieldx>...</fieldx></entry></root>
) est correctement formatée:
<root>
<entry>
<fieldx>...</fieldx>
<fieldy>...</fieldy>
<fieldz>...</fieldz>
<fieldarray>
<fielda>...</fielda>
<fieldb>...</fieldb>
...
</fieldarray>
</entry>
<entry>
...
</entry>
...
</root>
Logstash
logstash -f logstash-csv.conf
(Voir le contenu complet du .conf
fichier dans la section TL; DR.)
Ici, le multiline
filtre fait l'affaire. Il peut fusionner plusieurs lignes en un seul message de journal. Et c'est pourquoi le formatage avec xmllint
était nécessaire:
filter {
# add all lines that have more indentation than double-space to the previous line
multiline {
pattern => "^\s\s(\s\s|\<\/entry\>)"
what => previous
}
}
Cela signifie essentiellement que chaque ligne avec une indentation de plus de deux espaces (ou est </entry>
/ xmllint fait une indentation avec deux espaces par défaut) appartient à une ligne précédente. Cela signifie également que les données de caractères ne doivent pas contenir de nouvelles lignes (supprimées avec tr
dans le shell) et que le xml doit être normalisé (xmllint)