Centralizace logů Dockeru pomocí Elastic Stacku

Dokud vám na serveru běží pár kontejnerů, dají se jejich logy procházet ručně pomocí příkazu docker logs. S rostoucím počtem kontejnerů je ale obtížnější v logách něco najít. V tu chvíli může situaci usnadnit agregace logů. Jakmile máte logy na jednom místě, můžete v nich vyhledávat a filtrovat, je práce s nimi snažší. Jedním z nástrojů, který toto umožňuje se jmenuje Elastic Stack, dříve známý jako ELK Stack.

Spousta logů, kdo se v tom má vyznat

Pro absolvování tohoto tutoriálu předpokládám nainstalovaný Docker a docker-compose. Pokud jej ještě nemáte, postupujte podle oficiálního návodu. Tento článek předpokládá, že Docker běží nativně a kontejnery jsou dostupné přes localhost. Protože je ale tato funkčnost zatím oficiálně jen na Linuxu (pro OS X je ve verzi beta), je možné, že máte docker ve virtuálním stroji vytvořeným pomocí docker-machine. V tom případě nahraďte localhost ip adresou, kterou získáte pomocí docker-machine ip.

Elastic Stack

Elastic Stack sám o sobě není jediná aplikace, jde o sadu tří nástrojů, konkrétně se jedná o Elasticsearch, Logstash a Kibana. Ty bývají často použity společně (a například u Kibany by to ani jinak nešlo, protože se umí připojit jen na Elasticsearch).

Elastic stack oproti ELK stacku nenabízí prakticky nic nového, jde především o sjednocení verzí, nemusíte si tedy hlídat, které verze Elasticsearche, Kibany a Logstashe jsou vzájemně kompatibilní. Více na webu elastic.co. Pojďme se nyní podívat na jednotlivé části stacku.

Logstash

Logstash je nástroj, který umí vzít nějaký vstup, transformovat jej a následně jej někam uložit. Do konfiguračního souboru můžete zapsat všemožné kombinace vstupů, výstupů a filtrů. Vstupem může být soubor, data v Redisu nebo nějakém message brokeru. Můžete je také pouze nechat naslouchat na určeném portu a data mu dodávat z vaší aplikace, nebo použít nějaký jiný nástroj. Výstupem může být například Elasticsearch, soubor, standardní výstup... Popis všech možností by byl hodně dlouhý, odkážu radši na oficiální dokumentaci, kde můžete najít jak možné vstupy tak výstupy. Filtrem se rozumí různé transformace a parsování vstupních dat, například rozpadnutí záznamu v Apache access logu, CSV, JSONu...

Logstash

Elasticsearch

Elasticsearch je škálovatelné úložiště, do kterého můžete uložit lobovolný dokument ve formátu JSON. Je bezschémový, nemusíte tedy předem definovat sloupce jako v relační databázi. Je také škálovatelný, stačí spustit více instancí, data se automaticky rozloží mezi nody a dotaz můžete poslat na kterýkoliv z nich. Disponuje velmi dobrým fulltextovým vyhledáváním, pro které bývá často primárně použit, to však v tomto případě nevyužijeme.

Elasticsearch

Kibana

Kibana je poslední částí, se kterou budete nakonec pracovat nejčastěji. Je to webové rozhraní, které umí číst data z Elasticsearche a z nich vytvářet grafy, tabulky, metriky. Nad vytvořenými dashboardy můžete fulltextově vyhledávat, filtrovat data. Vytvořený dashboard v Kibaně může vypadat například takto:

Kibana

Spuštění Elastic Stacku

Protože budeme agregovat logy Dockeru, předpokládám, že máte Docker nainstalovaný. Spustit samotný Elastic Stack je otázkou vytvoření souboru docker-compose.yml a následným spouštěním pomocí nástroje docker-compose.

Elastic Stack

V soboru docker-compose.yml definuji tři sekce, z káždé následně vznikne jeden kontejner. Pro každou sekci využiji oficiální Docker image. Důležité je zpřístupnění portu 5601, abychom mohli otevřít Kibanu v browseru. V commandu logstashe je definováno jak bude spuštěn. Parametr -e znamená, že bude následovat konfigurace (která je jinak v souboru). V ní je definován jako vstup podt 5000 a jako výstup Elasticsearch. Vstupní port 5000 je také zveřejněn.

kibana:  
  image: kibana
  links:
    - 'elastic:elasticsearch'
  ports:
    - '5601:5601'
logstash:  
  image: logstash
  command: 'logstash -e "input { tcp { port => 5000 } } output { elasticsearch { hosts => elastic } }"'
  ports:
    - '5000/tcp:5000/tcp'
  links:
    - elastic
elastic:  
  image: elasticsearch

Uložte tento soubor a následně spusťte příkaz docker-compose up. Mělo by být vidět, jak jednotlivé služby startují:

ELK Stack v Dockeru

Nyní zapíšeme do Logstashe jednu zprávu. Otevřete další terminál a zadejte:

nc localhost 5000 <<< "This is my log message"  

Zpráva This is my log message by měla být uložená v Elasticsearch. Nyní otevřete prohlížeč na adrese http://localhost:5601 a potvrďte výchozí nastavení. Dále přejděte do sekce Discover, měla by tam být vidět zalogovaná zpráva.

Kibana: nastavení Kibana: zalogovaná zpráva

Pokud jste došli až sem, gratuluji, právě jste úspěšně spustili ELK Stack, zapsali zprávu do Logstashe, který ji uložil do Elasticsearche a následně zobrazili v Kibaně.

Propojení Dockeru a Elastic Stacku

Nyní stojíme před otázkou jak dostat logy kontejnerů do Logstashe. To je vzásadě možné několika způsoby:

1. Loguje samotná aplikace

Pokud máte v kontejneru vlastní aplikaci, můžete do ní doprogramovat zápis logů do Logstashe. Například v PHP pomocí Monologu - nastavíte adresu a port a vesele logujete. Zde je problém, jak získat ostatní logy, například webserveru nebo databáze. Lepší je dle mého všechny logy směřovat na standardní výstup a o sběr logů se postarat na úrovni Dockeru.

2. Aplikace loguje do souboru, Logstash jej čte jako vstup

Pokud už nyní logujete do souboru, nabízí se možnost jej zpřístupnit jako volume a Logstash nakonfigurovat, aby jej použil jsko vstup. Zde jsou problémy stejné jako výše, navíc je problém se sdílením souborů, pokud Docker běží na víc serverech.

3. Aplikace loguje na standardní výstup, logy sbírá další služba

Toto je lepší varianta - ke všem logům, které kontejnery produkují přistupuji jednotným způsobem. Ve výchozím stavu Docker zapisuje tyto logy jako JSON do filesystemu, zbývá tedy zajistit čtení těchto souborů.

4. Aplikace loguje na standardní výstup, kontejner má nastaveny logging-drivers

Obdoba předchozí varianty s tím rozdílem, že kontejneru při spuštění řeknete kam má logovat. Kontejner tak aktivně zapisuje logy na dané umístění, v předchozím případě je kontejner pasivní a logy jsou sbírány. Nahradíte tak výchozí logování do souboru ve formátu JSON. Tato funkčnost byla do Dockeru přidána ve verzi 1.6, aktuálně je k dispozici několik driverů, mezi issues na GitHubu jsem nalezl zmínku o driveru pro logstash, aktuálně je asi nejlepší variantou syslog.

Dále budu popisovat třetí variantu, protože umožní zapnutí logování nezávisle na běžících kontejnerech. Není teda nic měnit, jen se spustí další služba, která logy sbírá a přeposílá do Logstashe.

Sběr logů kontejnerů: logspout

Pro tento účel byl vytvořen nástroj logspout (GitHub, Docker Hub). Jde o připravený image, který se spustí, zpřístupní se mu socket docker démona skrz který čte logy všech běžících kontejnerů a přeposílá je dál. Automaticky zjišťuje nové kontejnery, takže existující stacky lze používat beze změny.

Propojení docker démona a logspoutu je pouze zpřístupněním souboru /var/run/docker.sock. Zbývá propojit logspout a Logstash. Logspout má několik adaptérů pro směřování výstupu: tcp, udp, syslog... Upravíme tedy soubor docker-compose.yml - doplníme sekci logspout:

logspout:  
  image: gliderlabs/logspout:v3
  command: 'udp://logstash:5000'
  links:
    - logstash
  volumes:
    - '/var/run/docker.sock:/tmp/docker.sock'
kibana:  
  image: kibana
  links:
    - 'elastic:elasticsearch'
  ports:
    - '5601:5601'
logstash:  
  image: logstash
  command: 'logstash -e "input { udp { port => 5000 } } output { elasticsearch { hosts => elastic } }"'
  links:
    - elastic
elastic:  
  image: elasticsearch

Dále jsem provedl změny v sekci logstash. Smazal jsem část s nastavením portů - k Logstashi přistupuje pouze logspout, což je automaticky zajištěno (kontenery v stacku na sebe vidí jako by byly na lokální síti), není tedy nutné ho zveřejňovat. Dále jsem upravil vstup Logstashe z tcp na udp. Nějakou dobu totiž trvá, než se Logstash spustí a pokud použiji spojení tcp, logspout ihned po spuštění skončí, protože se nemůže připojit na spouštějící se Logstash. UDP je nespojované spojení, logspout tak stále skouší zprávy poslat a je mu jedno, jestli na daném portu něco běží nebo ne, nevyžaduje žádné potvrzení.

Pokud vám ještě běžý starý stack, zabijte jej pomocí ctrl + c, případně i smažte kontejnery pomocí docker-compose rm -f. Nový stack spusťte opět příkazem docker-compose up.

Nyní jsou logy všech kontejnerů routovány přes logspout a Logstash do Elasticsearche. Můžeme zkusit spustit kontejner, který pouze zaloguje jednu zprávu a skončí:

docker run --rm alpine echo Hello world  

Pokud se podíváte do Kibany na http://localhost:5601, na záložce Discover by měla být vidět jako poslední zalogovaná zpráva Hello world:

Kibana: log kontejneru

Jistě jste si všimli spousty zpráv zalogovaných níže. Logspout automaticky přijímá logy všech kontejnerů, tedy i běžící Kibany, Logstashe a Elasticsearche. Pokud nechcete logy některého kontejneru zpracovávat, je třeba mu přidat proměnnou prostředí LOGSPOUT shodnotou ignore. To je možné doplněním do souboru docker-compose.yml. Funkční konfigurace, kdy jsou agregovány logy všech kontejnerů kromě spuštěného Elastic Stacku by tedy vypadala následovně:

logspout:  
  image: gliderlabs/logspout:v3
  command: 'udp://logstash:5000'
  links:
    - logstash
  volumes:
    - '/var/run/docker.sock:/tmp/docker.sock'
kibana:  
  image: kibana
  links:
    - 'elastic:elasticsearch'
  ports:
    - '5601:5601'
  environment:
    - LOGSPOUT=ignore
logstash:  
  image: logstash
  command: 'logstash -e "input { udp { port => 5000 } } output { elasticsearch { hosts => elastic } }"'
  links:
    - elastic
  environment:
    - LOGSPOUT=ignore
elastic:  
  image: elasticsearch
  environment:
    - LOGSPOUT=ignore

Přidání názvu kontejneru do logů

Pokud použijeme spojení logspoutu a Logstashe přes UDP, nezjistíme z jakého kontejneru logy pochází. Abychom to zjistili, je nutné použít jako adaptér syslog. Pro to je třeba upravit nastavení logspoutu i Logstashe. V případě logspoutu stačí upravit v docker-compose.yml část command: 'udp://logstash:5000' na command: 'syslog://logstash:5000'. Obdobnou úpravu je třeba provést na vstupu Logstashe - namísto input { udp { port => 5000 } } použít input { syslog { port => 5000 } }. Do message uložené v Elasticsearch se tak dostane několik dalších informací, mimo jiné název kontejneru a název stacku. Taková zpráva vypadá následovně:

2016-05-03T22:32:26.510Z 172.17.0.5 <14>1 2016-05-03T22:32:26Z 79445d78c31a hopeful_goldstine 1936 - - Hello world  

To však není úplně přehledné, je tedy ještě přidat sekci filter, která tuto zprávu zparsuje:

filter {  
  grok {
    match => { "message" => "%{SYSLOG5424PRI}%{NONNEGINT:ver} +(?:%{TIMESTAMP_ISO8601:ts}|-) +(?:%{HOSTNAME:service}|-) +(?:%{NOTSPACE:containerName}|-) +(?:%{NOTSPACE:proc}|-) +(?:%{WORD:msgid}|-) +(?:%{SYSLOG5424SD:sd}|-|) +%{GREEDYDATA:msg}" }
  }
  syslog_pri { }
  date {
    match => [ "syslog_timestamp", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss" ]
  }
  mutate {
    remove_field => [ "message", "priority", "ts", "severity", "facility", "facility_label", "severity_label", "syslog5424_pri", "proc", "syslog_severity_code", "syslog_facility_code", "syslog_facility", "syslog_severity", "syslog_hostname", "syslog_message", "syslog_timestamp", "ver" ]
  }
  mutate {
    remove_tag => [ "_grokparsefailure_sysloginput" ]
  }
  mutate {
    gsub => [
      "service", "[0123456789-]", ""
    ]
  }
  if ("" in [msg]) {
    mutate {
      rename => { "msg" => "message" }
    }
  }
  mutate {
    remove_field => [ "tags" ]
  }
}

Tím se ale stane zápis v docker-compose.yml dost nepřehledný, lepší je si vytvořit vlastní image - napsat Dockerfile. Jeden takový jsem vytvořil, je dostupný na Docker Hubu. Kromě zpracování zprávy ve formátu syslogu umí i zpracovat JSON, je tedy možné upravit vaši aplikaci tak, aby logovala ve formátu JSON a Logstash to automaticky zpracuje. Konečný docker-compose.yml bude vypadat následovně:

logspout:  
  image: gliderlabs/logspout:v3
  command: 'syslog://logstash:5000'
  links:
    - logstash
  volumes:
    - '/var/run/docker.sock:/tmp/docker.sock'

logstash:  
  image: ludekvesely/logstash-json
  environment:
    - DROP_NON_JSON=false
    - STDOUT=false
  links:
    - elasticsearch

kibana:  
  image: kibana
  environment:
    - LOGSPOUT=ignore
  links:
    - elasticsearch
  ports:
    - '5601:5601'

elasticsearch:  
  image: elasticsearch
  environment:
    - LOGSPOUT=ignore

Nyní bude v Kibaně k dispozipi zalogovaná zpráva včetně názvu kontejneru a stacku. V případě, že byla zpráva ve formátu JSON, bude i ten zpracován. Ted už zbývá vytvořit par přehledných tabulek a grafů v Kibaně a uložit si je jako dashboard.

Poslední úpravou před spuštěním na produkci bude pravděpodobně nastavení volume pro Elasticsearch, aby data přežila restart kontejneru. Nezapomeňte zabezpečit Kibanu, aby nebylo přístupná celému světu (například pluginem do ES Shield/SearchGuard). Pokud nechcete Kibanu kontrolovat každý den, nastavte si notifikace (plugin do ES Watcher nebo ElastAlert).

Zdroje:

Luděk Veselý

PHP Developer