Přejít k navigační liště

Zdroják » Různé » Grafana + Loki + Vector: logování bez zbytečné vědy

Grafana + Loki + Vector: logování bez zbytečné vědy

Články Různé, Webdesign

Docker logy nemusíte hledat ručně na každém serveru. Ukážeme si, jak pomocí Vectoru sbírat logy z Dockeru i journald, ukládat je do Loki a pohodlně procházet v Grafaně. Vše prakticky, pomocí Docker Compose a konkrétních příkladů konfigurace.

Logy v docker compose logs jsou fajn, dokud máte jeden server. Jakmile je serverů nebo služeb víc, začne být pohodlnější posílat všechno na jedno místo. Jenže tohle je často složitější, než to vypadá.

Pokud nechcete své logy posílat přes internet do komerční služby, jako je Datadog Logs nebo Better Stack, začínají být vaše možnosti značně omezené.

První, co se nabízí, je použít ELK (Elasticsearch + Logstash + Kibana), ale každý, kdo ELK Stack zkoušel, mi jistě dá za pravdu, že není úplně jednoduché ho nastavit, rozchodit a hlavně udržovat. Další možností je Graylog, ale tohle řešení je snad ještě horší. Všechna tahle řešení jsem zkoušel a několik let také provozoval.

Jako vždy je tady ještě jedna možnost (ta nejlepší), na kterou mě přivedl Jakub Onderka. V tomhle článku si ukážeme jednoduchý logovací stack postavený na třech službách:

  • Vector sbírá logy z Dockeru a upravuje je,
  • Loki je ukládá a umí v nich hledat,
  • Grafana nad nimi nabízí webové rozhraní.

Výsledkem bude lokální setup v Docker Compose. Není to Kubernetes, nepotřebujete Prometheus a první log uvidíte za pár minut.

Schéma toku Docker a journald logů přes Vector do Loki a Grafany

Jak data tečou

Celé řešení vypadá takhle:

Docker kontejnery -> Vector -> Loki -> Grafana

Vector čte Docker socket, takže automaticky vidí stdout a stderr všech kontejnerů na hostiteli. Každý záznam převede na JSON, přidá k němu název kontejneru a služby a odešle ho do Loki.

Loki není Elasticsearch. Neindexuje celý obsah každého řádku, ale především takzvané labels. Díky tomu má menší nároky na disk i paměť. O to důležitější je rozmyslet si, co do labels uložíte. host, service nebo container jsou dobrá volba. ID requestu nebo ID uživatele už ne, protože vytvářejí příliš mnoho unikátních streamů.

Struktura projektu

Vytvořte si následující soubory:

logging/
├── compose.yml
├── loki-config.yml
├── vector.yaml
└── provisioning/
    └── datasources/
        └── loki.yml
Code language: plaintext (plaintext)

Grafaně rovnou připravíme datasource pomocí provisioningu. Po prvním spuštění tedy nebudete muset nic ručně naklikávat.

Docker Compose

Do compose.yml vložte tři služby:

services:
  loki:
    image: grafana/loki:latest
    restart: unless-stopped
    command: -config.file=/etc/loki/local-config.yaml
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki

  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:
      - grafana-data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning:ro

  vector:
    image: timberio/vector:latest-alpine
    restart: unless-stopped
    command: --config /etc/vector/vector.yaml
    depends_on:
      - loki
    volumes:
      - ./vector.yaml:/etc/vector/vector.yaml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
  loki-data:
  grafana-data:

Code language: YAML (yaml)

Používám tady tag latest, aby byl příklad krátký. V produkci si verze připněte, aby vám nešedivěly vlasy. Upgrade Loki může vyžadovat úpravu konfigurace a nechcete ho řešit náhodou při obyčejném restartu serveru.

Port 3000 patří Grafaně. Port 3100 je API Loki, ne webové rozhraní. Když na http://localhost:3100/ dostanete 404, není to chyba, koukněte na http://localhost:3100/metrics, tam budou vidět základní metriky.

Konfigurace Loki

Pro první spuštění vystačíme s lokálním diskem a TSDB indexem. Do loki-config.yml vložte:

auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /loki
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

storage_config:
  filesystem:
    directory: /loki/chunks
  tsdb_shipper:
    active_index_directory: /loki/index
    cache_location: /loki/index_cache

limits_config:
  retention_period: 14d
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  discover_log_levels: true

compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  delete_request_store: filesystem
Code language: YAML (yaml)

auth_enabled: false vypíná multi-tenancy. Pro lokální síť nebo jeden interní server je to pohodlné, ale Loki pak sám nikoho neověřuje. Port 3100 proto nevystavujte volně do internetu. Dejte před něj reverzní proxy s autentizací, firewall, nebo klidně obojí.

Retenci jsme nastavili na 14 dní. Samotné retention_period nestačí, mazání musí provádět také compactor s retention_enabled: true.

Grafana bez klikání

Datasource nakonfigurujeme souborem provisioning/datasources/loki.yml:

apiVersion: 1

datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    isDefault: true
    editable: true
Code language: YAML (yaml)

Adresa je http://loki:3100, nikoli localhost. Uvnitř Compose sítě se služby hledají podle svého názvu. localhost by z pohledu Grafany znamenal kontejner Grafany.

Vector sbírá Docker logy

Teď přichází nejdůležitější část. Do vector.yaml vložte:

sources:
  docker:
    type: docker_logs
    exclude_containers:
      - vector
      - loki
      - grafana

transforms:
  normalize:
    type: remap
    inputs:
      - docker
    source: |
      .message = strip_ansi_escape_codes!(string!(.message))
      .container = .container_name
      .service = .label."com.docker.compose.service" ?? .container_name
      .project = .label."com.docker.compose.project" ?? "docker"

sinks:
  loki:
    type: loki
    inputs:
      - normalize
    endpoint: http://loki:3100
    encoding:
      codec: json
    labels:
      source: docker
      container: "{{ container }}"
      service: "{{ service }}"
      project: "{{ project }}"
    out_of_order_action: accept
    dangerously_allow_unconfined_template_resolution: true
Code language: YAML (yaml)

Zdroj docker_logs poslouchá Docker socket. Transformace v jazyce VRL odstraní ANSI barvy z konzole a vytáhne Docker Compose labels. Sink pak odešle celý záznam jako JSON do Loki.

Vector schválně nesbírá vlastní logy ani logy Loki a Grafany. Jinak si snadno vyrobíte šum, případně nepříjemnou logovací smyčku.

Konfiguraci si ještě před spuštěním ověřte:

docker compose run --rm vector validate /etc/vector/vector.yamlCode language: Shell Session (shell)

Vector má poměrně přísnou validaci. Překlep v názvu transformace nebo neexistující input tak odhalíte dřív, než začnete hledat chybu v Loki.

První spuštění

Stack nastartujte:

docker compose up -d
docker compose psCode language: Shell Session (shell)

Ověřte, že Loki odpovídá:

curl -s http://localhost:3100/loki/api/v1/labels | jqCode language: Shell Session (shell)

Pak spusťte kontejner, který bude něco zapisovat:

docker run --rm alpine sh -c \
  'echo "Ahoj z Dockeru"; echo "Něco se pokazilo" >&2'Code language: Shell Session (shell)

Otevřete http://localhost:3000, přihlaste se jako admin / admin a přejděte do Explore. Vyberte datasource Loki a zkuste LogQL dotaz:

{source="docker"}Code language: JSON / JSON with Comments (json)

Jen konkrétní službu najdete pomocí labelu:

{source="docker", service="api"}Code language: JavaScript (javascript)

A text v logu pomocí filtru:

{source="docker"} |= "pokazilo"Code language: Shell Session (shell)

Protože Vector posílá JSON, můžete parsovat jednotlivá pole až při dotazu:

{source="docker"} | json | message =~ "(?i)error|failed|exception"Code language: Shell Session (shell)

Tohle je důležitý princip Loki. Stabilní metadata patří do labels, proměnlivá data zůstávají v těle logu a parsují se až při hledání.

Co se severity

Grafana umí nad logy nabídnout filtr podle úrovně, ale nejdřív jí musíte dát použitelná data. Nejjednodušší je logovat z aplikace strukturovaný JSON:

{"level":"error","message":"Platba se nepodařila","order_id":1234}Code language: JSON / JSON with Comments (json)

Loki s discover_log_levels: true běžně pozná pole jako level nebo severity. Pokud aplikace posílá obyčejný text, můžete úroveň dopočítat ve Vectoru. Například pro Nginx lze odpovědi 500 a vyšší označit jako error, odpovědi 400499 jako warning a zbytek jako info.

Nedělejte ale z level automaticky label. Nízká kardinalita je sice bezpečná, ale structured metadata nebo JSON pole obvykle stačí a dovolí Grafaně úroveň rozpoznat bez vytváření dalšího streamu.

Journald místo souborů

Na serveru často nechcete jen Docker logy. Vector umí číst také journald (případně jakékoliv další textové logy):

sources:
  journal:
    type: journald
    current_boot_only: true

sinks:
  loki:
    inputs:
      - normalize
      - journalCode language: YAML (yaml)

V praxi je nejjednodušší spouštět Vector jako systemd službu přímo na hostiteli. Kontejner by potřeboval připojit journal adresáře a obvykle také /etc/machine-id. Agent na každém serveru pak může posílat do jednoho centrálního Loki label host, podle kterého v Grafaně snadno poznáte původ záznamu.

Postup vypadá následovně. Nejprve odstraňte z compose.yml službu vector, jinak budou Docker logy zdvojené. Pak přímo na stroji nainstalujte Vector:

curl --proto '=https' --tlsv1.2 -sSfL https://sh.vector.dev | bash
sudo apt-get install vector
sudo systemctl enable --now vectorCode language: Shell Session (shell)

Nastavte oprávnění:

sudo usermod -aG docker vector
sudo usermod -aG systemd-journal vector
sudo systemctl restart vectorCode language: Shell Session (shell)

Přístup ověřte:

sudo -u vector docker ps
sudo -u vector journalctl -n 1Code language: Shell Session (shell)

Oba příkazy musí fungovat bez chyby permission denied. Poté stačí vytvořit /etc/vector/vector.yaml, který bude obsahovat všechny zdroje:

data_dir: /var/lib/vector

sources:
  docker:
    type: docker_logs

  journal:
    type: journald
    current_boot_only: true

transforms:
  docker_labels:
    type: remap
    inputs:
      - docker
    source: |
      .message = strip_ansi_escape_codes!(string!(.message))
      .source = "docker"
      .container = .container_name
      .service = .label."com.docker.compose.service" ?? .container_name

      if exists(.label."com.docker.compose.project") {
        .project = .label."com.docker.compose.project"
      } else {
        .project = "docker"
      }

  journal_labels:
    type: remap
    inputs:
      - journal
    source: |
      .source = "journald"
      .container = "system"
      .project = "system"
      .service = ._SYSTEMD_UNIT ?? .SYSLOG_IDENTIFIER ?? "journald"

      priority = to_int(.PRIORITY) ?? 6

      if priority <= 3 {
        .severity = "error"
      } else if priority == 4 {
        .severity = "warning"
      } else {
        .severity = "info"
      }

      message = downcase(to_string(.message) ?? "")

      if contains(message, "segfault") {
        .severity = "error"
      }

sinks:
  loki:
    type: loki
    inputs:
      - docker_labels
      - journal_labels
    endpoint: http://logs.example.com:3100
    encoding:
      codec: json
    labels:
      host: "{{ host }}"
      source: "{{ source }}"
      container: "{{ container }}"
      service: "{{ service }}"
    out_of_order_action: accept
    dangerously_allow_unconfined_template_resolution: true
    buffer:
      type: disk
      max_size: 1073741824
      when_full: blockCode language: YAML (yaml)

Veškeré logy se teď budou posílat na adresu http://logs.example.com:3100, tady by vám měla běžet Grafana a Loki. Obě transformace vytvářejí stejná základní pole: host, source,container a service. Při ladění konfigurace se vám bude hodit validace:

sudo vector validate /etc/vector/vector.yamlCode language: Shell Session (shell)

A samozřejmě budete muset službu po změně konfigurace restartovat:

sudo systemctl restart vector
sudo systemctl status vectorCode language: Shell Session (shell)

Když něco nefunguje, koukněte do logů samotného Vectoru:

sudo journalctl -u vector -fCode language: Shell Session (shell)

Produkční provoz

Lokální filesystem je ideální pro demo a menší instalaci. Pokud chcete oddělit data od jednoho serveru, Loki umí ukládat chunks a TSDB index například do AWS S3.

Závěrem

Grafana, Loki a Vector tvoří příjemně malý stack. Vector se postará o sběr a normalizaci, Loki o levné uložení a LogQL dotazy a Grafana o procházení logů. Nemusíte kvůli tomu stavět velký observability cluster.

Začněte Docker logy z jednoho serveru. Jakmile setup funguje, přidejte journald, další hostitele, rozumné labels a případně S3. Největší službu přitom uděláte sami sobě tím, že aplikace budou od začátku logovat strukturovaný JSON. Všechno ostatní se pak hledá podstatně snáz.

Zdroje

Komentáře

Odebírat
Upozornit na
guest
1 Komentář
Nejstarší
Nejnovější Nejvíce hlasů
Michal Špondr

@zdrojak Hmm, osobně používám Loki Docker driver. https://grafana.com/docs/loki/latest/send-data/docker-driver/
Budu se muset zamyslet, co je tedy vhodnější, protože na první dobrou mi výsledek přijde stejný.