Логи веб-сервера — это не «архив для галочки», а главный источник правды о том, что реально происходило на периметре: кто пришёл, куда пошёл, сколько заняло времени, какой код ответа вернулся, какой апстрим тормозил и где возникла ошибка.
В продакшене всё чаще ожидают structured logging: когда каждая запись — это объект (обычно JSON), который удобно парсить, фильтровать и коррелировать.
Ниже — практичное сравнение логирования в трёх популярных решениях: Nginx (классика), Caddy (простая конфигурация и structured logs «из коробки») и Traefik (edge-прокси, часто в Docker/Kubernetes). Фокус: access/error, JSON-формат, уровни ошибок, journald vs файлы, ротация и корреляция по request_id.
Что считать «хорошими» логами для веб-периметра
Перед сравнением зафиксируем цели. Схема логирования должна отвечать на вопросы:
- Кто (клиент/реальный IP/прокси-цепочка) сделал запрос и куда (host, uri, method)?
- Что получилось (status, bytes, cache status) и как быстро (общая задержка, времена апстрима)?
- Почему сломалось (уровень ошибки, контекст, апстрим, таймауты)?
- Как связать события в одну цепочку edge → app → db (корреляция по request id / trace id)?
Практичный минимум полей для JSON access-лога: timestamp, host, method, uri, status, bytes, referer, user_agent, remote_addr, x_forwarded_for, request_time, upstream_addr, upstream_status, upstream_response_time, request_id.
Если вы настраиваете централизованный сбор, полезно сразу договориться о едином имени поля: например, везде использовать request_id в JSON и заголовок X-Request-Id на проводе.
Journald vs файлы: что выбрать
Оба подхода рабочие, но под разные сценарии:
- Файлы удобны там, где вы привыкли к ротации, бэкапам и «прочитать хвост» через
tail. Для Nginx это стандарт де-факто. - journald удобен в systemd-мире: единый сбор, лимиты диска политиками journald, быстрый поиск через
journalctl. Часто выбирают для Caddy/Traefik и для контейнеров (stdout/stderr собирает платформа).
Главное правило: куда бы вы ни писали, формат должен быть одинаковым и машиночитаемым. Смешивать «красивые» строки и JSON в одном потоке — почти всегда боль для наблюдаемости.
Если вы строите инфраструктуру на systemd и хотите подтянуть алерты/ретеншн «по-взрослому», пригодится статья про централизованный сбор journald через systemd-journal-remote.
Nginx: JSON access logs, уровни ошибок и корреляция
Nginx исторически ориентирован на файловые логи. Structured logging делается через log_format и подбор переменных. Плюс: максимальная гибкость и контроль. Минус: формат нужно аккуратно собирать и поддерживать, иначе легко получить «почти JSON», который ломает парсер.
Access JSON: базовый формат и полезные поля
Пример практичного JSON access-формата (обратите внимание на времена апстрима и идентификатор запроса):
http {
log_format json_access escape=json '{'
'"ts":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"xff":"$http_x_forwarded_for",'
'"host":"$host",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes":$body_bytes_sent,'
'"referer":"$http_referer",'
'"ua":"$http_user_agent",'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"request_id":"$request_id"'
'}';
access_log /var/log/nginx/access.json json_access;
}
Ключевые моменты:
escape=jsonзащищает JSON от кавычек и спецсимволов вUser-Agent, referer и прочих строковых полях.$request_time— полное время обработки на стороне Nginx (включая ожидание апстрима).$upstream_response_timeи$upstream_status— база для диагностики 502/504 и ответа на вопрос «кто тормозит».
Корреляция по request id: чтобы реально работало
Идея простая: один и тот же идентификатор должен пройти через edge и приложение, и попасть в логи с одинаковым именем. На практике важны два шага: (1) принять входящий X-Request-Id, если он уже пришёл от CDN/балансировщика, (2) если его нет — сгенерировать и дальше везде пробрасывать.
Типовой паттерн проброса в апстрим:
location / {
proxy_set_header X-Request-Id $request_id;
proxy_pass http://app_upstream;
}
Дальше приложение логирует X-Request-Id как request_id — и вы склеиваете цепочку без APM: фильтруете по одному значению и видите весь путь запроса.
Развёрнутый разбор (включая нюансы с проксированием и тем, где именно удобнее генерировать id) есть в материале про X-Request-Id для Nginx и PHP-FPM.
Error log levels: что включать на проде
У Nginx error-лог — отдельный поток, где важны уровни: debug, info, notice, warn, error, crit, alert, emerg. На бою обычно ставят warn или error, а debug включают точечно и на короткий период.
error_log /var/log/nginx/error.log warn;
Если расследуете нестабильные 499/502/504, временно поднимайте уровень до info или notice и заранее убедитесь, что ротация не даст забить диск.
logrotate для Nginx: безопасная ротация без потери логов
Nginx держит файловые дескрипторы открытыми; простого переименования файла недостаточно — нужно послать сигнал на переоткрытие логов. Классический вариант — USR1.
Шаблон правила logrotate:
/var/log/nginx/*.log /var/log/nginx/*.json {
daily
rotate 14
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/usr/sbin/nginx -t -q
/bin/systemctl kill -s USR1 nginx
endscript
}
kill -s USR1заставляет Nginx закрыть старые дескрипторы и открыть новые файлы.delaycompressснижает риск гонок, когда кто-то ещё читает «вчерашний» файл.
Если вам нужен предсказуемый периметр с изоляцией сервисов и удобным доступом к логам (и при этом вы хотите полный контроль над конфигурацией), часто проще вынести edge на отдельную виртуальную машину. Для таких задач обычно выбирают VDS под Nginx/Traefik с понятной политикой хранения логов и отдельным диском под ретеншн.

Caddy: structured logging «из коробки»
Caddy часто выбирают за простоту: HTTPS автоматом, конфиг лаконичный, и при этом логи пригодны для наблюдаемости без «самосборного» JSON. Он нормально живёт и с файлами, и с journald (через systemd unit и вывод).
Access JSON в Caddyfile: минимум боли
В Caddy логирование настраивается декларативно: вы задаёте логгер и формат (JSON — стандартный выбор). Пример, который обычно устраивает прод:
{
log {
format json
level INFO
}
}
example.com {
reverse_proxy 127.0.0.1:8080
log {
format json
output file /var/log/caddy/access.json
}
}
- JSON-формат в Caddy реально структурированный и удобный для отправки в Loki/ELK/ClickHouse.
levelвлияет на системные логи Caddy, а access-лог настраивается отдельно.
Корреляция запросов: как жить без «родного» request_id
В реальности request id почти всегда генерируется на edge. Если ваш edge — Caddy, проще всего стандартизировать заголовок X-Request-Id: принимать входящий (если он уже пришёл) и пробрасывать в апстрим. Дальше приложение пишет этот идентификатор в свои JSON-логи.
Даже если в вашей схеме access-логов Caddy заголовок не выведен отдельным полем «как есть», он всё равно останется в запросе и будет жить в логах приложения, а на edge вы сможете коррелировать по системным событиям/маршрутизации и времени.
Файлы или journald
Для systemd-сервиса типовая схема такая: access-лог — в файл (чтобы проще считать объём и ретеншн «по трафику»), а системные события (ошибки, перезапуски, ACME) — в journald. Так вы разделяете «шум» и «трафик» и проще настраиваете хранение.
Traefik: JSON access logs и контроль шума
Traefik — edge-прокси, который часто живёт в Docker/Kubernetes. В таких окружениях естественный путь — писать в stdout/stderr, а сбор и ретеншн делегировать платформе. При этом Traefik умеет access-логи и JSON, а ещё позволяет управлять полями и заголовками, сдерживая объём.
Access log: JSON и выбор полей
Цель — получить JSON с предсказуемыми ключами и не захлебнуться в объёме. Концептуальный пример статической конфигурации:
accessLog:
format: json
fields:
defaultMode: keep
headers:
defaultMode: drop
names:
X-Request-Id: keep
X-Forwarded-For: keep
User-Agent: keep
- По умолчанию оставляем основные поля запроса/ответа.
- Заголовки — дорогие по объёму, поэтому «дропаем всё» и сохраняем только то, что нужно для корреляции и диагностики.
Уровни ошибок и «почему так шумно»
В Traefik важно различать два потока:
- Access logs: запись на каждый запрос (самый большой поток).
- Сервисные логи: конфигурация, провайдеры, ACME, ошибки роутинга, рестарты.
Если поднять слишком подробный уровень сервисных логов, можно получить лавину событий при флапающих апстримах или частых обновлениях конфигурации (особенно в Kubernetes). На проде держите уровень пониже и включайте детализацию точечно.
request_id correlation в Traefik
Traefik часто стоит «первым» в цепочке, поэтому удобен как точка стандартизации: один заголовок X-Request-Id, проброс дальше, логирование на каждом уровне. Даже без полноценного трейсинга это сильно ускоряет расследования.

Сравнение: что выбрать под ваши задачи
Когда Nginx выигрывает
- Нужен детальный контроль формата, максимум переменных и тонкая настройка.
- У вас уже выстроен пайплайн вокруг файлов и logrotate.
- Критичны метрики по апстримам в access-логах (upstream timings) и привычная модель error-логов.
Когда Caddy удобнее
- Хотите меньше конфигов и при этом сразу structured logging в JSON.
- Нужно быстро поднять периметр с понятным логированием без доводки форматов.
- Нравится гибрид: системные события в journald, access — в файл (или тоже в journald, если так проще).
Когда Traefik логичнее
- Вы в Docker/Kubernetes и хотите нативный подход: stdout + централизованный сбор.
- Нужно управлять объёмом access-логов через выбор полей/заголовков.
- Важна интеграция с динамическими провайдерами и частыми изменениями маршрутизации.
Практический чек-лист: приводим логи к виду «готово для observability»
- Единый JSON-формат для access-логов на всех edge-узлах.
- Единый request id: заголовок + запись в логи на edge и в приложении.
- Минимально достаточный набор полей: не логируйте всё подряд, особенно заголовки.
- Разделяйте потоки: access отдельно от error/service.
- Ретеншн и ротация: для файлов — logrotate + корректный reopen; для journald — лимиты и политика хранения.
- Проверка под нагрузкой: включите больше полей на час в пиковое время и оцените рост объёма/стоимости хранения.
Типовые ошибки, из-за которых расследования затягиваются
- Нет корреляции: edge и приложение логируют «каждый своё», request id отсутствует.
- Смешаны форматы: часть строк JSON, часть — произвольный текст, парсер ломается.
- Нет upstream timing: 504 получается «в вакууме», непонятно, кто виноват.
- Слишком высокий уровень ошибок постоянно: error-лог превращается в поток, где сложно найти сигнал.
- Ротация сделана неправильно: дескрипторы не переоткрываются, место на диске не освобождается, логи «пропадают».
Итог
Если нужны максимально контролируемые и «богатые» логи, Nginx остаётся сильным выбором, но требует дисциплины в форматах и ротации. Caddy даёт быстрый путь к structured logging и удобен там, где важны скорость внедрения и читаемость конфигурации. Traefik особенно хорош в контейнерных сценариях: JSON access-логи, управление полями и естественная интеграция с централизованным сбором.
Вне зависимости от выбора, наибольший эффект обычно дают два шага: стандартизировать JSON access-лог и внедрить корреляцию по request_id по всей цепочке.


