Содержание урока · 10 разделов
Контейнер и stdout: одна лента на процесс#
Контейнер пишет JSON в stdout и stderr — это весь вклад приложения в логи docker go-инфраструктуры. Дальше работает Docker: демон читает поток процесса и складывает записи согласно драйверу логирования. Драйвер по умолчанию — json-file: записи ложатся файлом на диск хоста, в каталог контейнера, и docker logs читает именно их. Важное свойство: zerolog выдаёт поток JSON-строк, и драйвер их не разбирает — он сохраняет как есть, поэтому лента в файле остаётся вашей: те же ключи level и message, что и в любом другом writer-е. STDERR уходит в ту же ленту с маркером потока, и docker logs печатает обе части; zerolog по умолчанию пишет всё в один writer из New. Если сервис ведёт и stdout, и файл — например, в переходной схеме — держите одно содержание в обоих местах: расхождение лент хуже каждой поодиночке. Файлы json-file живут на хосте и переживают рестарт контейнера, но стираются при его удалении — для истории дольше жизни контейнера нужен агент. Частая ошибка — вести логи в файл внутри контейнера: файл попадает в слой, теряется при пересоздании и не виден docker logs, а сборщикам нужен доступ в файловую систему конкретного контейнера. Контракт один: процесс — поток — драйвер — лента. Всё, что ушло в stdout, доедет; всё, что спрятано в файлах контейнера, исчезнет вместе с подом.
Минимальный Dockerfile для логирующего сервиса
GOFROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app
FROM alpine:3.19
COPY --from=build /bin/app /bin/app
STOPSIGNAL SIGTERM
CMD ["/bin/app"]Многоступенчатая сборка: компилятор остаётся на build-этапе, в рантайм уезжает один статический бинарник. Zerolog внутри настраивается по окружению и пишет в os.Stdout — CMD не перенаправляет потоки, лента уходит демону как есть. STOPSIGNAL SIGTERM фиксирует сигнал остановки: сервис штатно дописывает события и выходит.
Драйвер json-file: ротация через max-size#
По умолчанию у json-file нет ни ограничения размера, ни ротации: max-size по документации Docker равен unlimited, max-file — единице, и лента одного болтливого контейнера способна съесть диск целиком. Ротация включается опциями драйвера: max-size задаёт порог файла, max-file — количество кусков; старший стирается при переполнении. Рабочий пример: max-size=10m и max-file=3 — потолок тридцать мегабайт на контейнер, три файла по десять. Те же опции ставятся глобально в daemon.json — тогда применяются ко всем контейнерам хоста; флаги docker run перекрывают глобальные и удобны точечно. Нюанс, о котором узнают на инциденте: docker logs читает только текущий набор файлов, и стёртые ротацией куски не вернуть — если записи нужны дольше потолка, это задача системы сбора, а не драйвера. Рядом с json-file есть драйвер local — тоже файловый, со сжатием и собственными лимитами по умолчанию; выбирается теми же флагами, а ленты читаются демоном одинаково.
Запуск с лимитами и daemon.json
GO# запуск: драйвер и лимиты на один контейнер
docker run -d --name app --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 registry.local/app:1.4
# daemon.json: лимиты для всех контейнеров хоста
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
# чтение ленты
docker logs app --tail 100
docker logs -f --since 10m app--log-driver=json-file выбирает драйвер, каждая --log-opt добавляет опцию: файл по десять мегабайт, максимум три — потолок тридцать мегабайт на контейнер. daemon.json повторяет те же опции глобально и требует перезапуска демона; флаги запуска сильнее и работают без него. docker logs показывает хвост, -f следит, --since режет окно времени.
Если сервис пишет миллион записей в час, потолок «десять мегабайт, три файла» стирает ленту за минуты: docker logs --since 1h вернёт пустоту, и на разборе это выглядит как сбой логирования, хотя сервис честно писал. Если прикинуть объём заранее — средний размер записи zerolog двести-четыреста байт, умножьте на поток, — то max-size и max-file подбираются так, чтобы окно держало хотя бы сутки. Когда записи нужны дольше потолка, подключайте сбор агентом: драйвер не архив и не хранилище.
Чтение лент: docker logs и journalctl#
Чтение — то, что делают чаще всего, и обе утилиты дают минимум, за который не стыдно. docker logs app печатает ленту контейнера целиком, --tail 100 обрезает до хвоста, -f следит в реальном времени, --since 10m или --since с меткой времени режет окно — записи zerolog при этом печатаются как есть, JSON-строками, поэтому по ним удобно ходить jq-фильтрами прямо в терминале. Для bare-metal сервиса под systemd ленту собирает journald: процесс пишет в stdout, юнит подключает его к журналу, и journalctl -u app показывает записи именно этого сервиса. Флаги те же по смыслу: -f следит, --since режет окно, -n 100 ограничивает число строк. Разница глубже утилит: journald добавляет к каждой записи свои поля — юнит, hostname, момент приёма, — но JSON zerolog доезжает внутри без изменений, и поля событий остаются ваши. Практичный порядок чтения при разборе одинаковый у обеих утилит: сначала хвост через --tail, затем окно --since вокруг инцидента, затем -f после воспроизведения; формат --since понимает и абсолютные метки, и относительные значения вроде 10m. Смешивать потоки двух сервисов в одну ленту не нужно никогда: у каждого юнита свой журнал, и разделение уже сделано за вас.
Драйверы делятся на локальные и пересылающие. json-file и local пишут на диск хоста: записи лежат рядом с контейнером, а сборщик-агент — filebeat, promtail, vector — читает каталог и увозит в хранилище; схема предсказуема, потому что сетевые проблемы роняют агента, а не демон Docker. Пересылающие драйверы — fluentd, gelf, syslog — шлют записи по сети прямо из демона: минус агент, плюс сетевой сбой внутри самого демона, и лента теряется вместе с ним. Для старта честнее локальный драйвер плюс агент: тот же принцип, что у файловой схемы из урока про ротацию. Агент делает ещё одну услугу: переживает ротацию json-file — следит за файлами и переходит к новым кускам, не теряя хвост, — и буферизует записи на случай недоступности хранилища. Рядом в кластере живут соседи: Envoy или nginx-прокси пишут свои access-логи в собственный stdout — отдельный контейнер, отдельная лента. Общий сборщик складывает обе ленты в одном хранилище, а склеивает их не время, а request_id из middleware: тот же идентификатор, что ставит zerolog в свои записи, и тот же принцип корреляции, что в уроке про трейсы.
Путь записи на проде#
| Сценарий | Кто пишет | Кто собирает | Чем читать |
|---|---|---|---|
| Контейнер | zerolog в stdout | json-file на диске хоста | docker logs --tail, -f |
| Контейнер в кластере | stdout пода | агент filebeat или promtail | хранилище Loki |
| Bare-metal systemd | zerolog в stdout | journald | journalctl -u app -f |
| Файл из урока 09 | zerolog в lumberjack | агент по каталогу | tail -F, каталог бэкапов |
| Прокси рядом | Envoy в своём stdout | тот же сборщик | общее хранилище лент |
journald и rate limit: болтливый сервис#
journald ведёт счёт на каждый сервис: RateLimitIntervalSec задаёт окно, RateLimitBurst — число сообщений на окно; в типовой конфигурации это тридцать секунд и десять тысяч сообщений на юнит, директивы живут в journald.conf. Пока сервис в лимите, всё доезжает; как только всплеск превысил burst, новые записи отбрасываются до конца окна — молча, без предупреждения в самой ленте. Эффект на «болтливый» сервис ровно такой: в худший момент, когда записей больше всего, лента исчезает, и на разборе видна дыра вместо истории. Лечится не поднятием лимита до бесконечности, а объёмом: сводки вместо построчных логов из урока про антипаттерны, прореживание семплером, дисциплина уровней. Проверка простая: отправьте всплеск из теста и посчитайте записи journalctl-ом — часть пропадёт ровно на burst. Юнит-файл сервиса тоже участвует в схеме: StandardOutput по умолчанию уходит в журнал, и переопределять его стоит осознанно — вывод в файл силами systemd возвращает проблему ротации, только теперь в конфиге юнита. Отдельная настройка — Storage в journald.conf: значение auto хранит журнал на диске только при существующем каталоге /var/log/journal, иначе в памяти — это первый вопрос, когда после рестарта journalctl оказывается пуст.
Файловая схема с lumberjack из урока про ротацию в logrotate не нуждается: библиотека сама делит файл по размеру, сжимает бэкапы и чистит лишнее — вторая ротация поверх её работы только путает каталог и может стереть свежие куски. Logrotate нужен для файлов, которые пишет не ваш процесс: чужие демоны, приложения без собственной ротации, журналы старой схемы. Классическая ловушка — работа с живым файлом. Утилита копирует и переименовывает журнал, а процесс держит открытый файловый дескриптор на inode: после переименования он продолжает писать в старый файл, и «текущий» журнал пустеет, хотя место продолжает расти. Лечение — copytruncate: копия делается, затем файл усекается на месте, и дескриптор остаётся валидным; цена — окно между копированием и усечением, когда записи могут потеряться. На практике окно короткое, но существует: для медленного ночного журнала оно незаметно, для болтливого сервиса — реальная потеря, и тогда сигнал переоткрытия честнее. Альтернатива — сигнал: процесс ловит SIGHUP и переоткрывает файл, как умеет lumberjack методом Rotate. Для zerolog-вывода в stdout ни logrotate, ни copytruncate не существуют: ленту режет драйвер, а не утилита хоста.
Docker и systemd в песочнице не заводятся: она показывает тематическую программу на чистом Go — логгер печатает JSON в stdout, и лента выглядит ровно так, как её увидит docker logs. Полный цикл проверяется локально: соберите образ из урока, запустите с --log-driver=json-file и max-size=1m, насыпьте записей и посчитайте куски ленты в каталоге контейнера.
Чек-лист прод-логов одним списком#
- контракт: процесс пишет JSON в stdout; файлы внутри контейнера — потерянные логи
- json-file по умолчанию без ротации: max-size и max-file задают потолок ленты
- daemon.json — лимиты на весь хост, флаги docker run — точечно на контейнер
- docker logs --tail, -f, --since: чтение текущих файлов; стёртые куски не вернуть
- bare-metal: stdout собирает journald; journalctl -u app -f — лента юнита
- rate limit journald: всплеск сверх burst отбрасывается молча до конца окна
- локальный драйвер плюс агент предсказуемее пересылающего драйвера внутри демона
- lumberjack ротирует сам; logrotate и copytruncate — для чужих файлов и живых дескрипторов
Проверка: ротация json-file
Контейнер с json-file без настроек пишет год — что с логами?
Проверка: logrotate и дескриптор
После переименования журнала logrotate-ом процесс пишет в старый файл — почему?
Сколько записей доедет до journald?
Прочитайте код и выберите, что он выведет:
# journald.conf: RateLimitIntervalSec=30s, RateLimitBurst=10000
# сервис печатает 12000 Info-записей за 10 секунд
journalctl -u app --since "09:00:00" | wc -lСоберите чтение лент
Восстановите команды чтения: хвост контейнера и слежение за юнитом.
Dockerfile по шагам
Расставьте строки Dockerfile в правильном порядке: от build-этапа до команды запуска.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: логи на проде
Что дальше по курсу#
Инфраструктура проверяет принципы курса с удвоенной ставкой: спам из урока про антипаттерны доезжает до драйвера, потерянные единицы — до хранилища, двойная запись — до алертов. Дальше — итоговый проект: логгер для CLI-сервиса, где конфиг, уровни, файл и stdout собираются в один каркас и проверяются сборкой. Справочное закрепление: соберите Dockerfile из урока, запустите контейнер с лимитами и посчитайте куски ленты; затем поднимите тот же сервис systemd-юнитом на виртуалке и сравните journalctl -u с docker logs — записи должны совпасть, потому что writer один и тот же. Разница, если найдётся, обычно в обёртке: journald добавляет свои поля, docker — маркер потока, но JSON событий остаётся тем же артефактом. Если разница есть, она в инфраструктуре, и теперь вы знаете, где искать.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- zerolog ничего не знает про Docker и journald: контракт один — JSON в stdout, сбор — инфраструктура.
- Контейнер: один процесс — одна лента; файлы внутри контейнера теряются с подом.
- json-file по умолчанию без ротации: max-size и max-file включают лимит, daemon.json — на весь хост.
- docker logs --tail, -f, --since читает текущие файлы: стёртые ротацией куски не вернуть.
- Локальные драйверы пишут на диск хоста, пересылающие шлют по сети из демона.
- Bare-metal: stdout собирает journald; journalctl -u app -f — лента юнита.
- Rate limit journald отбрасывает всплеск сверх burst молча — объём лечится сводками и семплерами.
- Lumberjack ротирует сам; logrotate и copytruncate — для чужих файлов и живых дескрипторов.
Домашнее задание
Соберите Dockerfile из урока для своего сервиса и запустите контейнер с max-size=1m и max-file=3: насыпьте записей в цикле и посчитайте куски ленты в каталоге контейнера, затем docker logs --tail 50 — свежие записи должны остаться на месте. Повторите запуск без max-size и убедитесь, что файл растёт без ограничения. На машине с systemd соберите тот же сервис юнитом: journalctl -u покажет те же JSON-строки. Смоделируйте rate limit: всплеск из пары тысяч записей и подсчёт journalctl-ом — найдите границу. Разберите файловую схему с lumberjack: убедитесь, что logrotate к ней не подключён, и объясните, что случится при двойной ротации.
Личные заметкиПусто
Вопросы и ответы по уроку
Куда писать логи zerolog в контейнере — stdout или файл?
В stdout: контракт контейнера — процесс пишет в поток, демон Docker собирает ленту по драйверу. Файл внутри контейнера живёт в слое, теряется при пересоздании и не виден docker logs. Если нужна файловая схема — монтируйте каталог и настраивайте lumberjack, но для контейнера stdout и json-file покрывают типовой случай.
Почему docker logs пуст, хотя сервис работал?
Первая причина — ротация: docker logs читает только текущий набор файлов, а стёртые max-file-ом куски не возвращаются. Вторая — процесс пишет в файл внутри контейнера вместо stdout. Третья — rate limit journald на bare-metal: записи отброшены сверх burst. Считайте объём до настройки: потолок в тридцать мегабайт может стираться за минуты.
Нужен ли logrotate рядом с lumberjack?
Нет: lumberjack из урока про ротацию сам делит файл по размеру, сжимает бэкапы и чистит лишнее — вторая ротация путает каталог. Logrotate нужен для файлов, которые пишет не ваш процесс. Если всё же запускаете его на живом файле — включайте copytruncate: без него процесс продолжает писать в переименованный файл по открытому дескриптору.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила