Як організувати 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)
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5",
"compress": "true"
}
}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
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
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):
console.log(JSON.stringify({
level: 'info',
msg: 'request handled',
request_id: 'abc-123',
user_id: 42,
duration_ms: 87
}))З json-file, Docker wrap'ить це:
{"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
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)
docker logs -f --tail=100 api # tail і follow
docker logs --since=10m api # останні 10 хвилин
docker logs --until=2024-01-15T10:00:00 api # до timestampdocker logs працює лише з json-file, local і journald-driver. З fluentd, loki, syslog тощо, логи не лишаються на disk, і docker logs повертає нічого, бери centralized-backend замість.
CloudWatch на AWS
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.0Docker-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-фікс надійніший.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів