1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Что пишется без тебя

Коротко

nginx ведёт две ленты сразу: access_log - что случилось, error_log - почему. Обе пишутся до всякой настройки, и обе врут по-своему, если не знать их правил.

  • access_log пишется после ответа, одной строкой, и время в ней - момент окончания.
  • error_log пишется по ходу дела и имеет восемь уровней. Заданный уровень означает «этот и всё, что серьёзнее», а половина объяснений живёт ниже привычного warn.
  • combined даёт адрес, запрос, код и размер тела. Ни домена, ни времени обработки, ни ответа бэкенда в нём нет.
  • В строке ошибки читай номер в скобках, а не фразу: фраза зависит от сборки.
  • Где лежат файлы, отвечает nginx -T, а не догадки о путях.

Если восемь уровней error_log и поля combined знакомы - листай до «Теперь сам».

Сначала ответь сам

Пользователь жалуется: сайт отдаёт 400 Bad Request. Ты открываешь логи. В access_log строка есть: код 400, запрос виден. В error_log за эту секунду пусто. В конфиге стоит обычное error_log /var/log/nginx/error.log warn;.

Куда делось объяснение?

Оно записано не было. nginx считает эту ошибку разговором с одним кривым клиентом и пишет её на уровне info - на два деления ниже warn. Причина существует, строка существует, но фильтр её отсёк.

Вот она, если понизить уровень:

↳  выводgrep "too long" /var/log/nginx/error.log
2026/09/03 15:45:48 [info] 363#363: *133 client sent too long header line:
"X-Big: aaaaaaaaaaaa..." while reading client request headers

На что это похоже

Представь приёмную поликлиники. Есть журнал посещений: кто пришёл, во сколько ушёл, к какому врачу. Строка появляется, когда человек уже вышел, и в ней записан момент выхода, а не входа. По журналу видно поток и видно, что вот этот посетитель ушёл ни с чем, но почему - там не написано.

И есть записи самого врача: что болело, что не сошлось, чего не хватило. Их ведут по ходу приёма. Только вот у записей есть градация важности, и заведующий велел подшивать в папку лишь то, что серьёзнее определённой отметки. Мелочь вроде «пациент принёс не тот полис» до папки не доезжает.

Ты пришёл разбираться с ушедшим ни с чем и смотришь в папку. Её содержимое зависит от того, где заведующий провёл черту.

Механизм: две ленты и одна черта

access_log: одна строка, после ответа запрос пришёл ответ ушёл здесь строка error_log: сколько угодно строк, по ходу дела уровень отсекает всё, что левее debug info notice warn error crit alert emerg 400, 499, «клиент отвалился» отказ по лимиту, нет прав, 502, 504 не попадут при warn попадут
Черта на шкале - это и есть решение, какую половину поломок ты сможешь объяснить.

Уровень лога - метка серьёзности, которую nginx ставит каждой строке error_log. Уровней восемь, от самого мелкого к самому тяжёлому: debug, info, notice, warn, error, crit, alert, emerg. Третьим словом в error_log /путь warn; ты называешь черту, и в файл попадает названный уровень и всё, что серьёзнее его; остальное nginx сочиняет и выбрасывает. Если уровень не назван, действует error: так стоит, например, в пакете Debian, где в nginx.conf написано просто error_log /var/log/nginx/error.log;.

Правило

access_log отвечает на вопрос «что», error_log - на вопрос «почему», и второй виден ровно настолько, насколько низко опущен уровень.

Разбор: строка combined по костям

↳  выводtail -n 1 /var/log/nginx/access.log
127.0.0.1 - - [03/Sep/2026:15:45:16 +0000] "GET /deadapi/x HTTP/1.1" 502 157 "-" "Mozilla/5.0 (X11; Linux) Firefox/1.0"
Кусок Переменная Что означает
127.0.0.1 $remote_addr кто подключился к nginx
первый - - просто символ, вписанный в формат
второй - $remote_user имя из HTTP-авторизации, обычно пусто
[03/Sep/2026:15:45:16 +0000] $time_local момент окончания обработки; +0000 - часовой пояс сервера
"GET /deadapi/x HTTP/1.1" $request метод, URI и версия
502 $status код, который ушёл клиенту
157 $body_bytes_sent байты тела, без заголовков
"-" "Mozilla..." $http_referer, $http_user_agent откуда и чем

Два поля стоит проверить на себе, потому что оба обманывают.

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

$body_bytes_sent - не весь трафик. Для файла в 300 байт замер даёт $body_bytes_sent = 300 и $bytes_sent = 539: заголовки ответа весят ещё 239 байт. На картинках разница теряется, на мелких ответах API - нет.

Разбор: где наивное правило подводит

Три вещи, из-за которых лог показывает не то, что произошло.

Уровень скрывает половину. Мы уже видели 400. Так же прячется 499 - клиент ушёл, не дождавшись: в access_log код есть, а объяснение epoll_wait() reported that client prematurely closed connection идёт на info. А вот worker_connections are not enough пишется на alert (когда слота не нашлось) или на warn с хвостом reusing connections (когда nginx закрыл простаивающие keepalive-соединения - те, что клиент держал открытыми на будущее, - и справился). alert видно даже при фильтре error, потому что это беда всего сервера, а не одного запроса; warn при уровне по умолчанию не пишется.

Номер надёжнее фразы. Одна и та же ошибка на разных сборках выглядит по-разному:

↳  вывододин и тот же таймаут на двух сборках nginx 1.31.5
alpine:  upstream timed out (110: Operation timed out) while connecting to upstream
debian:  upstream timed out (110: Connection timed out) while connecting to upstream

Текст берётся из библиотеки C, номер - из ядра. Ищи по (110:, (2:, (13:, и твой поиск переживёт переезд на другой образ.

Файлы могут быть не файлами. В официальных образах /var/log/nginx/access.log - это ссылка на /dev/stdout. Логи забирает движок контейнеров, tail -f по файлу висит и ничего не показывает (-f - «следить за новыми строками», выход - Ctrl+C), а попытка очистить его пишет в вывод. Если в каталоге тишина, а сайт работает, проверяй это раньше всего:

$  команда
ls -l /var/log/nginx
↳  выводls -l /var/log/nginx
total 0
lrwxrwxrwx    1 root     root            11 Sep  2 21:04 access.log -> /dev/stdout
lrwxrwxrwx    1 root     root            11 Sep  2 21:04 error.log -> /dev/stderr

Стрелка и буква l в начале строки - это ссылки. Такие логи читают командой docker logs имя-контейнера. А куда nginx пишет вообще, покажет nginx -T | grep -E "access_log|error_log": собранный конфиг со всеми include, из которого grep оставит нужные строки (-E разрешает | - «или», а кавычки не дают оболочке принять | за свою трубу).

Что ломается без этого

Разбор идёт по кругу. Человек видит в access_log код и не находит причины; делает вывод «в логах ничего нет» и начинает менять конфиг наугад. Через час выясняется, что причина писалась всё это время, просто на уровень ниже черты.

Второй способ потерять время - искать по фразе из чужой статьи. Статью писали на Debian, у тебя Alpine, поиск grep "Connection timed out" не находит ничего, и рождается вывод «у нас другая проблема».

Зачем это в работе

Первое, что делают на новом сервере, - опускают error_log до info и смотрят сутки. Шума будет много: закрытые keepalive-соединения, ушедшие клиенты. Но именно там лежат объяснения кодов 400 и 499, по которым иначе нечего сказать, кроме «у кого-то что-то не так».

Дальше уровень поднимают обратно до warn или error - и понижают точечно, когда снова понадобится.

Быстрый счёт по кодам за последние десять тысяч запросов:

$  команда
tail -n 10000 /var/log/nginx/access.log | awk '{print $9}' | sort | uniq -c | sort -rn

awk '{print $9}' печатает девятое поле каждой строки (поля разделены пробелами, это не переменные nginx), sort сортирует, uniq -c схлопывает одинаковые строки и считает их, второй sort -rn ставит самые частые наверх. В выводе слева количество, справа код:

↳  выводawk '{print $9}' | sort | uniq -c | sort -rn
   9436 200
    381 304
    118 404
     47 502
     18 301

Позиции полей ($1 - адрес, $7 - URI, $9 - код) верны для combined; поменяешь формат - поменяются и номера. Это одна из причин, по которой в следующем уроке появится структурированный лог.

Уровень debug требует особой сборки

error_log ... debug; работает, только если nginx собран с --with-debug; проверяется nginx -V 2>&1 | grep -o with-debug (nginx -V печатает в поток ошибок, 2>&1 сливает его с обычным выводом, -o оставляет только найденное слово: есть with-debug - сборка отладочная, пусто - нет). Без него nginx -t скажет test is successful, конфиг применится, а отладочных строк не появится ни одной - молчаливый отказ. И даже там, где debug доступен, включать его на весь сервер не стоит: debug_connection 192.168.1.44; в блоке events пишет отладку для одного адреса.

Теперь сам

В логе строка с кодом 499 и $body_bytes_sent = 0. В error_log за эту секунду ничего. Уровень - warn. Сколько ты можешь сказать о причине и что сделать, чтобы в следующий раз сказать больше?

О причине - почти ничего: 499 означает, что клиент закрыл соединение, не дождавшись ответа, а кто он и сколько ждал, в этой строке не написано. Чтобы в следующий раз было что сказать, нужны две вещи: понизить error_log до info (тогда появится строка про преждевременно закрытое соединение) и добавить в формат время обработки - без него не отличить «клиент нетерпелив» от «мы слишком медленные». Первая правка описана выше в этом уроке, вторая - тема следующего.

Главное

access_log пишется одной строкой после ответа, и время в ней - момент окончания. error_log пишется по ходу дела и фильтруется уровнем: info держит объяснения 400 и 499, alert - беды всего сервера. В строке ошибки читай номер в скобках, а не фразу: фраза зависит от сборки. $body_bytes_sent считает только тело, весь ответ считает $bytes_sent. Где лежат файлы и что вообще включено, отвечает nginx -T; в контейнерах логи обычно уходят в stdout, а не в файл.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.