65 лет полету человека в космос! Хостинг и домены со скидкой
до 22.04.2026 Подробнее
Выберите продукт

Nginx JSON-логи: request_id и upstream_time без боли

Покажу, как настроить в Nginx структурированные JSON access-логи: безопасный log_format с escape=json, единый request_id (X-Request-ID), upstream_*_time и разбор 5xx. Дам готовые конфиги для proxy и FastCGI и команды проверки.
Nginx JSON-логи: request_id и upstream_time без боли

JSON-логи в Nginx — это не «модно», а удобно: их легко парсить, индексировать, склеивать с логами приложения и быстро отвечать на вопросы вроде «почему выросли 5xx?» или «где теряем время: на Nginx или на апстриме?». Главная ошибка — пытаться «склеить JSON руками» без корректного экранирования: рано или поздно кавычка в User-Agent или управляющий символ в заголовке превратят строку в невалидный JSON.

Ниже — практичная схема: корректный log_format escape=json, единый request_id (в том числе через X-Request-ID), а также метрики апстрима (upstream_response_time, upstream_connect_time, upstream_header_time) для диагностики задержек и 5xx.

Зачем JSON-логи и почему combined уже не хватает

Классический combined-лог хорош глазами, но неудобен для машин: регулярки, «плавающие» поля, спецсимволы. Как только вы собираете логи централизованно или расследуете инциденты по нескольким сервисам, обычно хочется:

  • фиксированный набор ключей, который не меняется от строки к строке;
  • надёжное экранирование кавычек и управляющих символов;
  • корреляцию запроса через request_id между Nginx, приложением и апстримами;
  • явные числовые поля времени, чтобы быстро строить разрезы по latency;
  • понимание природы 5xx: Nginx не смог сходить к апстриму или апстрим вернул 500.

Структурированный формат решает это «в лоб»: одна строка — один JSON-объект. Но только если вы используете escape=json и не делаете ручное экранирование в строках формата.

База: безопасный JSON через log_format escape=json

Ключевой момент: без log_format ... escape=json вы рано или поздно получите невалидный JSON. Достаточно одного заголовка с кавычкой или непечатным символом, чтобы парсер на стороне сбора логов начал «сыпаться».

Пример базового формата (обычно размещают в контексте http):

log_format json_main escape=json
'{'
  '"ts":"$time_iso8601",'
  '"remote_addr":"$remote_addr",'
  '"xff":"$http_x_forwarded_for",'
  '"request_id":"$request_id",'
  '"request_method":"$request_method",'
  '"scheme":"$scheme",'
  '"host":"$host",'
  '"uri":"$uri",'
  '"args":"$args",'
  '"status":$status,'
  '"body_bytes_sent":$body_bytes_sent,'
  '"request_length":$request_length,'
  '"request_time":$request_time,'
  '"upstream_addr":"$upstream_addr",'
  '"upstream_status":"$upstream_status",'
  '"upstream_response_time":"$upstream_response_time",'
  '"upstream_connect_time":"$upstream_connect_time",'
  '"upstream_header_time":"$upstream_header_time",'
  '"http_referer":"$http_referer",'
  '"http_user_agent":"$http_user_agent"'
'}';

И подключение формата к файлу:

access_log /var/log/nginx/access.json json_main;

Почему часть полей строковые, а часть числовые

$status, $body_bytes_sent, $request_time и похожие поля удобнее хранить числами: фильтры и агрегации проще и быстрее. Но есть нюанс с upstream-полями: они могут быть пустыми или содержать несколько значений через запятую (ретраи, failover). Поэтому $upstream_response_time и родственников часто разумнее оставлять строками — так вы не потеряете исходную картину, а нормализацию сделаете уже на стороне системы логирования.

Если вы храните проекты на виртуальном хостинге или на VDS, JSON-логи особенно полезны при миграциях/масштабировании: вы быстрее увидите, что именно изменилось — сетевые задержки до апстрима, рост очередей в приложении или проблемы на уровне фронта.

Фрагмент JSON access-лога Nginx с request_id и upstream_*_time

request_id и X-Request-ID: единая корреляция по всей цепочке

Цель простая: один и тот же идентификатор запроса должен проходить через Nginx, приложение и любые промежуточные сервисы. Иначе вы увидите в логах приложения одно, в Nginx другое, а в логах апстрима третье — и расследование превращается в ручной «пазл».

Часто клиент, CDN или балансировщик уже присылает X-Request-ID. Рабочая практика:

  • если заголовок X-Request-ID пришёл — использовать его;
  • если не пришёл — сгенерировать ID на Nginx (через $request_id) и проставить дальше;
  • всегда возвращать X-Request-ID клиенту в ответе, чтобы саппорт мог попросить «ID запроса».

Как взять входящий X-Request-ID, иначе использовать $request_id

Удобнее всего сделать «нормализованную» переменную через map:

map $http_x_request_id $req_id {
  default $http_x_request_id;
  ""      $request_id;
}

Теперь в JSON-логе пишем не $request_id, а $req_id:

log_format json_main escape=json
'{'
  '"ts":"$time_iso8601",'
  '"request_id":"$req_id",'
  '"status":$status,'
  '"request_time":$request_time,'
  '"upstream_response_time":"$upstream_response_time"'
'}';

Проброс request id в апстримы и возврат клиенту

Для reverse proxy:

proxy_set_header X-Request-ID $req_id;
add_header X-Request-ID $req_id always;

Для FastCGI (PHP-FPM):

fastcgi_param HTTP_X_REQUEST_ID $req_id;
add_header X-Request-ID $req_id always;

Если Nginx стоит за CDN/балансировщиком, часто именно edge генерирует X-Request-ID. Задача Nginx — не «перепридумать» ID, а аккуратно принять и прокинуть его дальше.

В теме миграций и «разрезания» монолита на сервисы сильно помогает дисциплина с request-id. Если планируете переезды без простоев, пригодится чеклист из статьи про миграцию сайта с минимальным downtime.

upstream_time: какие поля реально помогают и как их читать

Когда говорят «upstream_time», обычно имеют в виду $upstream_response_time. Но для диагностики полезно держать три поля:

  • $upstream_connect_time — время установления соединения до апстрима;
  • $upstream_header_time — время до получения заголовков ответа от апстрима;
  • $upstream_response_time — полное время получения ответа от апстрима (включая тело).

Вместе с $request_time (общее время обработки на стороне Nginx) вы получаете понятную картину:

  • если request_time заметно больше upstream_response_time, ищите задержки на стороне Nginx: медленный клиент, буферизация, запись на диск, лимиты, TLS и т.д.;
  • если растёт upstream_connect_time, часто это сеть, исчерпание соединений, перегруз апстрима или проблемы маршрутизации;
  • если большой upstream_header_time, апстрим долго «думает» перед началом ответа (очереди, блокировки, медленная БД);
  • если большой upstream_response_time, апстрим долго отдаёт тело (генерация, стриминг, большие ответы, медленный диск).

Почему upstream_* может быть списком значений

Nginx может сходить к нескольким апстримам в рамках одного запроса: ретраи при ошибках, несколько upstream-серверов, failover. Тогда переменные вроде $upstream_response_time содержат значения через запятую (например, 0.120, 0.004). Это не помеха, а полезная диагностика: видно, что первый апстрим «подвис», а второй ответил быстро.

Как ловить и объяснять 5xx по JSON-логам

5xx — это не всегда «приложение упало». В связке Nginx + апстрим есть минимум два класса ситуаций:

  • апстрим вернул 5xx, и Nginx это проксировал (смотрите upstream_status);
  • Nginx сам сгенерировал ошибку (таймауты, connect error, некорректный ответ апстрима) — при этом upstream-поля могут быть пустыми.

Чтобы быстро отличать одно от другого, логируйте одновременно status (клиентский) и upstream_status (ответ апстрима). Тогда типовые сценарии читаются проще:

  • status=502 и upstream_status пустой — Nginx не получил ответа от апстрима (часто connect error/timeout);
  • status=504 и upstream_response_time близко к таймауту — типичный upstream timeout;
  • status=499 — клиент закрыл соединение; это не «ошибка сервера», но полезный сигнал для UX и нагрузки.

Минимальный набор полей для расследования инцидентов

Если не хотите логировать «всё подряд», но хотите разбирать 5xx быстро, оставьте хотя бы:

  • ts, host, uri, args (или request_uri), request_method;
  • status, request_time;
  • upstream_addr, upstream_status, upstream_response_time, upstream_connect_time, upstream_header_time;
  • request_id (через $req_id);
  • remote_addr и/или xff.

Готовый пример конфига: JSON + request_id + upstream timings

Ниже «скелет», который обычно достаточно положить в http, а дальше подключать в нужные server. По желанию разнесите map и log_format в отдельный include-файл, чтобы переиспользовать между проектами.

http {
  map $http_x_request_id $req_id {
    default $http_x_request_id;
    ""      $request_id;
  }

  log_format json_main escape=json
  '{'
    '"ts":"$time_iso8601",'
    '"request_id":"$req_id",'
    '"remote_addr":"$remote_addr",'
    '"xff":"$http_x_forwarded_for",'
    '"host":"$host",'
    '"request":"$request",'
    '"uri":"$uri",'
    '"args":"$args",'
    '"status":$status,'
    '"request_time":$request_time,'
    '"bytes_sent":$bytes_sent,'
    '"upstream_addr":"$upstream_addr",'
    '"upstream_status":"$upstream_status",'
    '"upstream_response_time":"$upstream_response_time",'
    '"upstream_connect_time":"$upstream_connect_time",'
    '"upstream_header_time":"$upstream_header_time",'
    '"http_user_agent":"$http_user_agent"'
  '}';

  access_log /var/log/nginx/access.json json_main;

  server {
    listen 80;

    add_header X-Request-ID $req_id always;

    location / {
      proxy_set_header X-Request-ID $req_id;
      proxy_set_header Host $host;
      proxy_pass http://app_upstream;
    }
  }
}
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Проверка: валидность JSON и быстрый поиск проблемных запросов

После изменений сделайте две проверки: синтаксис Nginx и «что реально пишется в лог». Начинаем с конфигурации:

nginx -t

Если на сервере есть jq, он моментально покажет, валидный ли JSON (и где именно ломается):

tail -n 50 /var/log/nginx/access.json | jq -c . > /dev/null

Быстрый поиск 5xx (без внешней системы логирования):

grep '"status":5' /var/log/nginx/access.json | tail -n 50

Найти конкретный запрос по ID (особенно полезно, когда ID прислал пользователь или саппорт):

grep '"request_id":"YOUR_ID"' /var/log/nginx/access.json

Если хотите быстро отобрать именно «пограничные» случаи проксирования, смотрите связку status и upstream_status в найденных строках.

Проверка JSON-логов Nginx через jq и поиск 5xx в access.json

Типовые грабли и как их обходить

1) Невалидный JSON из-за отсутствия escape=json

Это главный «скрытый» баг. Даже если сейчас всё выглядит нормально, одна кавычка в http_user_agent или неожиданный символ в http_referer сломают формат. Лечится включением escape=json и отказом от ручного экранирования.

2) request_id не совпадает между сервисами

Если приложение само генерирует ID, а Nginx — свой, получится два независимых мира. Выберите «источник истины» (входящий X-Request-ID на edge или генерация на Nginx) и строго прокидывайте дальше. Переменная $req_id через map — самый простой способ.

3) upstream_* пустые, и это нормально

Если запрос обслужен локально (статика) или Nginx отдал ошибку до проксирования, upstream-поля будут пустыми. Не пытайтесь насильно превращать их в числа на стороне Nginx: это только усложняет формат и ухудшает читаемость.

4) Несколько значений upstream_time ломают агрегации

Если вы строите метрики по upstream_response_time как по числу, нужно заранее решить, что считать: первое значение, последнее, максимум или сумму. На уровне Nginx это неудобно. Правильнее разбирать поле на стороне системы логирования (где есть нормальный парсинг и функции агрегации).

Что добавить дальше (когда базовый формат прижился)

Когда схема начнёт работать «на автомате», обычно хочется расширить JSON-лог ещё парой полей:

  • маркер сервиса/vhost (например, отдельный ключ service через map по $host);
  • $limit_req_status и $limit_conn_status, если используете лимиты;
  • $gzip_ratio, если важно понимать компрессию;
  • отдельные поля для $request_uri и $uri, чтобы различать «как пришло» и «как нормализовалось»;
  • маскирование чувствительных параметров в args на этапе логирования или обработки (токены, e-mail, телефоны).

Если параллельно закручиваете безопасность домена (HSTS, редиректы, миграции сертификатов), держите под рукой разбор по теме: редиректы 301, HSTS и SSL при переезде домена.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

База, которая даёт максимум пользы при минимуме сложности: надёжный JSON с escape=json, единый request_id и upstream timings. Дальше всё упирается не в «как логировать», а в то, какие вопросы вы хотите задавать логам и как быстро получать ответы.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: как исправить sshd re-exec requires execution with an absolute path OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить sshd re-exec requires execution with an absolute path

Если в Debian или Ubuntu команда systemctl restart ssh завершается ошибкой sshd re-exec requires execution with an absolute path, ...
Debian/Ubuntu: как исправить tar: Exiting with failure status due to previous errors при backup и deploy OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить tar: Exiting with failure status due to previous errors при backup и deploy

Ошибка tar: Exiting with failure status due to previous errors в Debian и Ubuntu обычно указывает не на поломку tar, а на конкретн ...
Debian/Ubuntu: Failed to connect to bus и System has not been booted with systemd as init system OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: Failed to connect to bus и System has not been booted with systemd as init system

Если в Debian или Ubuntu команда systemctl отвечает Failed to connect to bus или System has not been booted with systemd as init s ...