Когда сайт или API вырастает, access_log превращается в горячую точку: запись каждого запроса — это синхронные системные вызовы, буферы, ротация и последующая доставка в хранилище логов. На пиках это приводит к росту задержек и избыточному I/O. Сэмплирование — стратегия, которая сокращает объём логов без потери диагностической ценности: мы фиксируем всё важное (ошибки, медленные и нестандартные пути) и записываем лишь долю успешных запросов. В Nginx это делается элегантно: комбинацией split_clients, map и параметра if у директивы access_log.
Зачем сэмплировать access‑логи
С точки зрения производительности запись логов конкурирует с приложением за файлы, дисковый кэш и CPU. Чем выше RPS, тем заметнее:
- Снижается нагрузка на файловую подсистему и лог-агенты при уменьшении числа записей.
- Ускоряется разбор логов (парсеры, агрегация, алертинг).
- Уменьшаются расходы на хранение при длительных ретеншен-периодах.
При этом важно не «ослепнуть»: 4xx/5xx, редкие эндпоинты, маршруты аутентификации, вебхуки и события с подозрительными заголовками должны логироваться всегда. Остальное можно записывать частично.
Базовые кирпичики: access_log, if и map
Директива access_log поддерживает параметр if=, который включает запись, если предоставленное выражение-условие возвращает непустое и неравное нулю значение. Это условие — значение любой переменной Nginx. Мы можем вычислять эту переменную в map, разруливая сложные наборы правил (URI, статус, типы запросов, IP).
Важный момент: речь о параметре
if=уaccess_log, а не об использовании директивыifвнутриlocation. Первое — безопасно и предназначено для условного логирования, второе — требует осторожности при изменении запроса.
Минимальный каркас
# Формат лога (пример). Сократите его, если не всё нужно
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$request_time"';
# Базовая переменная-флаг: по умолчанию логируем всё
map $request_method $log_enable {
default 1;
}
server {
listen 80;
server_name example.test;
# Условное логирование: будет писать только если $log_enable не пусто и не "0"
access_log /var/log/nginx/access.log main if=$log_enable;
location / {
proxy_pass http://app;
}
}
Теперь задача — вычислить $log_enable так, чтобы:
- все ответы с 4xx/5xx писались всегда;
- статические файлы логировались редко или вовсе не логировались;
- динамические успешные ответы логировались с заданным процентом;
- опционально — отдельные файлы логов для ошибок и выборки.
Процентная выборка: split_clients
Директива split_clients детерминированно делит поток запросов на доли, используя хэш от переменной. Это идеальный инструмент для сэмплирования: например, логировать 1% всех успешных 2xx.
# Генерируем флаг $sample_2xx: 1% запросов попадает в выборку
# В качестве входа используем устойчивую строку — IP+URI, чтобы одинаковые запросы попадали в одну и ту же долю
split_clients "$remote_addr$request_uri" $sample_2xx {
1% 1;
* 0;
}
Чем лучше «перемешан» вход, тем равномернее распределение. Для API с авторизацией можно подставить идентификатор пользователя или токен (в обезличенном виде из заголовка), для публичных сайтов достаточно $remote_addr$request_uri.
Условия по статусу и URI: map
Статус ответа известен к моменту записи в лог, поэтому удобнее всего решать, писать ли лог, через map $status. Параллельно можно отметить «шумные» пути (статик), а критичные — логировать всегда.
# 1. Всегда логируем ошибки 5xx и 4xx (например, 429/403/404)
map $status $log_always_by_status {
~^5 1; # все 5xx
429 1;
403 1;
404 1; # по желанию — можно выключить на продах с большим объёмом 404
default 0;
}
# 2. Отмечаем статические файлы (расширение или префиксы путей)
map $uri $is_static {
~*\.(?:css|js|gif|jpg|jpeg|png|svg|webp|ico|woff2?|ttf|map)$ 1;
~*^/(?:assets|static|fonts|images|img|favicon)\b 1;
default 0;
}
# 3. Критичные пути, которые пишем всегда (логин, платежи, вебхуки, API-админка)
map $uri $log_always_by_uri {
~^/auth/ 1;
~^/payment/ 1;
~^/webhook/ 1;
~^/api/admin/ 1;
default 0;
}

Собираем всё вместе: комбинированная логика
Наша цель — итоговый флаг $log_enable со следующей логикой:
- Если
$log_always_by_status=1или$log_always_by_uri=1— логируем. - Если
$is_static=1— не логируем вовсе или логируем очень редко (например, 0.1%). - Если статус 2xx/3xx и не статик — логируем сэмпл, например 1%.
В map нет арифметики, поэтому удобно собрать промежуточные флаги и свести их воедино ещё одной картой.
# Сэмпл для статических файлов — ещё более редкий, 0.1%
split_clients "$remote_addr$uri" $sample_static {
0.1% 1;
* 0;
}
# Итоговый флаг: 1 — пишем, 0 — нет
map "$log_always_by_status$log_always_by_uri$is_static$sample_2xx$sample_static" $log_enable {
# Есть обязательное логирование по статусу или URI
~^1....$ 1; # первый символ = 1
~^.1...$ 1; # второй символ = 1
# Статик: пишем только если сработал редкий сэмпл
~^..1.1$ 1; # is_static=1 и sample_static=1
# Обычный динамический успех: 1% выборки
~^0001.$ 1; # нет обязательных флагов, не статик, sample_2xx=1
# Иначе — не пишем
default 0;
}
Читаемость таких «строковых матриц» можно сохранить комментариями и аккуратным порядком переменных. Альтернатива — несколько вложенных map с более простыми шаблонами.
Итоговый server‑блок
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" rt=$request_time';
# Карты из предыдущих блоков здесь же... (см. выше)
server {
listen 80;
server_name example.test;
# Пишем общий лог по условию
access_log /var/log/nginx/access.log main if=$log_enable;
# Можно добавить отдельный лог только для ошибок (всегда)
access_log /var/log/nginx/access_errors.log main if=$log_always_by_status;
location / {
proxy_pass http://app;
}
}
Два файла позволяют быстро фильтровать инциденты, не разбирая общий поток. При желании для «ошибочного» лога можно сделать отдельный log_format с минимально достаточным набором полей.
Тонкая настройка: производительность и надёжность
Буферизация и сброс
access_log поддерживает buffer и flush. Буфер уменьшает количество write‑вызовов, flush гарантирует, что записи не будут «висеть» слишком долго. Баланс зависит от RPS и требований к свежести логов.
# Пример: буфер 64k, сброс каждые 2 секунды
access_log /var/log/nginx/access.log main if=$log_enable buffer=64k flush=2s;
Сжатие на лету (gzip) для файлов логов редко оправдано, если они тут же ротируются и отдаются агенту. Лучше сжимать при ротации.
Формат логов и объём
Каждое поле в log_format — это байты на диске и CPU на форматирование. Если вы не используете $http_referer или детальные тайминги, смело их убирайте. Сэмплировать имеет смысл только после того, как формат оптимизирован.
Стабильность сэмплов
Чтобы аналитика была воспроизводимой, важно детерминированное разделение: одинаковые запросы должны стабильно попадать в одну долю. Поэтому вход в split_clients выбирайте так, чтобы он минимально «шумел». Для публичных сайтов достаточно $remote_addr$request_uri, для аутентифицированных — можно добавить обезличенный идентификатор.
Когда переменные вычисляются
Переменные вроде $status известны лишь к моменту логирования, но для map это не проблема: Nginx вычисляет условие непосредственно перед записью. Главное — не пытаться на основании $status управлять маршрутизацией запроса внутри location до ответа.
Частые паттерны и готовые рецепты
1. Всегда писать 5xx и 4xx, 2xx/3xx — 1%
map $status $always {
~^5 1;
~^4 1;
default 0;
}
split_clients "$remote_addr$request_uri" $sample {
1% 1;
* 0;
}
map "$always$sample" $log_enable {
~^1 1;
~^01 1;
default 0;
}
access_log /var/log/nginx/access.log main if=$log_enable;
2. Не писать статик вовсе, кроме редкого 0.1%
map $uri $is_static {
~*\.(?:css|js|jpg|jpeg|png|svg|ico|woff2?)$ 1;
default 0;
}
split_clients "$uri" $sample_static {
0.1% 1;
* 0;
}
map "$is_static$sample_static" $log_static {
~^10 0; # статик без сэмпла — не пишем
~^11 1; # попал в сэмпл — пишем
default 0;
}
map "$log_static" $log_enable { default $log_static; }
access_log /var/log/nginx/access.log main if=$log_enable;
3. Разные проценты для API и фронтенда
map $uri $is_api {
~^/api/ 1;
default 0;
}
split_clients "$remote_addr$request_uri" $sample_api {
5% 1;
* 0;
}
split_clients "$remote_addr$request_uri" $sample_www {
0.5% 1;
* 0;
}
map "$is_api$sample_api$sample_www" $log_enable {
~^11.$ 1; # API и попал в API‑сэмпл
~^0.1$ 1; # фронт и попал в фронт‑сэмпл
default 0;
}
access_log /var/log/nginx/access.log main if=$log_enable;
Отладка и верификация
Прежде чем выкатывать на прод, проверьте конфигурацию:
nginx -t— валидация синтаксиса.- Временное повышение процента (
50%) вsplit_clientsдля быстрой проверки попаданий. - Сравнение числа строк в логе со счётчиками запросов (метрики или простые
stub_status/statusметрики, если они настроены) — чтобы убедиться в ожидаемой доле. - Просмотр отдельных запросов на «критичных» путях и при ошибках: эти записи должны появляться всегда.
Для локальной проверки можно временно добавить в формат лога метку флага, по которому включалось логирование, например tag=$log_enable, чтобы видеть источник решения.

Ротация и доставка логов
Сэмплирование уменьшает объём, но ротация по размеру/времени всё равно нужна. Если логи подхватывает агент, убедитесь, что ротация выполняется с корректным сигналом USR1 Nginx для переподхвата файлов или используйте механизмы, которые не требуют этого (в зависимости от вашего стека). При наличии нескольких файлов логов (общий и «ошибочный») настройте отдельные правила ретенции.
Если проект вырос и Nginx стал узким местом, выносите его и сбор логов на отдельный инстанс — удобнее на VDS с быстрым диском и отдельными лимитами I/O. Так проще масштабировать и хранение, и ретеншены.
Безопасность и приватность
Не записывайте чувствительные данные: токены и куки лучше редактировать на уровне приложения, а в Nginx можно удалить/замаскировать заголовки до бэкенда и не включать их в log_format. Сэмплирование не отменяет требований к хранению и защите логов, оно лишь уменьшает объём.
Типовые ошибки и как их избегать
- Случайное «зануление» логов: проверьте, что
$log_enableможет принимать значение1хотя бы для ошибок и критичных URI. - Слишком «шумный» вход
split_clients: если включить$time_msec, распределение будет случайным на каждом запросе — доля не стабильна. - Слишком агрессивный сэмпл 2xx: аналитика конверсий и А/Б‑тесты могут потерять точность, если выборка слишком мала. Отдельно учитывайте «редкие» пути.
- Лишние поля в
log_format: сначала оптимизируйте состав полей, потом включайте сэмпл — так экономия будет максимальной.
Контрольные вопросы перед выкладкой
- Какие статусы/URI должны писаться всегда? Сверьте список с командой разработки и поддержки.
- Достаточен ли процент выборки для 2xx/3xx на ваших объёмах трафика?
- Нужно ли выделить отдельный файл под ошибки для быстрого разбора инцидентов?
- Настроены ли ротация и мониторинг объёмов логов после изменений?
Итоги
Сэмплирование access‑логов в Nginx — простой и мощный приём, который даёт ощутимую выгоду по производительности и стоимости хранения. Используйте split_clients для детерминированного процента, map для выразительных правил по статусам и URI, а параметр if у access_log — как переключатель. Комбинируйте: ошибки и критичные пути — всегда, статик — редко, остальное — в разумной доле. Правильно подобранные проценты и формат логов позволяют сохранить наблюдаемость без перегрузки инфраструктуры.


