flow 表本来就对得上
TMA1 的 tma1_token_usage_1m、cost_1m、latency_1m、status_1m 这几张 flow 表都是从 span_attributes.gen_ai.* 推导出来的,而这个插件按约定就会填这些字段。除此之外不用配任何东西。
DSH OTel
不需要 collector,不需要 sidecar,不用 fork DSH。它就是一个普通的 DeepSeek Harness 插件,装上之后每个 turn、每次模型调用、每次工具执行都变成 GreptimeDB 里一行可查的数据。
安装
dsh plugin --profile headless add @tma1-ai/dsh-plugin-greptimedb
包里自带 bundle patch,所以这一条命令就把它接进了 profile。dsh plugin 会转发给 PATH 上的 pnpm,而 dsh 的 profile 目录本身就是一个 pnpm workspace 根目录——pnpm 9 会拒绝在那里安装,并且忽略 dsh 写入的 linker 设置,所以请用 pnpm 10 及以上。默认配置已经指向本地的 GreptimeDB。
chat span 和 tool span 共享同一个 trace 和同一个 dsh.step,所以关联它们就是一次普通的 SQL join。每个时间戳都取自它对应的那条 session 事件,而不是处理事件时读的时钟。
traces、metrics、logs。signals 接受任意子集——关掉的信号根本不会构建 exporter。
默认的 content: none 只导出结构和计数。没有 prompt,没有消息内容,没有工具参数,没有工具返回。
配置有问题时插件加载阶段直接失败,并指出是哪个字段——而不是等到第一次导出才悄悄出错。
三种信号
traces 看结构,metrics 用于长期留存和不受采样影响的分位数,logs 保留原始 session 事件。
turn span 是根节点。chat span 和 tool span 作为兄弟节点挂在它下面,通过 dsh.step 关联。
四条路径关闭一个 chat span,包括崩溃这种情况。没有一条会让 span 悬在一个随意的时间点上。
| 情况 | 结束时间与状态 |
|---|---|
| 模型正常返回 | assistant/message · OK |
| 流被打断 | assistant/message · OK,附带 dsh.response.interrupted |
| 请求失败 | 该 step 的 step/end · ERROR,带错误类型 |
| 没有结束事件(崩溃、进程退出) | 最后见到的事件 · UNSET,附带 dsh.span.unclosed |
DSH 的计数是不相交的:inputTokens 只算未命中缓存的输入,缓存读和缓存写是独立字段。gen_ai.usage.input_tokens 是计费总量,所以插件把它们加起来。输出侧包含 reasoning token。
仪表盘放在 grafana/,配套一个把 GreptimeDB 和 Grafana 一起拉起来的 compose stack。每个面板的查询都由 node grafana/verify.mjs 对着真实数据库校验过。

每张表都能往下跳:trace id 打开那个 turn 的 waterfall,session id 在 trace 视图和日志视图之间切换。

四个属性通过 X-Greptime-Log-Extract-Keys 变成真正的列。assistant/chunk 永远不导出——拼装好的 assistant/message 已经带着同样的内容。
Metrics
和 traces 同一批活动,换成 PromQL 看——用于更长的留存,以及不受采样影响的分位数。
| Instrument | 类型 | 维度 |
|---|---|---|
gen_ai.client.token.usage | Histogram | gen_ai.token.type(只有 input/output)、model、provider |
gen_ai.client.operation.duration | Histogram | gen_ai.operation.name、model、provider |
gen_ai.invoke_agent.duration | Histogram | gen_ai.operation.name |
gen_ai.execute_tool.duration | Histogram | gen_ai.operation.name、gen_ai.tool.name |
dsh.token.detail | Histogram | dsh.token.detail_kind(cache_read/cache_write/reasoning) |
dsh.tool.invocations | Counter | gen_ai.tool.name、dsh.tool.outcome |
dsh.turns / dsh.steps | Counter | — |
配置
profile patch 会整个替换掉这一行的 config,而不是合并进去,所以想保留的字段都要重新写一遍。
| 键 | 默认值 | 说明 |
|---|---|---|
endpoint | 必填 | OTLP 基础 URL。插件会自己拼上每种信号的 /v1/{traces,metrics,logs} 后缀;写成单信号路径会在加载时被拒绝。 |
database | public | 作为 X-Greptime-DB-Name 发送。 |
username / password | 无 | Basic auth。要么都填,要么都不填。 |
signals | 三种全开 | traces、metrics、logs 的任意子集。 |
content | none | 允许多少 payload 离开进程。 |
ttl | 180d | 插件创建的日志表和 trace 表的留存时间,通过 x-greptime-hints 发送,也接受 forever。已存在的表要 ALTER TABLE 才会变。 |
批量、超时、service name、目标表名都有合理默认值;完整表格见 README。
哪些数据会离开这台机器
content 决定默认不导出任何 payload。要放开就按 profile 显式放开。
| 模式 | 导出内容 |
|---|---|
none (默认) | 结构和计数:事件类型、turn 与 step 编号、token 数、工具名、耗时、结果,以及错误的 name 和 code。 |
full | 增加用户和助手的消息内容、工具参数、工具返回。 |
full+prompt | 再增加 request/header:完整的 system prompt 和每个工具的 schema。 |
有三样东西在任何模式下都不会出去:工具私有的 meta payload、失败 turn 的内部 error.message,以及失败请求的 message 和 stack。这个投影是白名单式的,所以插件不认识的事件类型——包括未来某个 DSH 插件新声明的——只会导出它的身份,别的什么都没有。
配合 TMA1
TMA1 会把 OTLP 代理进它自己管理的 GreptimeDB。改一行配置,DSH 就出现在它的 OTel GenAI 视图里。
TMA1 的 tma1_token_usage_1m、cost_1m、latency_1m、status_1m 这几张 flow 表都是从 span_attributes.gen_ai.* 推导出来的,而这个插件按约定就会填这些字段。除此之外不用配任何东西。
已知限制
在第一个 tag 之前它会随意改名和重新打包。peer 版本范围就是 CI 实际跑的那个版本(0.1.1-rc.2);DSH 发新版需要在这里做一次经过测试的 bump。
字段名来自 @opentelemetry/semantic-conventions/incubating,会跟着它变。span 上同时带 gen_ai.provider.name 和已废弃的 gen_ai.system。
指标落在 metric engine 上,那里的留存是物理表的属性。hint 只到逻辑表,逻辑表会存下来并显示,但不会执行(greptimedb#8951)。需要自己执行 ALTER TABLE greptime_physical_table SET 'ttl' = '180d'。
没有按 turn 的 flush——导出跟着 batch processor 的节奏走。shutdownTimeoutMillis 到点时还在途中的记录可能在退出时丢失。
不会缝进父 trace 里。