Lehký úvod do Fluentd

Publikováno: 19.4.2021

Dnes budeme pokračovat dalším dílem minisérie o Fluentd. Podíváme se, jak si nakonfigurovat jednoduchou logovací infrastrukturu.

Celý článek

Text vyšel původně na autorově webu.

Dnes budeme pokračovat dalším dílem minisérie o Fluentd. Minule jsme si řekli něco o historii logování a zasadili si Fluentd do kontextu. A trochu jsme si načrtli architekturu a design. Dnes se podíváme, jak si nakonfigurovat jednoduchou logovací infrastrukturu. Ale ještě před tím, se krátce vrátím k minulému článku.

Když jsem předešlý článek nazval Fluentd, budoucnost logování, nemyslel jsem to úplně vážně. Na Twitteru se rozproudila diskuze a u dvou názorů bych se rád pozastavil.

Je JSON budoucnost logování? Ne

Ten původní odstavec (ve kterém jsem 4x napsal slovo JSON) jsem neformuloval úplně šťastně — nechtěně jsem zdůraznil přenosový formát, který je vlastně úplně nepodstatný.

To, co se mi na Fluentd líbí (a proč si myslím, že je to dobré moderní řešení), souvisí s designem, architekturou, otevřeností založenou na pluginech a dobrým pokrytím vstupních a výstupních kanálů (zajištěnou core pluginy).

O JSONu si každý můžeme myslet co chceme, ale buď jak buď, je to lingua franca současného cloudu. Ať už na úrovni infrastruktury (moje doména), nebo aplikací.

Je telemetry budoucností logování? Ne

Hodně zajímavá a (pro mne) přínosná byla diskuze o telemetry a jestli nahrazuje (a je lepší než) logování. Myslím si, že jsou to nesouvisející témata. Alespoň z hlediska Fluentd — tomu je totiž jedno, jaký obsah přenáší (hlavně, že je strukturovaný). Takže klidně může přenášet metriky. Nebo může přenášet logy ve formátu events. Je to jen na vás, co budou vaše aplikace produkovat.

Mimochodem, odkazované OpenTelemetry, je také součástí CNCF, stejně jako Fluentd, nebo třeba Prometheus (etalon metrik). Co může vést k neporozumění na základě výše zmíněného článku, API a výstupní formát může být identický jak pro events, tak pro metriky, tak pro logy. Ale jejich smysl a použití je diametrálně jiné. Už proto, že jde o rozdílné typy dat — log record (event) je jednorozměrná textová zpráva (může mít atributy), metrika je/může být vícerozměrná číselná hodnota, reprezentovaná jako číselná řada.

Každopádně, (Fluentd) metrikám se budu alespoň letmo věnovat v příštím článku.

Jednoduchá Fluentd pipeline

Dost bylo filozofování, pojďme se podívat na reálný kód. S něčím takovým budete pravděpodobně začínat, možná to bude vaše PoC, či prostě pouhé vyzkoušení Fluentd. Vezměme si jednoduchý use case:

  • Logovací soubory v JSON formátu budou uloženy v definované adresářové struktuře.
  • Uděláme jednoduchou sanitizaci, že logy jsou ve formátu UTF-8.
  • Logy pošleme do Elasticsearch (čistě jen z důvodu, abychom si je jednoduše a agregovaně zobrazili).

Načtení logů

Naším úkolem bude načíst logy z následující struktury:

Adresářová struktura a výpis jednoho logu
Adresářová struktura a výpis jednoho logu

Čili:

  • Každý log má svůj adresář,
  • log je strukturovaný (atributy msglevelcell-id),
  • časový identifikátor (atribut ts) je Unix timestamp v milisekundách.

Pro tento účel je ideální tail plugin, který se chová obdobně jako klasický unixový tail — čte z konce souboru, resp. nově přidané řádky.

<source>
  @type tail
  path /tmp/fluentd/*/cell.log
  pos_file /tmp/fluentd/cell.log.pos
  tag cloudsql.cell
  read_from_head true
  <parse>
    @type json
  </parse>
</source>

Atributy konfigurace jsou docela samopopisné, takže nepůjdu do detailu, ale u dvou z nich bych se zastavil.

Pokud aplikace hned po vzniku logovacího souboru do něj ihned velmi rychle zapisuje (v mém případě to byly milisekundy a desítky záznamů), může se stát, že tyto úvodní zprávy budou ignorovány. Důvod je ten, že Fluentd nejprve daný soubor zaregistruje pro sledování (viz následující výpis) a až teprve potom začne sledovat přibývající záznamy.

2020-12-18 10:54:06 [info]: following tail of /home/guido/dev/fluent/cell-1/cell.log

Řešením je nastavit atribut read_from_head na true. Nový soubor je tak čten vždy od začátku.

Samozřejmě, není efektivní číst soubor a nevědět, kde jsme se čtením skončili minule. Proto je dobré nastavit position file (atribute pos_file), který drží ukazatel na poslední čtenou pozici v každém sledovaném souboru.

Pozice čtení pro jednotlivé logy
Pozice čtení pro jednotlivé logy

Filtrování logů

OK, logy máme načtený (někde uvnitř Fluentd) a než je převážeme mašlí a doručíme, chceme je trochu porychtovat. Přichází ke slovu filter pluginy. Ty mají na starosti filtrovat, či/a transformovat jednotlivé záznamy, než se posunou do výstupních pluginů.

Řekněme, že máme následující use case: představme si (čistě hypoteticky), že se nemůžeme spolehnout na to, že nám vývojáři sypou do logů záznamy v očekávaném kódování. Takže než je posuneme dál, chceme záznamy pročistit, sanitizovat.

Poslouží nám record_transformer plugin. Jak název napovídá, transformuje jednotlivé záznamy. Za zmínku stojí jeho atribut enable_ruby, který umožní používat Ruby výrazy (není to tak zlé, jak to zní).

<filter cloudsql.cell.app>
  @type record_transformer
  enable_ruby
  <record>
    msg ${record["msg"].encode("UTF-8", invalid: :replace, replace: "__NON_ASCII_CHAR__")}
  </record>
</filter>

Konkrétně zde projdeme všechny záznamy a pokud tento obsahuje nějaké ilegální UTF-8 znaky, nahradí je řetězcem __NON_ASCII_CHAR__.

Publikování logů

Už máme skoro hotovo — zbývá záznamy někam publikovat, stačí si vybrat vhodný output plugin. V případě, že chceme záznamy publikovat do více cílových platforem, použije se copy plugin. V našem případě to ale uděláme jednoduše, starý dobrý Elasticsearch:

<match cloudsql.cell.app>
  @type elasticsearch
  host localhost
  port 9200
  index_name fluentd.${tag}
</match>

Nastavování Elasticsearch a případně Kibany zde přeskočíme (to už přece dávno umíte) a podíváme se rovnou na výsledek:

Logy přenesené do Elasticsearch, zobrazené v Kibaně
Logy přenesené do Elasticsearch, zobrazené v Kibaně

Celá konfigurace

## File input
<source>
  @type tail
  path /tmp/fluentd/*/cell.log
  pos_file /tmp/fluentd/cell.log.pos
  tag cloudsql.cell
  read_from_head true
  <parse>
    @type json
  </parse>
</source>

## Filter
<filter cloudsql.cell>
  @type record_transformer
  enable_ruby
  <record>
    msg ${record["msg"].encode("UTF-8", invalid: :replace, replace: "__NON_ASCII_CHAR__")}
    timestamp ${record["ts"]}
  </record>
</filter>

## Elasticsearch
<match cloudsql.cell>
  @type elasticsearch
  host localhost
  port 9200
  index_name fluentd.${tag}
  <buffer>
    flush_interval 10s
  </buffer>
</match>

Pokračování příště

Příště se podíváme na reálný use case, který se časem vyvinul v docela komplexní řešení.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace