Перейти к содержанию

Логирование

Сервер и агент оба логируют структурно через zap — в продакшене JSON, одна строка на событие. Эта страница — про формат, уровни и где физически искать логи каждого компонента.

Сервер

Формат и уровень

ALATYR_LOG_LEVEL (по умолчанию info; допустимые значения debug/info/warn/error) и ALATYR_DEV_MODE вместе определяют кодировщик:

  • dev_mode=false (прод) — продакшен-профиль zap (JSON-encoder, время в ISO8601). Один JSON-объект на строку stdout — рассчитан на сбор логовым агрегатором (Loki, ELK, CloudWatch и т.п.), не на чтение человеком в терминале напрямую.
  • dev_mode=true — dev-профиль zap с цветным human-readable выводом.

Строка starting alatyr-server, которую сервер пишет при старте, несёт version, commit, built_at — тот же билд, что отдаёт GET /api/v1/version (см. раздел «Публичные и служебные» на странице REST API), так что любой лог-стрим прослеживается до конкретного бинаря.

Структурная корреляция: request_id и serial

Каждый запрос получает поле request_id: входящий заголовок X-Request-ID используется как есть, либо генерируется новый UUID; сервер возвращает то же значение в заголовке ответа. Все строки лога, относящиеся к одному запросу — включая финальную access-строку — несут одинаковый request_id.

Как только запрос идентифицирует конкретное устройство (enroll, enroll/user, enroll/ssh, enroll/ssh-key), к последующим строкам того же запроса добавляется поле serial. С этого момента поиск по serial в Loki/ELK восстанавливает всю историю конкретного устройства поперёк множества запросов, тогда как request_id связывает строки только одного запроса.

На каждый завершённый запрос пишется одна структурная access-строка с полями: status, method, path (без query-строки — там могут быть токены), route (шаблон вида /api/v1/devices/:serial, низкая кардинальность, без PII), latency_ms, client_ip (первый адрес из X-Forwarded-For, иначе адрес соединения), bytes, и, если запрос аутентифицирован — actor/actor_role. Если обработчик приложил к запросу ошибку, рядом появляется поле errors — текст ошибки без тела запроса. Уровень строки следует за исходом: 5xx → Error, 4xx → Warn, иначе Info. Исключение — высокочастотные агентские polling-роуты (/requests/:id/status, /checkin, /bundle-version, /certificate): их успешные вызовы логируются на Debug, чтобы не захламлять Info-поток, а любой 4xx/5xx на этих же роутах всё равно всплывает на Warn/Error. GET /health не логируется вовсе — probe каждые несколько секунд забил бы сигнал шумом.

Паника в хэндлере перехватывается и записывается как структурная строка panic recovered (method, path, стектрейс) на уровне Error; клиент при этом получает generic 500 — вместо голого текстового стектрейса в stderr, который сломал бы предположение «один JSON-стрим на stdout».

Пример строки (прод, JSON)

{"level":"info","ts":"2026-08-06T10:15:03.120Z","msg":"request",
 "request_id":"3fa2...c91e","serial":"C02X1234ABCD",
 "status":200,"method":"POST","path":"/api/v1/requests/.../checkin",
 "route":"/api/v1/requests/:id/checkin","latency_ms":0.0124,
 "client_ip":"10.0.4.17","bytes":128,
 "actor":"agent","actor_role":""}

latency_ms в проде измеряется в СЕКУНДАХ

Имя поля обещает миллисекунды, а кодировщик продакшен-профиля пишет секунды: 0.0124 в примере выше — это 12,4 мс. В dev_mode=true формат другой, строка вида 12.4ms. Пороги в запросах и алертах задавайте по тому, что реально лежит в вашем потоке, а не по имени поля.

Точный набор полей одной строки зависит от того, что уже известно на момент записи (serial есть не у каждого запроса — только у тех, где устройство уже идентифицировано).

Агент

Логгер агента — тоже zap: по умолчанию JSON-encoder на уровне info, при флаге --verbose (или его платформенном эквиваленте) — human-readable console-encoder на debug. Вывод всегда идёт на stdout и, если агенту передан --log-file, дублируется («tee») в этот файл. Установщик передаёт его на всех платформах, поэтому файл есть и искать его стоит именно там.

У агента две половины, и журналов тоже два

Это первое, что стоит знать при разборе: машинная половина (цели wifi, общий enrollment_token) и пользовательская (user_mtls, ad_logon, ssh, k8s) — разные процессы под разными учётными записями, и пишут они в разные файлы. Симптом «цель не выдаётся» чаще живёт в пользовательском журнале, а его как раз и не открывают.

ОС Машинная половина Пользовательская половина
Linux /var/log/alatyr-agent.log, он же journalctl -u alatyr-agent.service -f ~/.local/state/alatyr-agent/user-agent.log (служба systemctl --user status alatyr-agent-user)
Windows C:\ProgramData\AlatyrAgent\agent.log %LOCALAPPDATA%\AlatyrAgent\user-agent.log
macOS — (машинной половины нет: агент целиком per-user) ~/Library/Logs/AlatyrAgent/agent.log, рядом sshagent.log для процесса ssh-agent

Обе службы Linux — резидентные, а не разовый запуск по таймеру: через машинную половину пользовательская получает enrollment_token, и живёт этот канал ровно столько, сколько живёт процесс. Поэтому systemctl status отвечает на вопрос «работает ли агент» буквально, а пустой журнал одной из половин означает, что она не запущена.

Детали установки — Установка, устройство агента — Агенты.

Журнал ограничен по размеру

Файл агента ротируется сам: он растёт до предела, после чего старое вытесняется. Это не настройка на будущее, а следствие замера — журнал обмена со смарт-картой дорастал до сотен мегабайт, переставал читаться и накапливал идентификаторы устройства и человека без срока хранения. Практическое следствие: снимайте нужный кусок журнала сразу, пока инцидент свежий, — через неделю его может уже не быть.

На машинах, где агент устанавливался раньше (до текущей версии), каталог может по-прежнему называться WifiCertAgent.

Куда смотреть при разборе инцидента

  1. Возьмите request_id из заголовка ответа X-Request-ID (или из строки в клиентском/агентском логе, если запрос делал агент — он получает тот же id обратно).
  2. Найдите по нему все серверные строки — они образуют полную историю одного HTTP-запроса, включая финальную access-строку с итоговым статусом и латентностью.
  3. Если инцидент касается конкретного устройства — ищите по serial, он связывает строки поперёк множества запросов и заявок этого устройства.
  4. GET /api/v1/audit — не то же самое, что структурные логи: это отдельный, персистентный в БД журнал административных действий (approve/reject/revoke, изменение ролей и т.п.), а не поток HTTP-запросов. Подробнее об аудите — в Администрирование.

См. также Диагностику — указатель по типовым отказам: какой симптом в каком разделе разобран.