Мониторинг шлюза
Мониторинг здоровья сервиса плюс структурированная эксплуатационная диагностика для демона шлюза Hermes, экспортируемые по OTLP/HTTP на настроенный оператором endpoint (OpenTelemetry Collector, DataDog или любой приёмник OTLP).
Эта плоскость по построению не содержит контента. Она экспортирует состояние жизненного цикла шлюза и cron, здоровье коннекторов платформ и диагностику предупреждений/ошибок без содержимого. Она никогда не экспортирует промпты, сообщения, аргументы и результаты инструментов, имена задач, адресатов, расписания, сырые ошибки, историю сессий, аналитику использования, журналы аудита или подробные трассы выполнения. Захват траекторий прогонов, моделей и инструментов — отдельная плоскость, которую обслуживают нативная интеграция Hermes с NeMo Relay SDK и явно настроенные подписчики или экспортёры Relay.
Что экспортируется
| Сигнал | Маршрут OTLP | Содержимое |
|---|---|---|
| Датчики (gauges) шлюза | /v1/metrics | hermes.gateway.up/state/busy/drainable/active_agents/background_work/background_delegations/restart_requested, hermes.platform.up/degraded с ограниченным набором атрибутов error_code |
| События здоровья/жизненного цикла | /v1/traces | Переходы состояний gateway.lifecycle (starting -> running -> draining -> stopped, startup_failed, выход), gateway.health_snapshot, изменения состояния платформ |
| Диагностика | /v1/logs | События шлюза уровня warning/error с постоянным телом и ограниченным набором атрибутов: подсистема, серьёзность, класс ошибки и код ошибки; отрендеренные сообщения логов не экспортируются никогда |
| Датчики планировщика cron | /v1/metrics | Heartbeat тикера и возраст последнего успеха (опускаются, если недоступны), монотонный счётчик наверстанных запусков из ветки планировщика для устаревшего окна, число включённых/выполняющихся задач и число просроченных, вычисленное из сохранённого next_run_at плюс существующее правило льготного окна планировщика |
| Жизненный цикл выполнения cron | /v1/traces | Устойчивые состояния claimed/running/completed/failed/unknown, ограниченные источник и класс ошибки, непрозрачный хешированный ключ задачи, прошедшее время, если есть метки времени, и результат доставки, когда планировщик его знает; терминальные состояния делают fail-open-попытку сброса буфера, которая может задержать завершение до одной секунды |
Сигналы несут service.name, версию, режим supervision и стабильный
односторонний хеш идентификатора установки, чтобы оператор мог различать
инстансы, не экспортируя идентичность аккаунта/профиля или сырой идентификатор
установки.
hermes.gateway.active_agents, hermes.gateway.background_work и
hermes.gateway.background_delegations дополняют друг друга. active_agents
считает ходы по сообщениям на переднем плане плюс выполняющиеся задачи cron плюс
прогоны через API — ту работу, которую шлюз дренирует при остановке.
background_work считает отсоединённую работу, которую active_agents не
включает никогда: фоновые субагенты delegate_task, процессы
terminal(background=true) и воркеры kanban; счёт идёт по задачам — пакет
fan-out из N субагентов считается как N, — так что метрика отражает реальную
одновременную нагрузку субагентами. background_delegations считает только
единицы асинхронного делегирования (каждый вызов delegate_task — одна,
пакет fan-out — тоже одна), в согласии с учётом ёмкости асинхронного пула;
сопоставляйте её в алертах с delegation.max_concurrent_children, чтобы видеть
давление на слоты. Сумма active_agents и background_work даёт всю живую
работу на инстанс; background_delegations показывает насыщение пула.
Включение
# config.yaml
monitoring:
gateway_health_export:
enabled: true
export:
otlp:
enabled: true
endpoint: http://collector-host:4318/v1/traces # маршруты metrics/logs выводятся из него
headers_env: {} # имя заголовка -> ИМЯ ПЕРЕМЕННОЙ ОКРУЖЕНИЯ (значения никогда не сохраняются)
Проверить текущее состояние можно в любой момент:
hermes monitoring status
OpenTelemetry SDK входит в extra otlp и устанавливается при первом использовании,
если это разрешает политика. Чтобы явно запросить его из подготовленного чекаута, выполните
python -c "import pm; pm.sync_venv(['otlp'], explicit=True)". Если SDK нет или endpoint
недоступен, шлюз работает как ни в чём не бывало: сбор метрик и обычный экспорт
событий не задевают горячий путь, а терминальные события cron делают одну
ограниченную fail-open-попытку сброса буфера длительностью до одной секунды,
чтобы финальное состояние потерялось с меньшей вероятностью.
Работает одинаково под supervision systemd/launchd/s6, в контейнерах, в tmux или
при простом hermes gateway run: экспортёр живёт в процессе шлюза, так что
никакой sidecar, агент или коллектор на хосте не нужен.
Сбор в DataDog
Запустите собственный OpenTelemetry Collector и пересылайте данные:
# конфигурация otel-collector
receivers:
otlp:
protocols:
http:
exporters:
datadog:
api:
key: ${env:DD_API_KEY}
service:
pipelines:
metrics: {receivers: [otlp], exporters: [datadog]}
traces: {receivers: [otlp], exporters: [datadog]}
logs: {receivers: [otlp], exporters: [datadog]}
Направьте monitoring.export.otlp.endpoint на коллектор. Алерты вешаются на
hermes.gateway.up, hermes.platform.up и hermes.platform.degraded.
Общие запросы и алерты по парку инстансов
Точный синтаксис зависит от бэкенда наблюдаемости, который использует заказчик. Примеры ниже написаны в стиле PromQL и намеренно обходят стороной вендор-специфичную маршрутизацию, адресатов и инвентарь заказчика.
Группируйте представления по парку по непрозрачному атрибуту ресурса
service.instance.id. Умерший процесс не может сам отправить свой ноль, так
что каждому развёртыванию нужны оба вида детекции: по явному состоянию и по
пропавшему ряду.
# Явный отказ шлюза.
hermes_gateway_up == 0
# Машина исчезла или перестала экспортировать. Выберите окно длиннее,
# чем настроенный интервал экспорта плюс запас на повторы коллектора.
absent_over_time(hermes_gateway_up[5m])
# Локально принадлежащий мост явно лежит.
hermes_platform_up == 0
# Поток планировщика протух, хотя шлюз, возможно, ещё жив.
hermes_cron_scheduler_heartbeat_age_seconds > 180
# Тикер крутится, но давно не завершал успешный тик.
hermes_cron_scheduler_last_success_age_seconds > 300
# Одна или несколько задач вышли за своё льготное окно планировщика.
hermes_cron_jobs_overdue > 0
# Счётчик наверстывания вырос: значит, как минимум один устаревший запуск
# был схлопнут и выполнен один раз с задержкой.
increase(hermes_cron_scheduler_catch_up_occurrences[15m]) > 0
Записи жизненного цикла выполнения cron приходят как спаны
hermes.cron_execution. Алерты и производные события стройте на ограниченных
атрибутах вроде:
hermes.status = failed|unknown
hermes.delivery_outcome = failed|not_configured
hermes.error_class = auth_failed|rate_limited|timeout|network_error|
dispatch_failed|interrupted|empty_response|
invalid_config|unknown
Рекомендуемые представления для оператора:
- по строке на каждый
service.instance.idс состоянием шлюза и настроенной локальной платформы; - heartbeat планировщика, возраст последнего успеха, число выполняющихся, число просроченных и рост счётчика наверстывания;
- лента жизненного цикла cron, ключом в которой служит только непрозрачный
hermes.job_key; - отдельные алерты на пропажу машины, падение локального моста, протухший
планировщик, cron в
failed/unknown, сбой доставки и активность по просрочке/наверстыванию.
Пороги алертов и маршрутизацию держите в конфигурации, принадлежащей развёртыванию. Не добавляйте имена задач, промпты, результаты, расписания, адресатов, сырые ошибки, имена профилей или идентичность аккаунта лишь ради того, чтобы дашборд было проще читать.
Сценарии приёмочной проверки релиза
Прежде чем принимать развёртывание, принудительно воспроизведите и проверьте все пять случаев через настоящий коллектор и бэкенд:
- Успех cron: наблюдайте
claimed -> running -> completed, длительность и правдивый результат доставки. - Сбой cron: наблюдайте
failedплюс ограниченный класс ошибки — без сырого исключения или контента в декодированной OTLP-нагрузке. - Прерывание cron: остановите владеющий шлюз во время выполнения,
перезапустите его и убедитесь, что состояние восстановилось в
unknown. - Падение локально принадлежащего моста: сломайте один нативный коннектор, наблюдайте его ограниченное состояние down/retrying/fatal и восстановление, а также убедитесь, что незатронутые машины остаются здоровыми.
- Убитый шлюз: завершите одну канарейку, проверьте детекцию пропавшего ряда, перезапустите её и убедитесь, что вернулась та же непрозрачная идентичность инстанса.
Здоровье транспорта Relay, принадлежащего Hermes Agent, остаётся в пределах этой плоскости. Отдельный сервис шлюза или коннектора остаётся авторитетным для любого общего состояния подключённых платформ, которым он владеет, и должен экспортировать это состояние по собственному пути телеметрии.
Для каждого сценария убедитесь, что сигнал и алерт снимаются при восстановлении, другие машины не затронуты, отказ коллектора остаётся fail-open, а декодированные метрики, спаны, логи и атрибуты ресурса не содержат контента.
Локальный smoke-тест (без Docker)
# терминал 1: коллектор-перехватчик на :4318
python scripts/observability/otel_capture_collector.py \
--host 127.0.0.1 --port 4318 --log ~/.hermes/cache/scratch/hermes_otel_capture.jsonl
# терминал 2: прогнать настоящий экспортёр через переходы жизненного цикла,
# фатальную платформу и структурированное событие-предупреждение, затем сбросить буфер
python scripts/observability/gateway_health_export_probe.py \
--endpoint http://127.0.0.1:4318/v1/traces \
--log ~/.hermes/cache/scratch/hermes_otel_capture.jsonl --wait 8
# код 0 печатает: {"requests": 6, "paths": ["/v1/logs", "/v1/metrics", "/v1/traces"]}
Сопровождение и расширение плоскости
Эта плоскость по замыслу — фиксированный, перечислимый словарь без контента. Добавить сигнал — это не просто «отправить новую метрику»: каждое новое имя и каждый атрибут должны быть объявлены в каждом слое, который обеспечивает ограниченность словаря, иначе ниже по потоку они молча отбрасываются. Следуйте чек-листу для того изменения, которое вносите. Золотое правило: новый сигнал, который отправляется, но не объявлен в каждом слое, выглядит как баг в коде, а на деле это баг регистрации словаря — ничего не падает, сигнал просто никогда не приходит.
Инвариант отсутствия контента (действует для любого изменения)
Прежде чем что-то добавлять, убедитесь, что оно не может нести контент. Числа,
булевы значения, возрасты, длительности, монотонные счётчики и односторонние
хеши безопасны. Никогда не добавляйте атрибут, который способен вместить имя
задачи, промпт, результат, расписание, адресата, сырой текст исключения, путь к
файлу, имя профиля, id аккаунта или строку свободной формы. Если запись нужно
привязать к задаче/сущности, хешируйте ключ (sha256(...)[:24], см. _job_key
в agent/monitoring/cron_health.py) — никогда не отправляйте сырой id. Все
строковые атрибуты, которые могут соприкасаться с пользовательским вводом,
обязаны проходить через redaction.redact_for_export и усекаться (см.
_span_attrs в agent/monitoring/otlp_exporter.py).
Добавление нового датчика/метрики
- Отправляйте её из сборщика снимка (
agent/monitoring/gateway_health.pybuild_gateway_health_snapshot,cron_health.pybuild_cron_health_snapshotили соседнего reader’а, подключённого к_read_runtime_snapshotвgateway_health_export.py). Best-effort: reader никогда не должен выбрасывать исключение в цикл сбора — оборачивайте его и логируйте WARNING без контента, только с именем ТИПА исключения (так делают reader’ы cron и фоновой работы), чтобы будущая регрессия была заметна, а сигнал не пропадал молча. - Зарегистрируйте имя метрики с точками в списке
metric_namesнаблюдаемых датчиков вgateway_health_export.py::_start_metric_provider. Датчик, который отправляется в снимке, но здесь не зарегистрирован, не наблюдается никогда. - Добавьте строку в таблицу экспорта и пример алерта в этом файле.
- Если перед экспортёром в развёртывании стоит OpenTelemetry Collector со
списком разрешённых имён метрик (процессор
filter/...с проверкамиname != "..."), добавьте новое имя и туда — иначе коллектор отбросит его до бэкенда. Это не код репозитория, но это самая частая причина, по которой корректно отправляемая новая метрика так и не появляется; отметьте это в PR, чтобы оператор развёртывания обновил конфигурацию коллектора.
Добавление новой подсистемы (нового семейства сигналов)
Повторите паттерн cron (cron_health.py плюс его подключение): вынесите логику
чтения/проекции в отдельный модуль, откройте одну функцию
build_<subsystem>_health_snapshot(), возвращающую ограниченные
GatewayMetric (и события, если они есть), и подключите её в
_read_runtime_snapshot с тем же best-effort-ограждением try/except-WARNING.
Затем пройдите чек-лист «добавление метрики» для каждого нового имени и
чек-лист «добавление атрибута» для каждого нового атрибута события. Добавьте
ниже сценарий приёмочной проверки релиза для режима отказа этой подсистемы.
Расширение словарей классов ошибок / статусов / источников / состояний
Это закрытые перечисления, которые держат плоскость ограниченной. Расширяйте МНОЖЕСТВО, затем классификатор — никогда одно без другого:
- Cron (
agent/monitoring/cron_health.py):_KNOWN_STATUSES,_KNOWN_SOURCES,_KNOWN_DELIVERY_OUTCOMESи корзины ключевых слов вclassify_cron_error. Всё, чего нет во множестве, на выходе приводится кunknown, так что новое значение, не добавленное во множество, невидимо. - Шлюз/платформа (
agent/monitoring/gateway_health.py):_KNOWN_GATEWAY_STATES,_KNOWN_PLATFORM_STATESиclassify_gateway_error.
Правила: держите словарь МАЛЕНЬКИМ и осмысленным с точки зрения эксплуатации
(класс ошибки должен соответствовать действию оператора, а не подклассу
исключения); новая корзина должна срабатывать на стабильное ключевое слово, а
не на текст сообщения, который может меняться; обновите список
hermes.error_class = ... в разделе про алерты этого файла и юнит-тест
перечисления, чтобы контракт проверялся утверждением, а не замораживался как
число элементов.
Добавление атрибута без контента к существующему событию/спану
Добавьте ключ в список разрешений keep_by_kind для соответствующего вида в
agent/monitoring/otlp_exporter.py::_span_attrs (неперечисленные ключи
отбрасываются), пропустите его через редактирование, если он хоть когда-нибудь
бывает строкой, и — как и с метриками — если у коллектора развёртывания есть
список разрешённых атрибутов спана keep_keys(...), добавьте атрибут и туда,
иначе он будет вырезан в пути.
Проверяйте всю цепочку, а не только отправку
Отправить — необходимо, но недостаточно. Убедитесь, что сигнал доживает до
бэкенда, потому что перечисления, регистрация в metric_names, список
разрешённых атрибутов эмиттера и любой список разрешений коллектора отбрасывают
неперечисленные значения без единой ошибки:
hermes monitoring status # текущее состояние
python scripts/observability/gateway_health_export_probe.py \
--endpoint http://127.0.0.1:4318/v1/traces \
--log ~/.hermes/cache/scratch/cap.jsonl --wait 8 # прогнать настоящий экспортёр
Декодируйте перехваченную OTLP-нагрузку и убедитесь, что новое имя/атрибут присутствует И что контент не утёк. Если впереди стоит настоящий коллектор, добавьте записи в его списки разрешений и перепроверьте уже по бэкенду, а не только по локальному перехвату.
Границы и планы
CLI hermes monitoring намеренно даёт только status. Этот первый релиз
покрывает лишь сигналы здоровья сервиса и эксплуатационной диагностики,
принадлежащие Hermes Agent, включая здоровье транспорта Relay, принадлежащего
Hermes Agent. Авторитетное общее состояние коннекторов/платформ Team Gateway
явно вне рамок, как и продуктовая аналитика, отчётность по аудиту/качеству и
подробные трассы выполнения. Общие метрики использования клиентов и
корпоративная трассировочная телеметрия проектируются поверх интеграции с NeMo
Relay со своими границами согласия, политик и экспорта; эта плоскость
мониторинга остаётся узкой, чтобы оператор мог включить её, не задев ни одного
сигнала с контентом. По мере того как это будет реализовано, поверхность
телеметрии могут реорганизовать.