ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

JSON-логи Nginx и Apache: структурирование, поля и быстрая доставка в Loki/ELK

JSON-логи радикально упрощают парсинг и ускоряют доставку данных в Loki/ELK. В статье — практические схемы полей для Nginx и Apache, готовые конфиги, рекомендации по индексам, ротации и метрикам, влияние на производительность и типичные ошибки.
JSON-логи Nginx и Apache: структурирование, поля и быстрая доставка в Loki/ELK

Структурированные JSON-логи давно стали стандартом де-факто для доступного и надёжного мониторинга веб-сервисов. Они избавляют от сложного парсинга «плоских» строк, упрощают схему в хранилище и ускоряют построение запросов. В этой статье разберём, как включить JSON-формат в Nginx и Apache, какие поля логировать, как сразу доставлять записи в Loki или ELK, не теряя производительность и контроль над кардинальностью.

Зачем переходить на JSON-логи

Классические комбинированные форматы логов хороши для человеческого глаза, но неудобны для машинной обработки. Любой нестандартный символ в заголовке или URL ломает grok-паттерны, а обновление формата требует переписывать парсинг. JSON решает это за счёт явных ключей/типов и предсказуемой структуры. Итог — меньше CPU на парсинг, меньше ошибок, быстрые запросы в Loki/ELK и простая эволюция схемы.

Типовые проблемы плоских логов

  • Ненадёжный разбор кавычек и пробелов в User-Agent/Referer.
  • Ограниченность полей: трудно добавлять кастомные атрибуты без регресса парсера.
  • Высокие затраты CPU в Logstash/Elasticsearch на grok.
  • Сложная диагностика: неизвестно, какие поля именно доступны и в каком порядке.

Полноценный контроль над форматом логов и доставкой обычно доступен на собственном сервере. Если вы разворачиваете стек наблюдаемости, удобнее делать это на VDS с полным доступом к конфигурации Nginx/Apache и агентам доставки.

Дизайн схемы: рекомендуемый минимум полей

Схема полей — половина успеха. Держим баланс между полезностью и кардинальностью (числом уникальных значений). Рекомендуемая основа:

  • time — ISO8601/RFC3339 время события.
  • remote_ip — клиентский IP (с учётом X-Forwarded-For при необходимости).
  • vhost — виртуальный хост/домен.
  • scheme, protocol — схема и протокол HTTP.
  • request_method — метод.
  • path, query — путь и строка запроса.
  • status — код ответа.
  • bytes_sent, body_bytes_sent — байты по TCP и в теле соответственно.
  • referer, user_agent — реферер и агент.
  • request — первая строка запроса (для быстрой диагностики).
  • request_time — общее время обработки запроса.
  • upstream_addr, upstream_status, upstream_response_time, upstream_connect_time — метрики бэкенда (если есть proxy_pass/балансировка).
  • cache_status — статусы кэша (Nginx proxy_cache/у CDN), если используется.
  • trace_id/request_id — корреляция запросов (из заголовка X-Request-ID или аналогичного).
  • tls_version, tls_cipher — TLS-атрибуты для HTTPS.

Лучше иметь «плоскую» схему с понятными типами (числа как числа, время как дата) и не превращать каждое поле в label (Loki) или keyword (ES) — это повышает кардинальность и стоимость.

Пример log_format JSON для Nginx

Nginx: включаем JSON-формат

Nginx позволяет определить собственный log_format с escape=json. Ниже — практичный рецепт с ключевыми полями и кэш-статусом.

map $upstream_cache_status $cache_status {
    default "";
    HIT "HIT";
    MISS "MISS";
    REVALIDATED "REVALIDATED";
    EXPIRED "EXPIRED";
    BYPASS "BYPASS";
}

log_format json_combined escape=json '{'
    '"time":"$time_iso8601",'
    '"remote_ip":"$remote_addr",'
    '"x_forwarded_for":"$http_x_forwarded_for",'
    '"vhost":"$host",'
    '"scheme":"$scheme",'
    '"protocol":"$server_protocol",'
    '"request_method":"$request_method",'
    '"path":"$uri",'
    '"query":"$args",'
    '"request":"$request",'
    '"status":$status,'
    '"bytes_sent":$bytes_sent,'
    '"body_bytes_sent":$body_bytes_sent,'
    '"referer":"$http_referer",'
    '"user_agent":"$http_user_agent",'
    '"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",'
    '"cache_status":"$cache_status",'
    '"gzip_ratio":"$gzip_ratio",'
    '"connection":"$connection",'
    '"connection_requests":"$connection_requests",'
    '"trace_id":"$http_x_request_id"'
'}';

access_log /var/log/nginx/access_json.log json_combined buffer=64k flush=5s;
open_log_file_cache max=1000 inactive=1h;

Пояснения и практические моменты:

  • $time_iso8601 даёт корректную временную метку с часовым поясом для парсинга в Loki/ELK.
  • $http_x_request_id заполняется приложением/балансировщиком. Если нет — всё равно логируйте это поле пустым для унификации.
  • buffer=64k и flush=5s уменьшают syscalls и нагрузку на диск. Подбирайте под трафик.
  • Если используете real IP за прокси, не забудьте корректно настроить set_real_ip_from и real_ip_header.
  • Ошибка Nginx-логов в JSON для error_log не поддерживается — оставьте stderr/syslog в стандартном формате и соберите их отдельно.

Apache: JSON через mod_log_config

Apache 2.4 поддерживает опцию escape=json в LogFormat, что позволяет безопасно кодировать кавычки и спецсимволы. Пример универсального формата:

LogFormat "{ \"time\": \"%{%Y-%m-%dT%H:%M:%S%z}t\", \"remote_ip\": \"%a\", \"vhost\": \"%v\", \"request_method\": \"%m\", \"path\": \"%U\", \"query\": \"%q\", \"protocol\": \"%H\", \"status\": %>s, \"bytes_sent\": %B, \"referer\": \"%{Referer}i\", \"user_agent\": \"%{User-Agent}i\", \"request\": \"%r\", \"request_time_s\": %T, \"request_time_us\": %D, \"port\": %p, \"ssl\": \"%{HTTPS}x\", \"tls_version\": \"%{SSL_PROTOCOL}x\", \"tls_cipher\": \"%{SSL_CIPHER}x\", \"trace_id\": \"%{X-Request-ID}i\" }" json escape=json
CustomLog logs/access_json.log json

Пояснения:

  • %{%Y-%m-%dT%H:%M:%S%z}t — ISO-время. Можете добавить суффикс :00 к зоне при постобработке, если требуется RFC3339.
  • %>s — финальный статус (после редиректов/проксирования).
  • %U и %q разделяют путь и строку запроса. %r сохраняет исходную первую строку.
  • %{HTTPS}x, %{SSL_PROTOCOL}x, %{SSL_CIPHER}x работают при наличии mod_ssl.
  • escape=json гарантирует корректные кавычки и спецсимволы в полях.

Если у вас ещё не включён HTTPS, оформите и подключите актуальные SSL-сертификаты — это даст корректные TLS-поля и закроет передачу чувствительных данных в открытом виде.

Ротация и буферизация

Ротация файлов критична для диска и доставки. Для Nginx используйте стандартный logrotate и директивы буферизации. Для Apache — либо logrotate, либо пайпинг в rotatelogs (удобно для частой ротации). Пример logrotate для Nginx:

/var/log/nginx/access_json.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    sharedscripts
    postrotate
        test -f /run/nginx.pid && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

Сигнал USR1 аккуратно переведёт Nginx на новый файл без перезапуска. Для Apache аналогично с apachectl graceful при необходимости.

Доставка в Loki: promtail и pipeline

Лучший путь для Loki — promtail, который понимает JSON и умеет превращать часть полей в метки. Главное правило: не повышайте кардинальность меток. Никогда не делайте метками path, query, user_agent, remote_ip — держите их полями в строке.

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /var/lib/promtail/positions.yml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: nginx_json
    static_configs:
      - targets: [localhost]
        labels:
          job: web
          app: nginx
          host: ${HOSTNAME}
          __path__: /var/log/nginx/access_json.log
    pipeline_stages:
      - json:
          expressions:
            time:
            vhost:
            status:
            request_method:
            path:
            query:
            remote_ip:
            user_agent:
            referer:
            request_time:
            upstream_status:
            upstream_response_time:
            upstream_addr:
      - timestamp:
          source: time
          format: RFC3339
      - labels:
          vhost:
          status:
          request_method:

Пояснения к promtail-пайплайну:

  • json.expressions извлекает поля; не перечисляйте всё подряд, только нужное.
  • timestamp переучивает время из поля time; указывайте формат RFC3339/RFC3339Nano в зависимости от точности.
  • labels ограничиваем минимумом: vhost, status, request_method. Остальное оставляем в JSON-строке лога.
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Примеры быстрых запросов в Loki

  • Ошибки 5xx по виртуальному хосту: используйте фильтр по меткам {app="nginx", vhost="example.org", status="500"}.
  • Топ IP для 404: topk(20, count_over_time({app="nginx", status="404"}[5m])) с | json на этапе запроса для выборки remote_ip.
  • 95-й перцентиль request_time: quantile_over_time(0.95, {app="nginx"} |= "request_time" | unwrap request_time [5m]).

Доставка в ELK: Filebeat или Logstash

Если строки логов — это уже валидный JSON, проще всего применять ndjson-парсер в Filebeat или codec => json в Logstash. Рекомендуем жёстко привести типы и парсить время в @timestamp.

Вариант 1: Filebeat -> Elasticsearch ingest pipeline

filebeat.inputs:
  - type: filestream
    id: nginx-json
    paths:
      - /var/log/nginx/access_json.log
    parsers:
      - ndjson:
          target: ""
          overwrite_keys: true
          add_error_key: true
          expand_keys: true
    fields:
      app: nginx
    fields_under_root: true

output.elasticsearch:
  hosts: ["localhost:9200"]
  pipeline: "nginx_json"

Пример ingest pipeline для Elasticsearch, который парсит время, приводит типы и переименовывает vhost в host.name:

{
  "processors": [
    { "date": { "field": "time", "formats": ["strict_date_optional_time", "yyyy-MM-dd'T'HH:mm:ssXXX"] } },
    { "remove": { "field": "time", "ignore_missing": true } },
    { "convert": { "field": "status", "type": "integer", "ignore_missing": true } },
    { "convert": { "field": "bytes_sent", "type": "long", "ignore_missing": true } },
    { "convert": { "field": "body_bytes_sent", "type": "long", "ignore_missing": true } },
    { "convert": { "field": "request_time", "type": "float", "ignore_missing": true } },
    { "rename": { "field": "vhost", "target_field": "host.name", "ignore_missing": true } }
  ]
}

Вариант 2: Logstash -> Elasticsearch

input {
  file {
    path => "/var/log/nginx/access_json.log"
    codec => json
    sincedb_path => "/var/lib/logstash/sincedb_nginx_json"
  }
}

filter {
  date { match => ["time", "ISO8601"] target => "@timestamp" }
  mutate {
    convert => { "status" => "integer" }
    convert => { "bytes_sent" => "integer" }
    convert => { "body_bytes_sent" => "integer" }
    convert => { "request_time" => "float" }
  }
  mutate { remove_field => ["time"] }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
    index => "nginx-json-%{+YYYY.MM.dd}"
  }
}

В ELK не используйте анализируемый тип для полей вроде user_agent, path, referer без необходимости. Обычно они нужны как keyword для фильтрации и агрегаций, а полнотекстовый анализ на них лишь увеличит размер индекса.

Дашборды и пайплайны для Loki и ELK

Контроль кардинальности и типизации

На практике именно кардинальность определяет стоимость и производительность. Несколько правил:

  • В Loki метки должны быть крайне стабильными: job, app, vhost, иногда status/request_method. Остальное — в теле.
  • В Elasticsearch избегайте динамических полей с бесконечным числом ключей (вложенных JSON-объектов). При необходимости создавайте явную маппинг-схему или ingest-процессор для нормализации.
  • Числа храните числами: status как integer, байты как long, время как date/float. Это ускоряет агрегации.
  • Не индексируйте поля, которые используются только для отображения (например, исходная request), или храните их как text с index=false.

Проверка и отладка

  • Проверка конфигурации Nginx: nginx -t, Apache: apachectl -t.
  • Валидность JSON одной строки: tail -n1 /var/log/nginx/access_json.log | jq ..
  • Время доставки: сгенерируйте запрос curl и проверьте его появление в Loki/ELK, замерьте задержку.
  • Следите за размером файлов и скоростью роста индекса — скорректируйте ротацию и поля.

Производительность: влияние логирования

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

  • Nginx: buffer и flush у access_log, open_log_file_cache, быстрый диск.
  • Apache: пайпинг в rotatelogs или BufferedLogs при работе через pipe; избегайте синхронных внешних обработчиков.
  • Поставщики: promtail/filebeat — используйте позиции, ограничивайте число открытых файлов, настраивайте backpressure.

Безопасность и ПДн

Не записывайте в логи секреты: токены, сессии, Authorization, полные номера карт и т. п. Если приложение передаёт такие параметры в query или заголовках, фильтруйте их на уровне Nginx/Apache или ingest-пайплайна. При необходимости редактируйте query и заголовки, маскируя критические значения (например, оставляя только последние 4 символа). Для соответствия требованиям о персональных данных обезличивайте IP (например, обнуляйте младшие биты IPv4) и сокращайте retention. Дополнительно проверьте жёсткость настроек по материалам о заголовках безопасности и CORS: см. рекомендации в статье про HTTP security headers для Nginx/Apache (подборка заголовков безопасности) и настройку CORS (CORS для Nginx/Apache).

Наблюдаемость: что смотреть в Loki/ELK

  • SLO по ошибкам: доля 5xx по вхождению в окно времени.
  • Перцентиль латентности: P50/P95/P99 по request_time с разрезом по vhost и upstream_addr.
  • Сбойные бэкенды: рост upstream_status 502/504 и длительное upstream_connect_time.
  • Кэш-хитрейт: доля HIT/MISS по cache_status. По теме кэша также см. практику по Cache-Control/ETag (управление кэшем статики).

Типовые ловушки

  • Неверный формат времени. Убедитесь, что ваш парсер понимает зону и точность (RFC3339 часто идеален).
  • Случайные метки в Loki из динамических полей — опасный взрыв кардинальности. Жёстко контролируйте labels.
  • Ротация без сигнала демону — потеря нескольких секунд логов (приложение продолжает писать в дескриптор удалённого файла).
  • JSON, сломанный непечатаемыми символами или бинарными данными — всегда используйте escape=json.

Минимальный чеклист внедрения

  1. Определите схему полей: минимум из раздела выше, добавьте специфические для вашего проекта.
  2. Включите JSON-лог в Nginx/Apache и перезапустите с проверкой конфигурации.
  3. Настройте ротацию и буферизацию логов на стороне веб-сервера.
  4. Поднимите promtail или filebeat, проверьте позиции и парсинг.
  5. Отдельным шагом создайте ingest-пайплайн (ELK) или pipeline в promtail для времени и типов.
  6. Соберите 2–3 ключевые дашборды: ошибки, латентность, кэш.
  7. Проведите нагрузочный тест: оцените overhead и возможные узкие места.
  8. Включите алерты по 5xx, timeouts и росту латентности.

Правильно спроектированные JSON-логи экономят часы на парсинге и поиске инцидентов, а также снижают стоимость хранения. Начните с минимальной схемы, аккуратно добавляйте новые поля и держите под контролем кардинальность меток и индексов — так вы получите предсказуемую производительность и прозрачную наблюдаемость без сюрпризов.

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

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

MySQL: EXPLAIN ANALYZE и optimizer_trace — читаем план, считаем время, находим узкие места OpenAI Статья написана AI (GPT 5)

MySQL: EXPLAIN ANALYZE и optimizer_trace — читаем план, считаем время, находим узкие места

Разберём диагностику медленных запросов в MySQL 8.0 с помощью EXPLAIN ANALYZE и optimizer_trace: где найти узел, который съел врем ...
Canary-выкатка и ротация PEM Let’s Encrypt без простоя в Nginx и Apache OpenAI Статья написана AI (GPT 5)

Canary-выкатка и ротация PEM Let’s Encrypt без простоя в Nginx и Apache

Пошаговый план обновления PEM-сертификатов Let’s Encrypt без обрывов: атомарная замена через симлинки или mv, canary-выкатка на од ...
IPv6 ACL ::/0 для reverse proxy: как не открыть админку всему миру OpenAI Статья написана AI (GPT 5)

IPv6 ACL ::/0 для reverse proxy: как не открыть админку всему миру

IPv6 нередко включён по умолчанию, а доступ к админке ограничивают только для IPv4. В режиме dual stack это превращается в «дыру»: ...