Logstash parsing dokumen xml yang berisi banyak entri log

8

Saat ini saya sedang mengevaluasi apakah logstash dan elasticsearch berguna untuk kasus penggunaan kami. Apa yang saya miliki adalah file log yang berisi banyak entri yang berbentuk

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

Setiap entryelemen akan berisi satu peristiwa log. (Jika Anda tertarik, file ini sebenarnya adalah work-log ekspor Tempo Timesheets (An Atlassian JIRA).)

Apakah mungkin untuk mengubah file seperti itu menjadi beberapa peristiwa log tanpa menulis codec saya sendiri?

ganda
sumber

Jawaban:

11

Baiklah, saya menemukan solusi yang bekerja untuk saya. Masalah terbesar dengan solusinya adalah bahwa plugin XML adalah ... tidak cukup tidak stabil, tetapi tidak terdokumentasi dengan baik dan bermasalah atau didokumentasikan dengan buruk dan salah.

TLDR

Baris perintah Bash:

gzcat -d file.xml.gz | tr -d "\n\r" | xmllint --format - | logstash -f logstash-csv.conf

Konfigurasi 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 {
    ...
}

Terperinci

Solusi saya berfungsi karena setidaknya sampai entrylevel, input XML saya sangat seragam dan dengan demikian dapat ditangani oleh beberapa jenis pencocokan pola.

Karena ekspor pada dasarnya adalah satu baris XML yang sangat panjang, dan plugin logstash xml pada dasarnya hanya bekerja dengan bidang (baca: kolom dalam baris) yang berisi data XML, saya harus mengubah data menjadi format yang lebih bermanfaat.

Shell: Mempersiapkan file

  • gzcat -d file.xml.gz |: Terlalu banyak data - jelas Anda dapat melewati itu
  • tr -d "\n\r" |: Hapus jeda baris di dalam elemen XML: Beberapa elemen dapat berisi jeda baris sebagai data karakter. Langkah selanjutnya mengharuskan ini dihapus, atau dikodekan dalam beberapa cara. Meskipun diasumsikan bahwa pada titik ini Anda memiliki semua kode XML dalam satu garis besar, tidak masalah jika perintah ini menghilangkan spasi putih di antara elemen

  • xmllint --format - |: Format XML dengan xmllint (dilengkapi dengan libxml)

    Di sini satu baris besar spageti XML ( <root><entry><fieldx>...</fieldx></entry></root>) diformat dengan benar:

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

Logstash

logstash -f logstash-csv.conf

(Lihat konten lengkap .conffile di bagian TL; DR.)

Di sini, multilinefilternya berfungsi. Itu dapat menggabungkan beberapa baris menjadi satu pesan log. Dan inilah mengapa format dengan xmllintitu diperlukan:

filter {
    # add all lines that have more indentation than double-space to the previous line
    multiline {
        pattern => "^\s\s(\s\s|\<\/entry\>)"
        what => previous
    }
}

Ini pada dasarnya mengatakan bahwa setiap baris dengan lekukan yang lebih dari dua spasi (atau </entry>/ xmllint melakukan lekukan dengan dua spasi secara default) milik baris sebelumnya. Ini juga berarti data karakter tidak boleh mengandung baris baru (dilucuti dengan trshell) dan bahwa xml harus dinormalisasi (xmllint)

ganda
sumber
Hai apakah Anda berhasil membuat ini bekerja? Saya ingin tahu karena saya memiliki kebutuhan yang serupa dan solusi multiline bersama dengan perpecahan tidak bekerja untuk saya. Terima kasih atas tanggapan Anda
yaitu
@viz Ini berfungsi, tetapi kami tidak pernah menggunakannya dalam produksi. Multiline hanya berfungsi jika Anda memiliki struktur XML yang sangat teratur dan telah memformatnya terlebih dahulu dengan lekukan (lihat jawaban, bagian "menyiapkan file")
digandakan
1

Saya punya kasus serupa. Untuk menguraikan xml ini:

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

Saya menggunakan konfigurasi ini untuk 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
  }
}

Saya harap ini bisa membantu seseorang. Saya membutuhkan waktu lama untuk mendapatkannya.

Drinor
sumber