Logstash che analizza il documento XML contenente più voci di registro


8

Attualmente sto valutando se logstash ed elasticsearch sono utili per il nostro caso d'uso. Quello che ho è un file di registro contenente più voci che è del modulo

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

Ogni entryelemento conterrebbe un evento di registro. (Se sei interessato, il file è in realtà un'esportazione del registro di lavoro Tempo Timesheets (An Atlassian JIRA Plug-in).)

È possibile trasformare un tale file in più eventi di log senza scrivere il mio codec?

Risposte:


11

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 entrylivello, 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 .conffile nella sezione TL; DR.)

Qui, il multilinefiltro fa il trucco. Può unire più righe in un singolo messaggio di registro. Ecco perché xmllintera 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 trin shell) e che l'xml deve essere normalizzato (xmllint)


Ciao, sei riuscito a farlo funzionare? Sono curioso poiché ho necessità simili e la soluzione multilinea insieme alla divisione non ha funzionato per me. Grazie per il tuo feedback
vale a dire il

@viz Questo ha funzionato, ma non l'abbiamo mai usato in produzione. Multiline funziona solo se si dispone di una struttura XML molto regolare e prima è stata formattata con rientro (vedere la risposta, sezione "preparazione del file")
doppiata il

1

Ho avuto un caso simile. Per analizzare questo XML:

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

Uso questa configurazione per 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
  }
}

Spero che questo possa aiutare qualcuno. Ho avuto bisogno di molto tempo per ottenerlo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.