Логирование: access_log, форматы и структурированный JSON

Рейтинг: 64.6% · 12 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

Логирование: access_log, форматы и структурированный JSON

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON (вы здесь)
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Видимость или слепота: зачем вообще трогать nginx логи

Падает прод. 502 сыпятся, пользователи орут, дашборд красный. Ты заходишь на шлюз и смотришь access.log. И видишь combined-формат образца 2004 года:

Код: Выделить всё

10.0.0.7 - - [17/Jun/2026:11:42:03 +0300] "POST /api/checkout HTTP/1.1" 502 167 "-" "okhttp/4.12"
И что? Кто из трех бэкендов отвалился - не видно. Сколько nginx ждал ответа - не видно. Был это таймаут или бэкенд закрыл коннект - не видно. По какому request_id искать этот же запрос в логах приложения - нечем. Ты слепой. А инцидент идет.

Логирование - это не "включил и забыл", это твои глаза в проде. Хороший access_log отвечает на вопрос "что именно сейчас сломалось" за секунды, плохой - заставляет гадать. В этом уроке разберем nginx логирование по-взрослому: как устроены log_format и переменные, почему json логи победили текст, что обязательно класть в каждую строку, как не логировать мусор, как ротировать без потери данных и где спрятана грабля производительности, которая роняет latency под нагрузкой.

Изображение

Как работает access_log и log_format: механика

Две директивы - фундамент. log_format описывает шаблон строки (что и в каком порядке писать), access_log говорит "пиши вот этим форматом вот сюда". Формат именованный, форматов может быть несколько, access_log на них ссылается по имени.

Полный синтаксис access_log важно знать целиком, потому что половина магии в опциональных параметрах:

Код: Выделить всё

access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
Запись лога происходит в самой последней фазе обработки запроса - log phase, когда ответ уже ушел клиенту. Поэтому в строке доступны переменные, которых в начале обработки еще не было: $status (финальный статус), $request_time (полное время), $upstream_response_time (сколько ждали бэкенд), $body_bytes_sent. Это ключевой момент: лог пишется ПОСЛЕ ответа, он подводит итог.

Переменные - сердце формата. Их сотни, но рабочий минимум такой:
  • $remote_addr - IP клиента (внимание: без модуля realip это IP ближайшего прокси, об этом ниже)
  • $request - метод, URI и протокол одной строкой; чаще лучше разбить на $request_method, $uri, $server_protocol
  • $status - HTTP-статус, который nginx отдал клиенту
  • $body_bytes_sent - байты тела ответа
  • $request_time - полное время обработки запроса nginx в секундах с миллисекундами
  • $http_user_agent, $http_referer - заголовки клиента (любой заголовок доступен как $http_имя_заголовка с нижним подчеркиванием)
И отдельно - переменные апстрима, без которых диагностика проксирования невозможна:
  • $upstream_addr - адрес бэкенда, к которому реально ходили (при failover тут будет список через запятую)
  • $upstream_status - статус ОТ бэкенда (может отличаться от $status, если nginx переписал, например отдал свою страницу ошибки)
  • $upstream_response_time - сколько секунд бэкенд отвечал
  • $upstream_connect_time, $upstream_header_time - время TCP-коннекта и время до первых заголовков (TTFB бэкенда)
  • $upstream_cache_status - HIT/MISS/BYPASS/EXPIRED/STALE, статус кэша
Простая, но уже полезная текстовая форма:

Код: Выделить всё

log_format upstream_detail
    '$remote_addr [$time_iso8601] "$request_method $uri $server_protocol" '
    '$status req=$request_time up=$upstream_response_time '
    'addr=$upstream_addr ustatus=$upstream_status cache=$upstream_cache_status '
    'rid=$request_id';

access_log /var/log/nginx/access.log upstream_detail;
Сравни с combined из начала урока. Теперь по строке видно: какой бэкенд, сколько он думал, был ли это кэш, и по какому $request_id искать сквозную трассу. $request_id - встроенная переменная nginx, уникальный 32-символьный hex на каждый запрос. Пробрасываешь его в бэкенд заголовком - и связываешь логи nginx с логами приложения. Это бесплатная трассировка из коробки.

nginx json логи: почему escape=json меняет всё

Текстовый лог отлично читается глазами и отвратительно парсится машиной. Как только в User-Agent попадет кавычка или в URI пробел, твой grep-regexp в Loki разъедет, и поле уползет. А когда логи едут в ELK, Loki или ClickHouse, парсинг - это деньги и нервы. Решение появилось еще в nginx 1.11.8 и стало индустриальным стандартом: параметр escape=json у log_format.

Что он делает технически: nginx сам экранирует значения всех переменных по правилам JSON. Кавычка становится \", обратный слеш \\, перевод строки \n, управляющие символы с кодом меньше 32 уходят в \u00XX. То есть какой бы мусор клиент ни прислал в заголовке, итоговая строка останется валидным JSON. Без escape=json одна злая кавычка в Referer ломает всю строку, и парсер в Loki давится.

Сам формат ты пишешь руками как JSON-объект, склеивая строки. Ловушка номер один: значение строковых полей берем в кавычки ("$var"), а числовые поля ($status, $bytes_sent, $body_bytes_sent) пишем БЕЗ кавычек - тогда в Elasticsearch/ClickHouse они приедут числами и по ним можно считать агрегаты и фильтровать по диапазону.

В эталонном шлюзе ngx-trace-gateway этим занимается global_logging.conf, и формат там не игрушечный - это structured_log на 50+ полей. Не пугайся объема, логика простая - поля сгруппированы во вложенные объекты по смыслу: timestamp, severity, service, trace, client, network, tls, http (с подобъектами request/response/server/upstream). Вот компактная, но боевая выжимка того же подхода:

Код: Выделить всё

log_format structured_log escape=json
    '{'
        '"timestamp":"$time_iso8601",'
        '"timestamp_msec":"$msec",'
        '"severity":"$log_level",'
        '"service":{"name":"api-gateway","instance":"$hostname"},'
        '"trace":{'
            '"request_id":"$request_id",'
            '"trace_id":"$trace_request_id"'
        '},'
        '"client":{'
            '"ip":"$remote_addr",'
            '"real_ip":"$realip_remote_addr",'
            '"forwarded_for":"$http_x_forwarded_for",'
            '"user_agent":"$http_user_agent"'
        '},'
        '"tls":{'
            '"protocol":"$ssl_protocol",'
            '"cipher":"$ssl_cipher",'
            '"session_reused":"$ssl_session_reused",'
            '"sni":"$ssl_server_name"'
        '},'
        '"http":{'
            '"method":"$request_method",'
            '"uri":"$uri",'
            '"args":"$args",'
            '"host":"$host",'
            '"protocol":"$server_protocol",'
            '"status":$status,'
            '"request_time":$request_time,'
            '"body_bytes_sent":$body_bytes_sent,'
            '"upstream":{'
                '"addr":"$upstream_addr",'
                '"status":"$upstream_status",'
                '"cache_status":"$upstream_cache_status",'
                '"connect_time":"$upstream_connect_time",'
                '"header_time":"$upstream_header_time",'
                '"response_time":"$upstream_response_time"'
            '}'
        '}'
    '}';
Одна такая строка в Loki/Kibana разбирается на структурированные поля автоматически, без regexp. Хочешь все запросы, где upstream.response_time больше 1 секунды и cache_status=MISS - это один фильтр по полю, а не пляска с парсингом. $time_iso8601 даешь как timestamp, а $msec (секунды эпохи с долями) - для точной корреляции между системами без парсинга строки.

Заметь $upstream_response_time отдельно от $request_time. Разница между ними - это время, которое съел сам nginx: чтение тела от медленного клиента, буферизация, ожидание в очереди. Если request_time большой, а upstream_response_time маленький - тормозит не бэкенд, а клиент или сеть. Эти два поля рядом в каждой строке экономят часы разбора инцидентов.

severity через map, реальный IP и условное логирование

Теперь три приема, которые отличают зрелый конфиг от учебного.

severity из статуса. Привычные лог-фреймворки пишут уровень (info/warn/error). nginx из коробки так не умеет, но severity выводится из $status через map. Это поле $log_level из формата выше:

Код: Выделить всё

map $status $log_level {
    ~^2  "info";
    ~^3  "info";
    ~^4  "warn";
    ~^5  "error";
    default "info";
}
map срабатывает лениво - вычисляется только когда переменная реально используется (в строке лога), накладных нет. Теперь в Kibana ставишь фильтр severity=error и видишь только пятисотки. Алертинг по уровню, а не по магическим числам кодов.

Реальный IP. Если nginx стоит за балансировщиком, CDN или другим nginx, то $remote_addr - это адрес соседнего прокси, а не клиента. Логи врут, rate-limit и geo считают не того. Лечится модулем realip: ему говоришь, каким сетям доверять, и из какого заголовка брать настоящий IP.

Код: Выделить всё

set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Критично: set_real_ip_from должен перечислять ТОЛЬКО доверенные сети твоих прокси/CDN. Если доверишь всему интернету (0.0.0.0/0), любой клиент подделает X-Forwarded-For и подставит чужой IP - сломает тебе и логи, и auth-по-IP. В JSON-формате я держу оба поля: $remote_addr (кто реально пришел по TCP) и $realip_remote_addr (исходный адрес коннекта до подмены) плюс сырой $http_x_forwarded_for - чтобы при разборе атаки видеть полную картину.

Условное логирование. Не всё стоит писать. Healthcheck от Kubernetes молотит /healthz каждые 2 секунды, статика - тысячи строк ни о чем. Раздуешь диск и зашумишь поиск. Параметр if= у access_log + map решают это:

Код: Выделить всё

map $request_uri $loggable {
    ~^/healthz   0;
    ~^/nginx_status  0;
    default      1;
}

map $status $log_sampling {
    ~^[23]  0;   # успешную статику можно не логировать поштучно
    default 1;   # а вот ошибки - всегда
}

server {
    access_log /var/log/nginx/access.log structured_log if=$loggable;
    location ~* \.(css|js|png|jpg|woff2)$ {
        access_log off;          # полностью выключить для статики
        log_not_found off;       # не плодить error_log по favicon.ico
        expires 30d;
    }
}
if=$loggable: если переменная равна "0" или пустой строке - запрос не логируется. log_not_found off гасит спам в error_log про отсутствующие файлы (вечный favicon.ico, apple-touch-icon). access_log off в location-е статики полностью отключает запись.

Буферизация, сжатие, syslog и error_log: эксплуатация

Грабля производительности. По умолчанию каждая строка access_log - это отдельный синхронный write() на диск. На шлюзе с десятками тысяч rps это превращается в шторм мелких операций ввода-вывода, и worker блокируется на диске вместо обработки запросов. Latency растет на ровном месте. Лечение - буферизация:

Код: Выделить всё

access_log /var/log/nginx/access.log structured_log buffer=64k flush=5s;
buffer=64k копит строки в памяти и сбрасывает пачкой, когда буфер полон. flush=5s гарантирует, что даже на тихом сайте лог уйдет на диск максимум через 5 секунд - иначе на малотрафичном сервере записи висели бы в памяти до заполнения буфера. Можно добавить gzip - тогда nginx жмет лог на лету:

Код: Выделить всё

access_log /var/log/nginx/access.log structured_log gzip=5 buffer=128k flush=5s;
Важная деталь: gzip требует буферизации (стримить сжатый поток построчно нельзя), поэтому при gzip буфер обязателен и неявно включается. Экономит диск в разы, но логи теперь читаются через zcat.

syslog. Вместо файла лог можно слать прямо в syslog по сети - удобно, когда сбор централизованный и не хочется ставить агент-сборщик на каждый хост:

Код: Выделить всё

access_log syslog:server=10.0.0.50:514,facility=local7,tag=nginx,severity=info structured_log;
error_log - отдельная история. access_log про каждый запрос, error_log про проблемы самого nginx: ошибки конфигурации, обрывы апстрима, проблемы TLS. У него есть уровни (от низкого к высокому): debug, info, notice, warn, error, crit, alert, emerg. Указанный уровень и всё, что выше, пишется в файл.

Код: Выделить всё

error_log /var/log/nginx/error.log warn;
warn - разумный прод-дефолт: видно реальные проблемы, но не тонешь в шуме. info и тем более debug включай точечно для отладки конкретной проблемы и сразу выключай - debug пишет гигабайты и сам по себе тормозит (плюс debug требует сборки nginx с --with-debug). Учти: error_log должен быть объявлен на самом верхнем уровне (main), иначе при ошибке старта nginx некуда писать. И в отличие от access_log, error_log нельзя выключить полностью директивой off - это не значение уровня, а буквально путь к файлу с таким именем (исторический сюрприз).

Практика: ротация без потери логов через USR1

Логи растут, диск кончается. Ротация - это переименовать старый файл и начать писать в новый. Но есть тонкость: nginx держит лог-файл открытым по файловому дескриптору. Если просто переименовать access.log в access.log.1, nginx продолжит писать в ТОТ ЖЕ дескриптор - то есть в переименованный файл. Новый access.log не появится, пока nginx не переоткроет файлы.

Механизм переоткрытия - сигнал USR1. По нему nginx закрывает текущие лог-файлы и открывает их заново по тем же путям (master передает сигнал воркерам). Поэтому правильная ротация всегда: сначала переименовать файлы, потом послать USR1.

Вручную:

Код: Выделить всё

mv /var/log/nginx/access.log /var/log/nginx/access.log.1
nginx -s reopen
# nginx -s reopen эквивалентен kill -USR1 $(cat /run/nginx.pid)
В реальном проде это делает logrotate. Типичный конфиг /etc/logrotate.d/nginx:

Код: Выделить всё

/var/log/nginx/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}
Разбор: daily + rotate 14 - храним 14 дней. compress + delaycompress - жмем, но не самый свежий файл (в него еще могли долетать строки). sharedscripts - postrotate выполнится один раз после ВСЕХ файлов, а не на каждый. И главное - postrotate шлет USR1, чтобы nginx начал писать в свежий файл. Без этой строки logrotate переименует файл, а nginx будет молча писать в никуда (в переименованный inode), и новые логи пропадут до следующего reload. Эту ошибку допускают постоянно.

Замечание про gzip-буферизацию и ротацию: если включил gzip у access_log, дай logrotate флаг nocompress или copytruncate-альтернативу осторожно - двойное сжатие бессмысленно. Проще не жать в nginx, а отдать сжатие logrotate.

Мини-лаба: собери боевой JSON-лог руками

Повтори по шагам на тестовом nginx (можно в Docker openresty/openresty или nginx:latest).
  • Шаг 1. В http-блок добавь map $status $log_level из урока и log_format structured_log escape=json (компактная версия выше). Проверь nginx -t - частая ошибка тут несбалансированные кавычки в склейке строк.
  • Шаг 2. В server подключи access_log с этим форматом и buffer=32k flush=3s. Сделай пару запросов curl, среди них один на несуществующий URL (получишь 404).
  • Шаг 3. Открой лог. Прогони строку через jq: tail -1 access.log | jq . - убедись, что это валидный JSON и severity у 404 равен "warn".
  • Шаг 4. Добавь map $request_uri $loggable с исключением /healthz и параметр if=$loggable. Сделай запрос на /healthz и проверь, что строка НЕ появилась в логе.
  • Шаг 5. Ротация: mv access.log access.log.1, затем nginx -s reopen. Сделай новый curl и убедись, что строка ушла в новый access.log, а не в .1.
После лабы у тебя на руках формат, который можно лить в Loki или ClickHouse без единого regexp.

Контрольные вопросы
  • 1. Зачем в log_format ставить параметр escape=json и что сломается без него, если клиент пришлет кавычку в User-Agent?
  • 2. В чем разница между $request_time и $upstream_response_time, и что значит ситуация, когда первое большое, а второе маленькое?
  • 3. Почему при ротации логов недостаточно переименовать файл, и какую роль играет сигнал USR1 (nginx -s reopen)?
  • 4. Чем $remote_addr отличается от реального IP клиента за CDN, и почему set_real_ip_from нельзя задавать как 0.0.0.0/0?
Итог

Логи - это видимость, без которой ты слеп в инциденте. Уходи от combined к структурированному JSON через escape=json: числовые поля без кавычек, severity через map $status, реальный IP через realip, обязательно $upstream_response_time/$upstream_status/$upstream_cache_status/$request_id в каждой строке. Не логируй мусор (if= + access_log off + log_not_found off), буферизуй (buffer/flush/gzip), чтобы синхронный ввод-вывод не убивал latency, и ротируй честно - через USR1, иначе потеряешь данные. error_log держи на warn и не забывай про него отдельно от access_log. Такой лог отвечает на вопрос "что сломалось" за секунды, а не за час.
👍2 ❤️2 🔥1 😄 🤔
Аватара пользователя
terraformguru
Сообщения: 1
Зарегистрирован: 16 май 2026, 10:37

Re: Логирование: access_log, форматы и структурированный JSON

Сообщение terraformguru »

Наконец дошло зачем числовые поля типа status писать без кавычек - до этого лил все в кавычках и в кибане не мог по диапазону status отфильтровать, теперь переделал формат, спасибо
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
solidity2
Сообщения: 1
Зарегистрирован: 01 июн 2026, 12:16

Re: Логирование: access_log, форматы и структурированный JSON

Сообщение solidity2 »

Вопрос про logrotate: у нас как раз пропадали логи после ротации пока не добавили kill -USR1 в postrotate, неделю искали почему access.log пустой. Подтверждаю граблю, ловите все
👍2 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
Следующая глава →
Сквозная трассировка запросов и OpenTelemetry

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
  • Похожие темы
Похожие запросы: Мониторинг и метрики nginxЛоги nginx: access_log и JSON

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость