Выберите продукт

Сэмплирование access‑логов в Nginx: map, if и условия по URI/статусу

На загруженных проектах access‑логи съедают диск и CPU быстрее, чем сам трафик. Разумное сэмплирование в Nginx позволяет писать всё важное (ошибки, аномалии) и лишь часть рутины (2xx/статик). В статье — рабочие паттерны и готовые конфиги.
Сэмплирование access‑логов в Nginx: map, if и условия по URI/статусу

Когда сайт или 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;
}

Схема принятия решения: статус и URI через map формируют флаг логирования

Собираем всё вместе: комбинированная логика

Наша цель — итоговый флаг $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 с минимально достаточным набором полей.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Тонкая настройка: производительность и надёжность

Буферизация и сброс

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, чтобы видеть источник решения.

Ротация и буферизация access‑логов Nginx: файлы и сигнал USR1

Ротация и доставка логов

Сэмплирование уменьшает объём, но ротация по размеру/времени всё равно нужна. Если логи подхватывает агент, убедитесь, что ротация выполняется с корректным сигналом 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 — как переключатель. Комбинируйте: ошибки и критичные пути — всегда, статик — редко, остальное — в разумной доле. Правильно подобранные проценты и формат логов позволяют сохранить наблюдаемость без перегрузки инфраструктуры.

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

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

ACME renewal monitoring: systemd timer, healthcheck и правильный fullchain.pem OpenAI Статья написана AI (GPT 5)

ACME renewal monitoring: systemd timer, healthcheck и правильный fullchain.pem

Разберём практичную схему мониторинга продления ACME/Let’s Encrypt: почему systemd timer удобнее cron, как правильно использовать ...
EPP-статусы домена: ok, clientHold и serverHold — что означают и как снять блокировку OpenAI Статья написана AI (GPT 5)

EPP-статусы домена: ok, clientHold и serverHold — что означают и как снять блокировку

Если домен зарегистрирован и NS прописаны, но сайт и почта не работают, часто виноваты EPP-статусы в WHOIS/RDAP. Разберём ok, clie ...
Kubernetes NodeLocal DNSCache: как победить DNS latency и NXDOMAIN storm в CoreDNS OpenAI Статья написана AI (GPT 5)

Kubernetes NodeLocal DNSCache: как победить DNS latency и NXDOMAIN storm в CoreDNS

DNS в Kubernetes часто становится скрытым узким местом: растёт latency, CoreDNS уходит в CPU, на нодах раздувается conntrack и всп ...