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

Nginx access.log: почему real_user_agent пустой и как логировать реальный User-Agent

Если в access.log поле real_user_agent пустое, причина обычно в цепочке прокси/CDN или в том, что логируется не та переменная. Покажу, как диагностировать заголовки, собрать «реальный» User-Agent через map с fallback и маркером пустоты, и при желании обогатить логи geoip2.
Nginx access.log: почему real_user_agent пустой и как логировать реальный User-Agent

Иногда в nginx access.log внезапно появляется поле вида real_user_agent — и оно пустое. Обычно это всплывает в момент, когда вы хотите сделать аналитику по ботам/браузерам, собрать метрики «пустых» запросов, настроить алерты по аномалиям или просто найти проблемный клиент. Пустой real User-Agent почти всегда симптом: заголовок теряется в цепочке, вы логируете не то значение, или переменная заполняется только в отдельных сценариях.

Ниже — практичная схема: как понять, что именно у вас записано в поле real_user_agent, как проверить, видит ли Nginx User-Agent, как собрать «реальный UA» через map с понятным fallback, и как аккуратно добавить geo-контекст через geoip2 для observability.

Что такое real_user_agent в access.log и почему он может быть пустым

В «чистом» Nginx нет встроенной переменной $real_user_agent. Если вы видите в логе real_user_agent=..., то это, как правило, имя поля внутри вашего log_format, а значение берётся из какой-то переменной или из результата map.

Чаще всего в подобных полях встречается одно из следующего:

  • $http_user_agent — заголовок User-Agent, который реально пришёл в Nginx;
  • $upstream_http_user_agent — заголовок из ответа апстрима (почти никогда не подходит для клиентского UA);
  • $http_x_original_user_agent, $http_x_device_user_agent и подобные — попытка взять UA из прокси-заголовков;
  • кастомная переменная из map, где задана логика «если есть X-Original-*, иначе обычный UA».

Причины пустого значения обычно такие:

  • клиент или бот действительно не прислал User-Agent;
  • CDN/WAF/LB удаляет или переписывает заголовок;
  • в конфиге вы логируете переменную, которая не существует или не заполняется в данном контексте;
  • вы ждёте UA в X-* заголовке, но он не пробрасывается до вашего Nginx;
  • часть запросов — health-check/мониторинг/внутренние вызовы, где UA нередко пустой.

Быстрая диагностика: видит ли Nginx User-Agent на входе

Сначала выясните, какую именно переменную вы подставляете в поле real_user_agent. Удобнее всего посмотреть эффективный конфиг:

sudo nginx -T

Найдите свои log_format и access_log и проверьте: откуда берётся значение (например, $http_user_agent или результат map).

Дальше на короткое время включите диагностический формат в отдельный файл, чтобы увидеть «сырые» входные заголовки (и не ломать основной парсинг):

log_format ua_debug 'ua="$http_user_agent" x_orig_ua="$http_x_original_user_agent" x_dev_ua="$http_x_device_user_agent" req="$request"';
access_log /var/log/nginx/ua_debug.log ua_debug;

Сделайте тестовый запрос:

curl -I -H 'User-Agent: TestUA/1.0' http://127.0.0.1/

Если в ua_debug.log ua="..." заполнен — базовый $http_user_agent до Nginx доходит, а проблема именно в вашей логике «real». Если пусто — заголовок теряется раньше (или тестируете не тот вход: другой порт, другой балансировщик, другой vhost).

Пример настройки log_format и map для выбора реального User-Agent

Собираем «реальный» User-Agent через map: понятный fallback и метка источника

Самая надёжная практика — сделать прозрачную логику выбора источника UA. То есть: берём «канонический» заголовок от доверенного edge (если он есть), иначе используем обычный User-Agent. И обязательно логируем источник, чтобы было видно, почему сработал fallback.

Пример: доверенный балансировщик добавляет X-Original-User-Agent. Тогда:

map $http_x_original_user_agent $real_user_agent {
    default $http_x_original_user_agent;
    ""      $http_user_agent;
}

map $http_x_original_user_agent $real_user_agent_source {
    default x_original_user_agent;
    ""      user_agent;
}

И нормализуем пустоту явным маркером (чтобы парсер/дашборд не путал «пусто» и «поля нет»):

map $real_user_agent $real_user_agent_norm {
    default $real_user_agent;
    ""      "-";
}

Дальше выводим в расширенный формат логов (key=value удобен для grep/awk и для последующего парсинга):

log_format access_kv 'ts=$time_iso8601 '
                    'host=$host '
                    'client_ip=$remote_addr '
                    'method=$request_method uri="$request_uri" '
                    'status=$status bytes=$body_bytes_sent '
                    'rt=$request_time uct=$upstream_connect_time urt=$upstream_response_time '
                    'ua="$real_user_agent_norm" ua_src=$real_user_agent_source '
                    'ref="$http_referer"';

Теперь «пустой UA» становится нормальным управляемым кейсом: вы увидите ua="-" и сможете посчитать долю таких запросов, корреляцию с URI, подсетями и т.д.

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

Если real_user_agent всё равно пустой

Значит, пустыми оказались оба источника: и канонический X-Original-User-Agent, и обычный User-Agent. Для части трафика это норма (health-check), но если это «вдруг стало много» — ищите фильтрацию заголовков на edge.

Проверьте две вещи: 1) действительно ли edge пробрасывает нужный заголовок к Nginx, 2) не переопределяете ли вы доступ к заголовкам где-то выше (например, другим прокси-слоем). Полезно также почитать про связанные истории с прокси-цепочками и кэшированием заголовков в статье HTTP Range и кэширование в Nginx/Apache: где ломаются заголовки и ответы.

Частая ошибка: путают client headers и upstream_* в log_format

Одна из типичных причин пустого «real_user_agent» — использование $upstream_http_user_agent. Это заголовок, который пришёл в ответе от апстрима (или не пришёл вовсе). Для клиентского UA он не подходит, и вы будете «лечить клиентов», хотя ошибка в формате логов.

Запомните простое правило:

  • клиентские заголовки — $http_* (например, $http_user_agent);
  • заголовки ответа апстрима — $upstream_http_*;
  • «реальный IP клиента» за прокси решается отдельными настройками realip и не связан напрямую с UA.

Если вы за прокси/CDN/WAF: как не потерять заголовки и не доверять подделкам

Если Nginx стоит за балансировщиком, CDN или WAF, то часть X-* заголовков может быть вырезана «санитизацией». Также некоторые решения не пробрасывают нестандартные заголовки без allowlist.

Считать «реальным» можно только тот User-Agent, который пришёл от клиента напрямую или был добавлен доверенным узлом вашей инфраструктуры. Любой X-Original-User-Agent, который клиент может прислать сам, подделывается за одну секунду.

Практичная схема такая: выберите один канонический заголовок, который добавляет только ваш edge. На edge удаляйте входящий пользовательский вариант этого заголовка (если он есть), затем добавляйте свой. А на Nginx используйте map, как показано выше, и логируйте источник.

Если вместе с UA вы внедряете HSTS/редиректы и приводите домены к единому виду, полезно держать под рукой чек-лист из материала миграция домена: 301, HSTS и SSL без сюрпризов.

Обогащение access.log через geoip2: полезно, но аккуратно

Geo-контекст часто помогает понять всплески «пустых UA» и аномальные сигнатуры: страна, ASN, организация. Но помните: это увеличивает размер строк логов и может повысить чувствительность данных.

Минимальный пример (если модуль geoip2 установлен и базы доступны на сервере):

geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
    $geoip2_country_code country iso_code;
    $geoip2_country_name country names en;
}

geoip2 /usr/share/GeoIP/GeoLite2-ASN.mmdb {
    $geoip2_asn autonomous_system_number;
    $geoip2_as_org autonomous_system_organization;
}

log_format access_obs 'ts=$time_iso8601 ip=$remote_addr status=$status '
                      'ua="$real_user_agent_norm" ua_src=$real_user_agent_source '
                      'cc=$geoip2_country_code asn=$geoip2_asn as_org="$geoip2_as_org" '
                      'req="$request" rt=$request_time';

Рекомендации по эксплуатации:

  • не пишите в access.log слишком много геополей сразу: кардинальность и размер строк растут быстро;
  • продумайте ротацию, компрессию и ретеншн, чтобы «богатый» лог не съел диск;
  • если логи уходят в централизованное хранилище, заранее согласуйте схему полей и доступы.
FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Контроль качества: тесты, чтобы убедиться, что всё работает

После изменений проверьте три сценария:

  1. Обычный запрос с User-Agent: значение должно логироваться в ua.
  2. Запрос без UA: в логе должен быть маркер ua="-", а не пустота.
  3. Запрос с вашим каноническим заголовком: должен выбираться он, а ua_src должен отражать источник.

Пример тестов:

curl -I -H 'User-Agent: TestUA/1.0' http://127.0.0.1/
curl -I -H 'User-Agent:' http://127.0.0.1/
curl -I -H 'X-Original-User-Agent: EdgeUA/9.9' -H 'User-Agent: ClientUA/1.0' http://127.0.0.1/

И применяйте конфиг безопасно:

sudo nginx -t
sudo systemctl reload nginx

Проверка логирования User-Agent с помощью тестовых curl-запросов

Частые грабли и как их избежать

Разная логика в разных server/location

Если у вас много виртуальных хостов, легко случайно разъехать по форматам логов и правилам map. Держите map в контексте http, а access_log и log_format — максимально единообразными.

Непредсказуемые X-заголовки от клиентов

Не полагайтесь на заголовки, которые клиент может прислать сам. Если нужен «реальный UA от edge» — делайте внутренний стандарт и гарантируйте, что только ваш edge выставляет канонический заголовок.

Слишком тяжёлый access.log

User-Agent может быть длинным, плюс referer, плюс geoip2 — и лог начинает расти очень быстро. На нагруженных проектах часто разумнее включать «богатый» формат точечно (на проблемных vhost/URI), а в остальных оставить базовый. Если для проекта нужен предсказуемый стек и контроль ресурсов, чаще проще жить на VDS, где вы сами задаёте правила логирования, ротации и отправки логов.

Итог: что делать, если в access.log real_user_agent пустой

  1. Выясните, какая переменная реально подставляется в поле real_user_agent (через nginx -T и временный debug-лог).
  2. Проверьте, доходит ли $http_user_agent до Nginx на нужном входе.
  3. Если вы за прокси/CDN — выберите доверенный канонический заголовок и настройте его проброс (и защиту от подделки на edge).
  4. Соберите «реальный UA» через map с fallback и явным маркером пустоты.
  5. Опционально добавьте geoip2 поля, но контролируйте размер логов и схему полей.

После этого access.log перестанет быть «простынёй» и станет рабочим инструментом: вы сможете уверенно фильтровать и анализировать User-Agent, видеть источник значения и быстро находить причины, почему поле внезапно стало пустым.

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

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

LVM snapshot и I/O: почему растёт задержка и как делать backup без сюрпризов OpenAI Статья написана AI (GPT 5)

LVM snapshot и I/O: почему растёт задержка и как делать backup без сюрпризов

LVM snapshot удобен для горячего backup, но почти всегда увеличивает нагрузку на диск. Объясняю, откуда берётся рост I/O latency, ...
PostgreSQL SSL/TLS: sslmode require и verify-full, root.crt, SNI и типовые ошибки проверки сертификата OpenAI Статья написана AI (GPT 5)

PostgreSQL SSL/TLS: sslmode require и verify-full, root.crt, SNI и типовые ошибки проверки сертификата

Пошагово настраиваем SSL/TLS в PostgreSQL и клиентах: разница между sslmode=require и verify-full, где хранится root.crt, как устр ...
DNS TTL: как работает время жизни записей, кэш резолверов и «распространение» изменений OpenAI Статья написана AI (GPT 5)

DNS TTL: как работает время жизни записей, кэш резолверов и «распространение» изменений

TTL в DNS — не «время распространения», а срок хранения ответа в кэше. Разберём, где живут кэши резолверов, ОС и приложений, почем ...