Logstash analysant un document xml contenant plusieurs entrées de journal

8

J'évalue actuellement si logstash et elasticsearch sont utiles pour notre cas d'utilisation. Ce que j'ai, c'est un fichier journal contenant plusieurs entrées qui est de la forme

<root>
    <entry>
        <fieldx>...</fieldx>
        <fieldy>...</fieldy>
        <fieldz>...</fieldz>
        ...
        <fieldarray>
            <fielda>...</fielda>
            <fielda>...</fielda>
            ...
        </fieldarray>
    </entry>
    <entry>
    ...
    </entry>
    ...
<root>

Chaque entryélément contiendrait un événement de journal. (Si vous êtes intéressé, le fichier est en fait une exportation de journal de travail Tempo Timesheets (An Atlassian JIRA Plug-in).)

Est-il possible de transformer un tel fichier en plusieurs événements de journal sans écrire mon propre codec?

dualed
la source

Réponses:

11

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 entryniveau, 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 .conffichier dans la section TL; DR.)

Ici, le multilinefiltre 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 trdans le shell) et que le xml doit être normalisé (xmllint)

dualed
la source
Salut avez-vous réussi à faire ce travail? Je suis curieux car j'ai un besoin similaire et la solution multiligne avec la scission n'a pas fonctionné pour moi. Merci pour vos commentaires
savoir
@viz Cela a fonctionné, mais nous ne l'avons jamais utilisé en production. Le multiligne ne fonctionne que si vous avez une structure XML très régulière et que vous l'avez d'abord formatée avec une indentation (voir la réponse, section "préparation du fichier")
dualed
1

J'avais un cas similaire. Pour analyser ce xml:

<ROOT number="34">
  <EVENTLIST>
    <EVENT name="hey"/>
    <EVENT name="you"/>
  </EVENTLIST>
</ROOT>

J'utilise cette configuration pour logstash:

input {
  file {
    path => "/path/events.xml"
    start_position => "beginning"
    sincedb_path => "/dev/null"
    codec => multiline {
      pattern => "<ROOT"
      negate => "true"
      what => "previous"
      auto_flush_interval => 1
    }
  }
}
filter {
  xml {
    source => "message"
    target => "xml_content"
  }
  split {
    field => "xml_content[EVENTLIST]"
  }
  split {
    field => "xml_content[EVENTLIST][EVENT]"
  }
  mutate {
    add_field => { "number" => "%{xml_content[number]}" }
    add_field => { "name" => "%{xml_content[EVENTLIST][EVENT][name]}" }
    remove_field => ['xml_content', 'message', 'path']
  }
}
output {
  stdout {
    codec => rubydebug
  }
}

J'espère que cela peut aider quelqu'un. J'ai eu besoin de beaucoup de temps pour l'obtenir.

drinor
la source