跳到主要内容
版本:0.6

使用 Meter 采集指标

Span 描述单次运行的形态;直方图描述整个集群的形态。 cubepi.tracing.MeterTracer 对称,发出 OTel GenAI 指标集,让 dashboard 开箱即用。

发出的指标

Instrument说明
gen_ai.client.operation.duration直方图(秒)——在 chatexecute_toolinvoke_agent 关闭时记录
gen_ai.client.operation.time_to_first_chunk直方图(秒)——当 chat 至少收到一个内容 chunk 时记录
gen_ai.client.token.usage直方图({token})——每次 chat 响应按 gen_ai.token.typeinputoutput)各记录一次

每个数据点携带操作、provider 和请求模型属性,因此失败/已取消的请求 (没有响应体或响应模型落地)仍可按请求内容分组:

  • gen_ai.operation.name —— chat / execute_tool / invoke_agent
  • gen_ai.provider.name —— anthropicopenaiopenai_responses……
  • gen_ai.request.model —— 例如 claude-sonnet-4-5-20250929
  • gen_ai.response.model —— 例如 claude-sonnet-4-5-20250929(仅成功时)
  • gen_ai.token.type —— inputoutput(仅 token 用量)

挂载 Meter

RAII 惯用形式——async with 全包,无需手动清理:

from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter,
)
from cubepi.tracing import Tracer, Meter
from cubepi.tracing.exporters import JsonlSpanExporter

async with (
Tracer(
service_name="my-bot",
agent_name="assistant",
exporters=[JsonlSpanExporter(directory="./cubepi-traces")],
) as tracer,
Meter(
resource=tracer.resource, # 共享 Resource,使 service.* 与 span 匹配
exporters=[
OTLPMetricExporter(endpoint="http://otel-collector:4318/v1/metrics"),
],
) as meter,
tracer.attached(agent),
meter.attached(agent),
):
await agent.prompt("...")
# 退出顺序自动:先 detach(关闭已取消运行的 span,flush trace pipeline)
# → 再 shutdown 两者(flush + 关闭 exporter)。

Meter.attach()Tracer.attach() 相互独立,可以单独使用任一个; 推荐同时使用并共享一个 Resource,让后端将它们视为同一服务。

如果需要非 RAII 的显式形式(例如在长时间运行的服务器生命周期内动态挂载 agent):

tracer_detach = tracer.attach(agent)
meter_detach = meter.attach(agent)
try:
...
finally:
tracer_detach() # 关闭已取消运行的 span
meter_detach() # 取消订阅 meter 的监听器
await tracer.shutdown()
await meter.shutdown()

Bucket 边界

duration 直方图采用 OTel GenAI semconv 推荐的边界值(单位:秒):

0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300

OTel 将这些值作为 _advisory 边界暴露;后端可以直接使用或覆盖。

单个 Meter 对应多个并发 agent

Tracer 一样,一个 Meter 实例可以安全地挂载到同一进程中的多个 agent。 每次 attach() 调用都有独立的内部 _MeterState,保存各自的 open-ns 时间戳和属性字典,来自两个 agent 的并发运行永远不会共享或相互覆盖指标状态。

meter = Meter(resource=tracer.resource, exporters=[exporter])
meter.attach(agent_a)
meter.attach(agent_b)

两个 agent 各自发出独立的 duration / token / TTFC 观测值,可按 gen_ai.agent.name(使用 Tracer(agent_name=…) 时在 Resource 层面设置) 或 gen_ai.request.model 过滤。

关闭

上面的 RAII 形式(async with … as tracer, … as meter, tracer.attached(agent), meter.attached(agent):)会自动处理关闭顺序:先 detach 内层 → 再执行外层 Tracer/Meter__aexit__,调用 shutdown()

对于手动模式,顺序很重要——tracer_detach() 必须在 tracer.shutdown() 之前运行,确保正在飞行的取消操作遗留的 span 在同一批次中被关闭和导出:

finally:
tracer_detach()
meter_detach()
await tracer.shutdown()
await meter.shutdown()

Meter.shutdown() 等待指标 reader 完成 flush,然后关闭它。 PeriodicExportingMetricReader 按固定间隔(默认 60 秒)导出——shutdown 是在进程退出前保证最后一个时间窗口数据落地的唯一方式。

查询示例(Honeycomb)

过去一小时内按 provider 划分的 p95 chat 延迟:

VISUALIZE P95(duration_s)
GROUP BY gen_ai.provider.name
WHERE gen_ai.operation.name = "chat"
TIME last 1 hour

按模型划分的 token 用量:

VISUALIZE SUM(token_count)
GROUP BY gen_ai.request.model, gen_ai.token.type
WHERE gen_ai.operation.name = "chat"

替换为你所用后端的查询 DSL——属性名和聚合方式相同。