Структурированные 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) — это повышает кардинальность и стоимость.

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-строке лога.
Примеры быстрых запросов в 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 метки должны быть крайне стабильными:
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_status502/504 и длительноеupstream_connect_time. - Кэш-хитрейт: доля HIT/MISS по
cache_status. По теме кэша также см. практику по Cache-Control/ETag (управление кэшем статики).
Типовые ловушки
- Неверный формат времени. Убедитесь, что ваш парсер понимает зону и точность (
RFC3339часто идеален). - Случайные метки в Loki из динамических полей — опасный взрыв кардинальности. Жёстко контролируйте
labels. - Ротация без сигнала демону — потеря нескольких секунд логов (приложение продолжает писать в дескриптор удалённого файла).
- JSON, сломанный непечатаемыми символами или бинарными данными — всегда используйте
escape=json.
Минимальный чеклист внедрения
- Определите схему полей: минимум из раздела выше, добавьте специфические для вашего проекта.
- Включите JSON-лог в Nginx/Apache и перезапустите с проверкой конфигурации.
- Настройте ротацию и буферизацию логов на стороне веб-сервера.
- Поднимите promtail или filebeat, проверьте позиции и парсинг.
- Отдельным шагом создайте ingest-пайплайн (ELK) или pipeline в promtail для времени и типов.
- Соберите 2–3 ключевые дашборды: ошибки, латентность, кэш.
- Проведите нагрузочный тест: оцените overhead и возможные узкие места.
- Включите алерты по 5xx, timeouts и росту латентности.
Правильно спроектированные JSON-логи экономят часы на парсинге и поиске инцидентов, а также снижают стоимость хранения. Начните с минимальной схемы, аккуратно добавляйте новые поля и держите под контролем кардинальность меток и индексов — так вы получите предсказуемую производительность и прозрачную наблюдаемость без сюрпризов.


