Иногда в 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).

Собираем «реальный» 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, подсетями и т.д.
Если 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 слишком много геополей сразу: кардинальность и размер строк растут быстро;
- продумайте ротацию, компрессию и ретеншн, чтобы «богатый» лог не съел диск;
- если логи уходят в централизованное хранилище, заранее согласуйте схему полей и доступы.
Контроль качества: тесты, чтобы убедиться, что всё работает
После изменений проверьте три сценария:
- Обычный запрос с
User-Agent: значение должно логироваться вua. - Запрос без UA: в логе должен быть маркер
ua="-", а не пустота. - Запрос с вашим каноническим заголовком: должен выбираться он, а
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

Частые грабли и как их избежать
Разная логика в разных 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 пустой
- Выясните, какая переменная реально подставляется в поле
real_user_agent(черезnginx -Tи временный debug-лог). - Проверьте, доходит ли
$http_user_agentдо Nginx на нужном входе. - Если вы за прокси/CDN — выберите доверенный канонический заголовок и настройте его проброс (и защиту от подделки на edge).
- Соберите «реальный UA» через
mapс fallback и явным маркером пустоты. - Опционально добавьте geoip2 поля, но контролируйте размер логов и схему полей.
После этого access.log перестанет быть «простынёй» и станет рабочим инструментом: вы сможете уверенно фильтровать и анализировать User-Agent, видеть источник значения и быстро находить причины, почему поле внезапно стало пустым.


