Skip to main content

Як організувати log management у Docker?

Log-management у Docker це дисципліна capturing, rotating і centralizing того, що container'и пишуть. Дефолт (json-file) працює для dev, але приховує сюрпризи у production: chatty-container може заповнити disk за години, lookup через багато container'ів tedious, логи однієї node зникають, якщо node вмирає. Реальний production-стек: stdout-only-app + log-driver з rotation + shipper, що виносить логи off-host.

Теорія

TL;DR

  • Twelve-Factor: писати у stdout/stderr; нехай platform обробляє решту.
  • Log-driver'и (json-file, journald, syslog, fluentd, loki, awslogs, gcplogs, splunk тощо) визначають, що відбувається з цими stream.
  • Default json-file пише у /var/lib/docker/containers/<id>/<id>-json.log. Без rotation за замовчуванням.
  • Set rotation через log-opts: max-size, max-file.
  • Для multi-host setup, ship-логи off-node: Loki, ELK, CloudWatch, Datadog.
  • Структуруй логи (JSON), не plaintext. Майбутній ти подякує.

Чому stdout-only

Якщо твій app пише у file всередині container:

  • Зупиняється на docker logs, Docker бачить лише stdout/stderr.
  • Зникає з container, якщо не mount'нув volume, а management-volume per container це busywork.
  • Перемагає log-driver'и, driver'и оперують на stdout/stderr.
  • Ламає orchestration, Swarm/K8s припускають stdout/stderr-семантику.

Twelve-factor-app пише у stdout. Platform (Docker, Compose, Swarm, K8s) направляє куди треба.

Built-in log-driver'и

DriverЩо робитьКоли використовувати
json-fileДефолт. Пише JSON на disk host.Dev, малий ops.
localЯк json-file, але binary, швидше, має rotation built-in.Малий ops, нижчий overhead.
journaldШле у systemd-journal.Linux-host'и, що використовують journald централізовано.
syslogШле у syslog-daemon (локальний або remote).Legacy, простий central-collection.
fluentdШле у Fluentd/Fluent Bit-aggregator.Зрілий, vendor-neutral central-pipeline.
lokiШле у Grafana Loki.Сучасний, дешевий, інтегрується з Grafana.
awslogsШле у AWS CloudWatch Logs.AWS-деплої.
gcplogsШле у GCP Cloud Logging.GCP-деплої.
splunkШле у Splunk HEC.Splunk-shop'и.
noneДропає логи.Тести, де байдуже.

Куди логи їдуть за замовчуванням

/var/lib/docker/containers/<id>/ ├── <id>-json.log # активний log-file ├── <id>-json.log.1 # rotated (якщо rotation set) └── <id>-json.log.2.gz # rotated, compressed

Приклади

Ставити host-wide rotation (production must-have)

json
// /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "5", "compress": "true" } }
bash
sudo systemctl restart docker

Тепер кожен новий container cap'ить на 5 файлах по 100 MB = 500 MB worst case, зі старими файлами compressed.

Нота: існуючі container'и тримають свій старий log-config до recreation. Якщо ти ставиш це на сервер, де вже крутяться container'и, ті container'и ще можуть заповнити disk. Recreate або per-container override.

Per-container override

bash
docker run -d \ --log-driver=json-file \ --log-opt max-size=50m \ --log-opt max-file=3 \ --name=app \ myorg/app

Корисно, коли один chatty-сервіс потребує різних limit.

Compose з loki-driver

yaml
services: loki: image: grafana/loki:2.9.0 ports: ["3100:3100"] grafana: image: grafana/grafana:10 ports: ["3000:3000"] depends_on: ["loki"] api: image: myorg/api:1.0 logging: driver: loki options: loki-url: "http://loki:3100/loki/api/v1/push" loki-retries: "5" loki-batch-size: "400" loki-external-labels: "job=api,env=prod"

loki-external-labels додає tag, щоб ти міг filter by service у Grafana. Цей driver потребує docker-driver plugin (one-time install): docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions.

Structured-logging (JSON)

App-side (Node.js example):

js
console.log(JSON.stringify({ level: 'info', msg: 'request handled', request_id: 'abc-123', user_id: 42, duration_ms: 87 }))

З json-file, Docker wrap'ить це:

json
{"log": "{\"level\":\"info\",\"msg\":\"request handled\",...}\n", "stream": "stdout", "time": "..."}

Зовнішній wrapper Docker'а; внутрішній JSON твій structured-log. Loki, ELK тощо парсять inner-JSON, і ти можеш query {job="api"} |= "abc-123" або filter by user_id.

Fluentd / Fluent Bit-pipeline

yaml
services: fluentbit: image: fluent/fluent-bit:2.2 ports: ["24224:24224"] volumes: - ./fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro api: image: myorg/api:1.0 logging: driver: fluentd options: fluentd-address: localhost:24224 tag: api # Якщо fluent-bit unreachable, drop'ай логи, щоб не блокувати app: fluentd-async: "true"

Fluent Bit-config може route'ити ті ж логи у кілька destination: hot-search-index за останній день, cold-storage для compliance.

Читання логів з Docker (і limit'и docker logs)

bash
docker logs -f --tail=100 api # tail і follow docker logs --since=10m api # останні 10 хвилин docker logs --until=2024-01-15T10:00:00 api # до timestamp

docker logs працює лише з json-file, local і journald-driver. З fluentd, loki, syslog тощо, логи не лишаються на disk, і docker logs повертає нічого, бери centralized-backend замість.

CloudWatch на AWS

bash
docker run -d \ --log-driver=awslogs \ --log-opt awslogs-region=us-east-1 \ --log-opt awslogs-group=myapp \ --log-opt awslogs-stream=api-1 \ --log-opt awslogs-create-group=true \ myorg/api:1.0

Docker-daemon потребує IAM-permission писати у CloudWatch (logs:CreateLogStream, logs:PutLogEvents). На EC2 з instance-role, додай policy; на Fargate, task-role обробляє.

Kubernetes-style: kubelet + sidecar

Docker driver-less варіант: лиши логи на disk через json-file, запусти per-host-shipper (Fluent Bit, Promtail, Vector), що читає /var/lib/docker/containers/*/*-json.log і ship'ить. Decouples app-deploy від log-destination.

Реальне застосування

  • Tiny / hobby: json-file з rotation. Читай через docker logs.
  • Single-host з monitoring: Loki + Promtail + Grafana, читання /var/lib/docker/containers.
  • Multi-host self-hosted: Fluent Bit per host → Elasticsearch/OpenSearch + Kibana, або Loki + Grafana.
  • Cloud: native, awslogs, gcplogs, Datadog, Splunk.
  • Compliance-environment: довгострокове cold-storage (S3) + hot-index за останні 30 днів.

Типові помилки

Писати у log-файли всередині container

Java-app з log4j, що пише у /var/log/app.log, перемагає docker logs і зникає з container. Конфігуруй log4j писати у console (stdout).

Без rotation на json-file

Дефолтний Docker без log-rotation. Chatty-app на default-config заповнить disk. Завжди ставь max-size і max-file.

Logging sensitive data

Пароль, JWT, PII з'являються в логах, бо хтось console.log(req.body). Sanitize у джерелі. Centralized log-indexing робить leak легким для пошуку, і легким для scrape.

Бери docker logs у production для всього

Працює на single-host, але не масштабується. Centralize рано; retroactively додавати log-shipping посеред incident'у боляче.

Не ставити compress: true

Rotated .log.N-файли unrotated і uncompressed сидять на disk у full-розмірі. compress: "true" gzip'ить їх, часто 10x менше.

Питання для поглиблення

Q: Чому docker logs повертає нічого для деяких container?


A: Цей container використовує non-disk-driver (fluentd, loki, awslogs). Логи shipping, не storing. Query destination замість.

Q: Різниця між json-file і local?


A: Обидва зберігають на disk на host. local використовує binary-format (менший, швидший), підтримує built-in rotation і рекомендується для нових setup. json-file старший і historical-default. Функціонально similar для docker logs.

Q: JSON чи plaintext-логи?


A: JSON. Сучасні log-backend парсять його natively, можеш filter by field (level=error, user_id=42). Plaintext OK для tiny-сервісів, але не масштабується.

Q: (Senior) Які trade-off між fluentd-async: true і synchronous?


A: Synchronous: якщо Fluent Bit down, Docker-daemon блокує на write(), що може hang container'и. Async: Docker буферизує (малий, in-memory) і продовжує; на overflow, логи drop. Для app-стабільності, async з generous-буфером безпечніший; для compliance, де log-loss unacceptable, запускай high-availability log-aggregator (failover, persistent-queue) і бери sync.

Q: (Senior) Як обробляти multi-line stack-trace у логах?


A: stdout пише кожну лінію як окрему подію. 30-лінійний Java stack-trace стає 30 unrelated log-entry. Два фікс: (1) Нехай app emit single JSON-event з повним trace як string-field. (2) Бери log-shipper (Fluent Bit, Filebeat, Vector) з multiline-парсинг-правилами, що join'ять лінії, починаючи з whitespace, у попередню подію. Source-side-фікс надійніший.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів