Сбор логов Docker-контейнеров с помощью OpenTelemetry
Перевод оригинальной статьи Collecting Docker Container Logs with OpenTelemetry.
Перевод сделан специально для телеграм-канала Мониторим ИТ. Подписывайтесь! Там еще больше полезных постов о мониторинге.
Когда в Docker-контейнерах что-то выходит из строя, команда docker logs позволяет довольно быстро приблизиться к поиску причины. Однако её ограничения становятся очевидными, когда количество хостов превышает число тех, к которым можно подключиться по SSH, контейнеры уже завершили работу, а поиск по логам разных сервисов требует ручного объединения вывода.
Передача логов с каждого хоста через конвейер OpenTelemetry решает эту проблему, однако процесс настройки не является очевидным. Получатель filelog кажется подходящим инструментом, пока вы не столкнётесь с ограничениями доступа к файловой системе, а альтернативные подходы зачастую оказываются значительно проще, чем кажется на первый взгляд.
В этом руководстве показано, как на практике реализовать прием логов Docker, а также как построить полностью работоспособный конвейер от начала до конца.
Как Docker собирает и хранит логи
По умолчанию Docker перехватывает все, что контейнер записывает в stdout и stderr, после чего передаёт эти данные драйверу логирования. Драйвер, используемый по умолчанию — json-file, — записывает эти потоки в файл на хосте.
/var/lib/docker/containers/<container-id>/<container-id>-json.log
Этот файл содержит по одному объекту JSON для каждой строки лога, включая текст сообщения, поток (stdout или stderr) и временную метку.
{
"log": "Starting cloud controller for cluster desktop",
"stream": "stderr",
"time": "2026-04-18T14:28:28.812549652Z"
}Проблема заключается в том, что эти файлы связаны с жизненным циклом контейнера. Когда контейнер удаляется, Docker удаляет вместе с ним и файл логов. Такое поведение является штатным для эфемерных сред, однако это означает, что нельзя полагаться исключительно на локальные файлы как на единственный источник достоверных данных.
Более эффективный подход заключается в запуске OpenTelemetry Collector на каждом хосте и настройке Docker таким образом, чтобы он передавал логи непосредственно в Collector в момент их записи контейнерами, полностью исключая необходимость доступа к файлам. Затем Collector пересылает эти логи в централизованную систему хранения, где можно выполнять поиск и корреляцию данных по всей инфраструктуре. Остальная часть этого руководства посвящена настройке именно такого решения.
Почему чтение файлов логов Docker зачастую является не лучшим выбором
Когда специалисты впервые интегрируют логи Docker в OpenTelemetry, наиболее очевидным решением кажется настройка получателя filelog на каталог /var/lib/docker/containers/ с последующим разбором файлов. Такой подход действительно работает, однако нередко сопровождается дополнительными сложностями.
JSON-оболочка, которую Docker добавляет вокруг каждой строки лога, не полностью соответствует исходному сообщению, записанному приложением. Получателю filelog требуется конвейер операторов, который извлечёт внешний JSON, получит содержимое поля log, затем, если приложение также пишет логи в формате JSON, разберёт внутренний JSON и сформирует корректную запись лога OpenTelemetry.
Кроме того, необходимо предоставить Collector доступ к сокету Docker или каталогу с логами контейнеров на каждом хосте. В средах, где такой доступ ограничен, либо при использовании управляемых сервисов контейнеризации, не предоставляющих доступ к файловой системе хоста, чтение файлов логов становится полностью невозможным.
Более простой подход: драйвер логирования Fluentd
Docker поддерживает драйвер логирования fluentd, который отправляет логи непосредственно на совместимую с Fluentd конечную точку сразу после их записи контейнером, вместо того чтобы сначала сохранять их в файлы и затем считывать.
Получатель fluentforward в OpenTelemetry Collector использует тот же протокол, поэтому можно полностью отказаться от использования самого Fluentd и принимать события непосредственно в Collector.
Схема выглядит следующим образом:
Логи передаются через сокет сразу после их создания, поэтому отсутствует необходимость в чтении файлов, а также не требуется настраивать конвейер операторов для удаления JSON-оболочки, добавляемой Docker.
Настройка драйвера логирования Docker
Сначала необходимо настроить Docker на использование драйвера логирования fluentd. Это можно сделать глобально в файле /etc/docker/daemon.json, чтобы настройка применялась ко всем контейнерам данного хоста.
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "localhost:24224",
"fluentd-async": "true",
"fluentd-sub-second-precision": "true",
"tag": "docker.{{.Name}}"
}
}Параметр fluentd-async играет здесь важную роль. Без него Docker блокирует запись логов контейнером до тех пор, пока Collector не подтвердит получение каждого сообщения. Если Collector окажется недоступен, контейнер немедленно прекратит работу.
При использовании асинхронного режима Docker буферизует сообщения локально и повторяет попытки их отправки в фоновом режиме. Однако если Collector так и не станет доступен, Docker сохранит не более fluentd-buffer-limit сообщений (по умолчанию 1 048 576), после чего начнёт молча отбрасывать новые записи.
Без параметра fluentd-sub-second-precision временные метки будут округляться до целых секунд. Это делает ненадёжным порядок логов и их корреляцию в сервисах, которые записывают более нескольких строк в секунду.
После изменения файла daemon.json необходимо перезапустить демон Docker. Следует учитывать, что новые настройки будут применены только к вновь созданным контейнерам, поэтому существующие контейнеры потребуется пересоздать:
sudo systemctl restart docker
Теперь, когда Docker начнёт пересылать логи контейнеров на порт 24224, потребуется экземпляр Collector, прослушивающий этот порт и принимающий поступающие данные.
Конфигурация для каждого сервиса в Docker Compose
Если вы не можете изменить конфигурацию демона Docker напрямую или если для отдельных сервисов требуются разные параметры, драйвер логирования можно настроить отдельно для каждого сервиса в файле Compose:
# docker-compose.yml
services:
api:
image: my-api:latest
logging:
driver: fluentd
options:
fluentd-address: localhost:24224
fluentd-async: "true"
fluentd-sub-second-precision: "true"
tag: docker.{{.Name}}
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
ports:
- 24224:24224При использовании Docker Compose необходимо указывать localhost, а не имя сервиса (otel-collector) для разрешения адреса Fluentd, поскольку этот адрес разрешается демоном Docker на хосте, где имена сервисов Compose отсутствуют в DNS.
Чтобы избежать повторения одинаковой конфигурации для нескольких сервисов, можно использовать YAML-якорь:
# docker-compose.yml
x-logging: &default-logging
driver: fluentd
options:
fluentd-address: localhost:24224
fluentd-async: "true"
fluentd-sub-second-precision: "true"
tag: docker.{{.Name}}
services:
api:
image: my-api:latest
logging: *default-logging
worker:
image: my-worker:latest
logging: *default-logging
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
ports:
- 24224:24224Обеспечение надежной асинхронной доставки
Асинхронный режим решает проблему обратного давления (back-pressure), однако создаёт другую проблему: если буфер в памяти заполняется быстрее, чем Collector успевает обработать накопленные данные, Docker начинает отбрасывать сообщения логов. По умолчанию буфер рассчитан на 1 048 576 событий, чего достаточно для большинства сервисов, однако при обнаружении потерь логов его размер можно увеличить:
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-async": "true",
"fluentd-buffer-limit": "2097152"
}
}По умолчанию Docker повторяет попытки доставки неограниченное количество раз, если Collector недоступен. Также можно настроить интервал ожидания между повторными попытками с помощью параметра fluentd-retry-wait, значение которого по умолчанию составляет одну секунду.
{
"log-opts": {
"fluentd-async": "true",
"fluentd-retry-wait": "2s"
}
}Если Collector работает на том же хосте, что и Docker, а именно такая схема используется чаще всего, вероятность того, что он окажется настолько медленным, чтобы вызвать переполнение буфера, крайне мала.
Настройка OpenTelemetry Collector
Вам потребуется использовать образ otel/opentelemetry-collector-contrib, поскольку получатель fluent_forward отсутствует в базовой поставке OpenTelemetry Collector.
docker pull otel/opentelemetry-collector-contrib:0.154.0
Вот минимальная конфигурация, настраивающая получатель fluent_forward для приема логов Docker:
# otelcol.yaml
receivers:
fluent_forward:
endpoint: 0.0.0.0:24224
processors:
resourcedetection/system:
detectors: [system]
system:
hostname_sources: [os]
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [fluent_forward]
processors: [resourcedetection/system]
exporters: [debug]Процессор resourcedetection/system автоматически добавляет имя хоста, что особенно полезно при корреляции логов, поступающих с нескольких хостов.
Добавьте Collector в качестве сервиса в файл Compose:
# docker-compose.yml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.154.0
volumes:
- ./otelcol.yaml:/etc/otelcol-contrib/config.yaml
ports:
- 24224:24224После перезапуска сервисов можно проверить логи Collector и убедиться, что логи Docker-контейнеров успешно поступают:
docker compose logs otel-collector -f --no-log-prefix
Вы должны увидеть записи логов, поступающие от ваших контейнеров, где поле Body содержит исходную строку лога, а получатель fluent_forward автоматически добавляет такие атрибуты, как container_name, container_id и source:
2026-06-08T07:43:51.763Z info ResourceLog #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.40.0
Resource attributes:
-> host.name: Str(4e940d4722ea)
-> os.type: Str(linux)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope
LogRecord #0
ObservedTimestamp: 1970-01-01 00:00:00 +0000 UTC
Timestamp: 2026-06-08 07:43:51 +0000 UTC
SeverityText:
SeverityNumber: Unspecified(0)
Body: Str({"timestamp": "2026-06-08T07:43:51+00:00","level": "info","pid": "29","client.address": "172.22.0.1","client.port": "34230","url.path": "/index.html","url.query": "","network.protocol.name": "HTTP/1.1","server.address": "localhost","server.port": "80","user_agent.original": "curl/8.5.0","http.request.method": "GET","http.request.header.referer": "","http.response.status_code": 200,"http.response.body.size": 615,"http.server.request.duration": 0.000,"trace_id": "cdc134217e5afb428b188b4271c3305f","span_id": "930898f94fe03e5b","parent_sampled": 0})
Attributes:
-> fluent.tag: Str(docker.nginx-server)
-> source: Str(stdout)
-> container_id: Str(18d07bfa79470448896c0f2fcbd0aa076dd0082fc8642f1c6339e6174c4b5bc6)
-> container_name: Str(/nginx-server)
Trace ID:
Span ID:
Flags: 0Начиная с этого момента можно приступать к дальнейшей настройке конвейера, чтобы привести эти логи в большее соответствие с моделью данных логов OpenTelemetry.
Например, атрибуты container_name и container_id в настоящее время находятся среди атрибутов записи лога, однако они описывают источник логов, а не содержимое конкретной записи. Поэтому их следует перенести в атрибуты ресурса. Для этого можно использовать процессор resource или процессор transform.
Поле Body также содержит необработанную строку JSON, включающую данные, которые должны располагаться в других элементах модели данных OpenTelemetry: уровень критичности, временную метку, контекст трассировки, а также такие атрибуты, как http.response.status_code и http.request.method. Разбор этого JSON с помощью процессора transform и перенос каждого поля в соответствующее место модели данных OpenTelemetry делает эти данные доступными для поиска и анализа, вместо того чтобы они оставались скрытыми внутри строки.
После того как вы убедитесь, что логи успешно поступают и записи полностью соответствуют модели данных OpenTelemetry, экспортёр debug можно заменить на экспортёр, который будет передавать логи в используемую вами платформу наблюдаемости.
Заключительные мысли
Построенный в этом руководстве конвейер намеренно остается минималистичным. Один получатель, несколько процессоров и один экспортёр. Этого достаточно, чтобы передавать логи с хоста в систему хранения, однако возможности дальнейшего развития значительно шире: можно разбирать JSON в поле Body, переносить поля в соответствующие атрибуты OpenTelemetry, отфильтровывать лишние данные до их записи в хранилище или направлять логи различных сервисов в разные наборы данных.
Руководство по OpenTelemetry Collector — хороший следующий шаг, если вы хотите научиться строить более сложные конвейеры, а руководство по Fluent Forward Receiver более подробно описывает работу получателя.
Подписывайтесь на телеграм-канал Мониторим ИТ, там еще больше полезной информации о мониторинге!