Gateway 监控

面向 Hermes gateway 守护进程的服务健康监控加结构化运维诊断,经 OTLP/HTTP 导出到运维配置的端点(OpenTelemetry Collector、DataDog,或任意 OTLP receiver)。

该面按设计是无内容的。它导出 gateway 与 cron 生命周期状态、平台连接器健康,以及无内容的告警/错误诊断。它绝不导出提示词、消息、工具参数或结果、任务名、目标、调度、原始错误、会话历史、用量分析、审计日志或详细执行轨迹。运行/模型/工具轨迹捕获是另一个面,由 Hermes 原生 NeMo Relay SDK 集成以及显式配置的 Relay 订阅者或 exporter 提供。

导出什么

信号OTLP 路由内容
Gateway 仪表/v1/metricshermes.gateway.up/state/busy/drainable/active_agents/background_work/background_delegations/restart_requested、hermes.platform.up/degraded,带有界 error_code 属性
健康/生命周期事件/v1/tracesgateway.lifecycle 状态迁移(starting -> running -> draining -> stopped、startup_failed、退出)、gateway.health_snapshot、平台状态变更
诊断/v1/logs告警/级 gateway 事件,带恒定正文与有界的子系统、严重级、错误类、错误码属性;渲染后的日志消息绝不导出
Cron 调度器仪表/v1/metricsticker 心跳与最近成功年龄(不可用时省略)、来自调度器陈旧窗口分支的单调追赶发生次数、启用/运行中的任务数,以及由持久化 next_run_at 加调度器既有宽限规则推得的逾期数
Cron 执行生命周期/v1/traces持久的 claimed/running/completed/failed/unknown 状态、有界 source 与错误类、不透明哈希任务键、有时间戳时的耗时、以及调度器知道时的投递结果;终态会做一次 fail-open flush 尝试,最多延迟完成一秒

信号携带 service.name、版本、监管模式,以及安装 id 的稳定单向哈希,因此运维无需导出账号/profile 身份或原始安装标识符,即可区分实例。

hermes.gateway.active_agents、hermes.gateway.background_work 与 hermes.gateway.background_delegations 互补。active_agents 统计前台消息轮次加在途 cron 任务加 API 运行——即 gateway 在关闭时排空的工作。background_work 统计 active_agents 永不包含的分离工作:后台化的 delegate_task 子智能体、terminal(background=true) 进程、看板 worker;它是任务粒度的——一次 N 个子智能体的扇出计 N——因此反映真实的并发子智能体负载。background_delegations 只统计异步委派单元(每次 delegate_task 派发算一个,一次扇出批次算一个),对应异步池的容量核算;把它对 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: {}   # 头名 -> ENV 变量名(值绝不存储)

随时查看姿态:

hermes monitoring status

OpenTelemetry SDK 属于 otlp extra,在策略允许时首次使用即安装。要在准备好的 checkout 中显式安装,运行 python -c "import pm; pm.sync_venv(['otlp'], explicit=True)"。当 SDK 缺失或端点宕机时,gateway 不受影响地运行:指标采集与普通事件导出保持在热路径之外,而终态 cron 事件做一次最多一秒的有界 fail-open flush 尝试,使最终状态更不易丢失。

在 systemd/launchd/s6 监管、容器、tmux 或裸 hermes gateway run 下行为一致:exporter 住在 gateway 进程内,因此宿主机上不需要 sidecar、agent 或 collector。

采集到 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 指向 collector。告警落在 hermes.gateway.up、hermes.platform.up、hermes.platform.degraded 上。

通用集群查询与告警

确切语法取决于客户的可观测后端。下面的示例用 PromQL 风格表达式,刻意避开厂商特定的路由、目标或客户清单。

按不透明的 service.instance.id 资源属性分组集群视图。一个已死的进程无法发出自己的零值,因此每个部署都需要显式状态检测与缺失序列检测二者。

# 显式 gateway 故障。
hermes_gateway_up == 0

# 机器消失或停止导出。选一个长于
# 配置导出间隔与 collector 重试余量的窗口。
absent_over_time(hermes_gateway_up[5m])

# 自有的本地桥显式宕机。
hermes_platform_up == 0

# 调度器线程陈旧,即使 gateway 可能仍活着。
hermes_cron_scheduler_heartbeat_age_seconds > 180

# ticker 在循环但近期未完成一次成功 tick。
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 span 到达。基于有界属性告警或派生事件,例如:

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

推荐的运维视图:

  1. 每个 service.instance.id 一行,带 gateway 与已配置本地平台状态;
  2. 调度器心跳、最近成功年龄、运行数、逾期数、追赶增量;
  3. 仅按键为不透明 hermes.job_key 的 cron 生命周期流;
  4. 对机器缺失、本地桥宕机、调度器陈旧、cron failed/unknown、投递失败、逾期/追赶活动分别告警。

把告警阈值与路由留在部署自有的配置中。不要仅仅为了让仪表盘易读就加入任务名、提示词、输出、调度、目标、原始错误、profile 名或账号身份。

发布验证场景

接受部署之前,强制并验证全部五种案例都经真实 collector 与后端:

  1. Cron 成功: 观察 claimed -> running -> completed、耗时,以及真实的投递结果。
  2. Cron 失败: 观察 failed 加一个有界错误类,解码后的 OTLP payload 中无原始异常或内容。
  3. Cron 中断: 执行期间停掉所属 gateway,重启,并观察恢复到 unknown。
  4. 自有的本地桥中断: 打断一个原生连接器,观察其有界 down/retrying/fatal 状态与恢复,并确认未受影响的机器保持健康。
  5. 被杀的 gateway: 终止一个 canary,验证缺失序列检测,重启它,并确认同一不透明实例身份返回。

Hermes Agent 自有的 Relay 传输健康仍在范围内。由独立 gateway 或连接器服务拥有的共享连接平台状态,仍以其为准,并应经它自己的遥测路径导出。

对每个场景,验证信号与告警在恢复时清除、其他机器不受影响、collector 失败保持 fail-open,且解码后的指标、span、日志与资源属性保持无内容。

本地冒烟测试(无 Docker)

# 终端 1:在 :4318 上起捕获 collector
python scripts/observability/otel_capture_collector.py \
  --host 127.0.0.1 --port 4318 --log ~/.hermes/cache/scratch/hermes_otel_capture.jsonl

# 终端 2:驱动真实 exporter 走过生命周期迁移、
# 一个致命平台与一条结构化告警事件,然后 flush
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
# exit 0 打印:{"requests": 6, "paths": ["/v1/logs", "/v1/metrics", "/v1/traces"]}

维护与扩展该面

该面按设计是一份固定、枚举、无内容的词表。加一个信号不只是"发一个新指标"——每个新名字与属性都必须在执行有界词表的每一层声明,否则会在下游被静默丢弃。按你正在做的改动走检查清单。黄金法则:一个被发出但未在每一层声明的新信号,看起来像代码 bug,实则是词表注册 bug——没有任何报错,信号就是到不了。

无内容不变量(适用于每次改动)

在加任何东西之前,确认它不携带内容。数字、布尔、年龄、耗时、单调计数、单向哈希是安全的。绝不加一个能装任务名、提示词、输出、调度、目标、原始异常文本、文件路径、profile 名、账号 id 或自由字符串的属性。当你必须把记录键到某个任务/实体时,哈希它(sha256(...)[:24],见 agent/monitoring/cron_health.py 中的 _job_key)——绝不发原始 id。所有可能触达用户输入的字符串属性都必须经 redaction.redact_for_export 并截断(见 agent/monitoring/otlp_exporter.py 中的 _span_attrs)。

加一个新仪表/指标

  1. 在快照构建器中发出它(agent/monitoring/gateway_health.py 的 build_gateway_health_snapshot、cron_health.py 的 build_cron_health_snapshot,或接进 gateway_health_export.py 中 _read_runtime_snapshot 的兄弟读取器)。尽力而为:绝不让读取器抛进采集循环——包起来并记一条无内容、只带异常类型名的 WARNING(cron 与后台工作读取器用的模式),让未来的回归可见,而不是静默丢信号。
  2. 在 gateway_health_export.py::_start_metric_provider 的可观测仪表 metric_names 列表中注册该点分指标名。在快照中发出但未在此注册的仪表永不会被观测到。
  3. 在本文件的导出表中加一行,并加一个告警示例。
  4. 若部署在 exporter 前用 OpenTelemetry Collector 且其指标名白名单(带 name != "..." 守卫的 filter/... 处理器),也在那里加新名——否则 collector 在到达后端前丢掉它。这不是仓库代码,却是正确发出的新指标始终不出现的最常见原因;在 PR 中点明,好让部署运维更新其 collector 配置。

加一个新子系统(一族新信号)

镜像 cron 模式(cron_health.py + 其接线):把读取/投影逻辑放进自己的模块,暴露一个返回有界 GatewayMetric(以及事件,若有)的 build_<subsystem>_health_snapshot(),并以同样的尽力 try/except-WARNING 守卫把它扩进 _read_runtime_snapshot。然后对每个新名走"加指标"清单,对每个新事件属性走"加属性"清单。在下面为该子系统的失败模式加一个发布验证场景。

扩展错误类 / 状态 / source / 状态词表

这些是保持该面有界的封闭枚举。先扩展 SET,再扩展分类器,二者不可缺一:

  • Cron(agent/monitoring/cron_health.py):_KNOWN_STATUSES、_KNOWN_SOURCES、_KNOWN_DELIVERY_OUTCOMES,以及 classify_cron_error 的关键字桶。集合之外的一切在出口处被强制为 unknown,因此未加入集合的新值不可见。
  • Gateway/平台(agent/monitoring/gateway_health.py):_KNOWN_GATEWAY_STATES、_KNOWN_PLATFORM_STATES、classify_gateway_error。

规则:保持词表小且对运维有意义(一个错误类应映射到一个运维动作,而非一个异常子类);新桶必须按稳定关键字匹配,而不是按可能变化的消息文本;更新本文件告警段中的 hermes.error_class = ... 列表与该枚举的单测,让契约被断言,而不是冻结成一个计数。

给既有事件/span 加一个无内容属性

把键加入 agent/monitoring/otlp_exporter.py::_span_attrs 中 emitter 按 kind 的 keep_by_kind 白名单(未列出的键被丢弃),若它是字符串形状则过一遍脱敏;并且——和指标一样——若部署的 collector 有 span 属性 keep_keys(...) 白名单,也在那里加该属性,否则在传输中被剥掉。

验证整条链,而不只是发出

发出是必要但不充分的。确认信号一路存活到后端,因为枚举、metric_names 注册、emitter 属性白名单以及任何 collector 白名单都会各自丢弃未列出的值且无报错:

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          # 驱动真实 exporter

解码捕获的 OTLP payload,断言新名/属性存在,且无内容泄漏。当真实 collector 在前面时,加上它的白名单条目并对照后端重新验证,而不只是本地捕获。

边界与路线图

hermes monitoring CLI 刻意只暴露 status。首个版本只覆盖 Hermes Agent 自有的服务健康与运维诊断信号,包括 Hermes Agent 自有的 Relay 传输健康。Team Gateway 拥有的权威共享连接器/平台状态明确不在范围内,产品分析、审计/质量报告与详细执行轨迹亦然。共享客户端用量指标与企业 trace 遥测正在 NeMo Relay 集成上设计,带它们自己的同意、策略与导出边界;本监控面保持窄,以便运维无需触碰任何带内容的信号即可启用它。随着那部分落地,遥测面可能重新组织。