可观测面板

每个 cache / 压缩 / 延迟问题现在在哪回答——admin 任务检查器(DB)、Loki info 级事件、Tempo 里的 OTel trace——各自的查询与判读标准

面板的设计原则

不是“能看就加”,而是每个面板回答一个具体问题。问题越狭窄,出现异常时行动路径越短。

可观测拆成三个面。先读下一节:它决定你的问题该由哪个面回答,再去写查询。

三个面——各回答什么

适合数据源对应下文小节
Admin 任务检查器per-task 深挖:本任务的 cache 读写 token、压缩次数 + 省量、每步预测 vs 实测DB stepUsages[](每步落库)Cache & 压缩——per-task
Loki / LogQL跨部署聚合,基于 info 级事件:生命周期、压缩触发率、预算线pino → Loki(仅 info+)Loki 面板
OTel trace(Tempo)延迟 + 吞吐 + 每工具耗时——作为 span 属性,用 TraceQL 查@ai-sdk/otel → Tempo延迟/吞吐面板

贯穿这一切的约束:pino 只把 info+ 上报 Lokiapps/server/src/lib/logger.ts 把 debug 丢掉以保护免费档配额)。两个最与 cache 相关的事件——stream.step_usage(per-step usage)和 cache.breakpoints_placed——都是 debug 级,所以生产环境不进 Loki。它们的 per-task 细节落在 admin 检查器里。要在 Loki 里对它们上面板,先把它们提到 info 并接受量。

Cache & 压缩——per-task(admin 检查器)

per-task 的 cache 和压缩细节落在 admin 任务检查器里,数据来自 DB stepUsages[] 快照(agent-loop.tscreateOnStepEnd),每步落库 cacheReadTokens / cacheCreationTokens / predictedTokens / compactedTokens / durationMs。点开一个任务看:

  • 这个任务的 prompt cache 在省钱吗? —— 每步 cacheReadTokens vs cacheCreationTokens。跨任务的聚合读/写比是下面那个 TraceQL 面板。
  • 压缩在触发吗,省了多少? —— 每步 compactedTokens 和预测 vs 实测 input。
  • cache 断点落对了吗? —— cache.breakpoints_placed payload { messagesCount, stepIndex, placedAt, lastRole }model.tsmarkPrefixCacheBoundary)。placedAtcomputeInnerAnchors + 尾块标记的消息下标集合。在检查器的运行时上下文 tab 看。

Loki 面板——今天就能跑的 info 级事件

这些读的是确实上报 Loki(info+)的事件,所以生产环境直接能跑。

面板:压缩触发率 + 省量

问题——“in-loop 压缩多久触发一次,省了多少?”

sum(rate({job="zapvol-server", event="compaction.step_fired"}[5m]))

省量趋势:

avg_over_time(
  {job="zapvol-server", event="compaction.step_fired"} | json | unwrap savedTokens [10m]
)

compaction.step_fired{ taskId, step, savedTokens }agent-loop.ts),仅在某步触发 reduce(savedTokens > 0)时 emit——所以这个 rate 是真实触发频率,不是每步 replay 的持续套用。

面板:预算线 vs 上下文窗口

问题——“实测 overhead(instructions + tools)离模型上下文窗口还有多远?”

{job="zapvol-server", event="compaction.budget_measured"}
  | json
  | line_format "instr={{.instructionsTokens}} tools={{.toolsTokens}} window={{.contextWindow}}"

compaction.budget_measured{ taskId, instructionsTokens, toolsTokens, contextWindow }agent-loop.ts)。

面板:生命周期 & prefix 尺寸

task.created / task.completedroutes/tasks.ts)看生命周期 rate;stream.messages_prepared { inputMessages, modelMessages }agent-loop.ts)看一轮起手的 prefix 尺寸;agent.created 看装配。全是 info+,都能安全上面板。

延迟/吞吐面板(TraceQL)

上面这些都在用日志或 DB 检查器回答 cache / 压缩问题——全都不带时延stream.step_usage 有 token 数,但日志行里没有 TTFO、没有 tokens/s、没有每步耗时。这条轴活在 OTel span 里。

@ai-sdk/otel 产出的是 span、不是 metrics——它不注册 meter(见 可观测栈总览 → 分布式追踪)。所以时延作为 span 属性活在 Tempo 里,下面的面板是 TraceQL,挂在 Tempo data source 上——不是 PromQL、不是 Loki。Tempo 的 TraceQL-metrics 函数(ratequantile_over_timesum_over_time)直接从 span 现算聚合,不需要单独的 metrics 管道。

前提:启动时 registerTelemetry(new OpenTelemetry())。注册后 span 默认就 emit,没有 per-call 开关——SDK 7 去掉了 experimental_telemetry: { isEnabled }。没注册之前,Tempo data source 是空的,这些面板读空。

面板:首字延迟(TTFO)

问题——“用户等多久才看到第一个 token 流出?” 体感延迟指标;这里劣化,即使总吞吐正常用户也能感觉到。TTFO 挂在 chat span 上:

{ name = "chat" } | quantile_over_time(span.gen_ai.client.operation.time_to_first_chunk, .95) by (resource.service.name)

正常值域跟模型强相关(看 p50/p95 的分布,不看绝对值)。告警看台阶式跳变(对齐某次发布或 provider 事故),或 p95 比 p50 偏离超过 ~3×(排队 / 限流退避)。

面板:每步耗时

问题——“一步端到端在变慢吗?” chat span 自身的 duration:

{ name = "chat" } | quantile_over_time(duration, .95) by (resource.service.name)

这里升高但 TTFO 稳定,说明是流中途生成变慢,而不是排队;和 onLanguageModelCallEnd 里的 output tokens per second 交叉核对。

面板:每工具执行耗时

问题——“一步里哪个 tool 是长尾?” execute_tool span 按 tool 名拆开,直接暴露慢工具——这件事日志现在逼你按时间戳重排才能推出来:

{ name =~ "execute_tool.*" } | quantile_over_time(duration, .95) by (span.gen_ai.tool.name)

再下钻:从慢任务的 trace_id 打开 invoke_agent 根 span,在一条时间线上看它的 execute_tool / chat 子节点——这正是日志难以廉价回答的“step N 卡在哪”。

面板:Cache 经济性(读/写 token 比)

问题——“cache 写入的内容被复用了吗,还是空耗?” chat span 带 gen_ai.usage.cache_read.input_tokenscache_creation.input_tokens,各自作为一条 TraceQL 查询聚合:

{ name = "chat" } | sum_over_time(span.gen_ai.usage.cache_read.input_tokens)
{ name = "chat" } | sum_over_time(span.gen_ai.usage.cache_creation.input_tokens)

再在 Grafana 的 math 表达式里把两者相除。R > 3 健康(每次写被读 3+ 次);R < 1 告警(写多于读——cache 策略失效)。要 per-task 版的这个比值,admin 检查器的 stepUsages[] 才是权威——TraceQL 形态是跨部署聚合。

扩展新面板的工作流

Step 0:先选面

写任何查询之前,先判断这个问题属于三个面里的哪一个:

  • operator 会点进去看的 per-task 细节 → admin 检查器(DB)。别为它加 Loki 事件。
  • 跨部署聚合 / 趋势 / 告警 → Loki(必须是 info+ 事件)或 OTel trace(TraceQL)。
  • 延迟 / 吞吐 → 只走 OTel trace(TraceQL);日志不带时延。

新 Loki 事件必须打在 info 或更高级别——log.debug(...) 在进 Loki 前就被丢了(见上面的上报约束)。量大的 per-step 数据有意留在 debug,该落 DB 检查器或 OTel trace,不是 Loki。

Step 1:先加 event,再加面板

绝不要把 Grafana 当代码写。 面板是数据的视图,不是业务逻辑的载体。先在 Zapvol 代码里 emit 一个结构化 event:

log.info("your_event.name", {
  taskId,
  /* 低基数字段 */ kind: "...",
  /* 数值字段 */ someMetric: 42,
});

运行几次,在 Grafana Explore 里用 {event="your_event.name"} | json 确认能查到、字段对不对。

Step 2:定义面板四要素

在 Dashboard 创建之前,在文档里写下来(可以先在 PR 描述里):

  • 问题:这个面板回答什么
  • 查询:LogQL / TraceQL 原型
  • 正常值域:不加入我的判读,别人也能看懂“这个数字该是多少”
  • 异常信号:至少 2 条可操作的排查路径

写不出来说明问题没想清楚,不要开始画面板。

Step 3:Dashboard JSON 提交到 repo

Grafana 的 Dashboard → Settings → JSON Model 复制出来,放到:

ops/grafana/dashboards/{domain}-{name}.json

在同目录的 README.md 里追加导入说明和判读要点(或链接到本章对应小节)。

为什么要 commit JSON:Grafana 单实例丢了数据就没了。commit 到 repo 后,新人 15 分钟能在本地拉起等效 Grafana。也是 code review 的好载体——别人改了一个查询你能在 PR 里看到 diff。

Step 4:如果需要告警

用 Grafana 自带的 Alert rules(声明式 YAML,不要用点点点的 UI)。声明式规则也 commit 到 repo:

ops/grafana/alerts/cache-hit-degradation.yaml

告警规则要极度克制。Agent 系统里“可能出问题”的事很多,但“必须立刻处理”的事很少。默认阈值:

  • for: 15m 以上——短暂波动不告警
  • 只告结果性指标(cache hit ratio、error rate、latency 尾部),不告过程性指标(断点数、压缩频率)

相关章节

这页有帮助吗?