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

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-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

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

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

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

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...