The flow tables already line up
TMA1’s tma1_token_usage_1m, cost_1m, latency_1m, and status_1m flow tables derive from span_attributes.gen_ai.*, which this plugin populates by convention. Nothing else to configure.
DSH OTel
No collector. No sidecar. No fork of DSH. It installs as an ordinary DeepSeek Harness plugin, and every turn, model call, and tool execution becomes a row in GreptimeDB you can query.
INSTALL
dsh plugin --profile headless add @tma1-ai/dsh-plugin-greptimedb
The package ships a bundle patch, so that one command wires it into the profile. dsh plugin forwards to whichever pnpm is on your PATH, and a dsh profile directory is its own pnpm workspace root — pnpm 9 refuses to install there and ignores the linker settings dsh writes, so use pnpm 10 or newer. The defaults already point at a local GreptimeDB.
Chat spans and tool spans share a trace and a dsh.step, so correlating them is a plain SQL join. Every timestamp comes from the session event it belongs to, not from a clock read while the event is being handled.
Traces, metrics, and logs. signals takes any subset — a disabled signal builds no exporter at all.
The default content: none exports structure and accounting only. No prompts, no messages, no tool arguments, no tool results.
Bad configuration fails when the plugin loads, with the offending field named — not silently at the first export.
Signals
Traces for shape, metrics for long retention and sampling-proof percentiles, logs for the raw session events.
Turn spans are roots. Chat and tool spans hang off them as siblings, correlated by dsh.step.
Four paths close a chat span, including the crash case. None of them leaves a span dangling at an arbitrary time.
| Situation | End time and status |
|---|---|
| Model responded | assistant/message · OK |
| Stream interrupted | assistant/message · OK, plus dsh.response.interrupted |
| Request failed | that step’s step/end · ERROR, with the error type |
| No end event (crash, teardown) | last event seen · UNSET, plus dsh.span.unclosed |
DSH’s counts are disjoint: inputTokens is uncached input alone, cache reads and writes are separate fields. gen_ai.usage.input_tokens is the billed total, so the plugin sums them. Output includes reasoning tokens.
They ship in grafana/ with a compose stack that brings up GreptimeDB and Grafana together. Every panel query is checked against a live database by node grafana/verify.mjs.

Every table links onward: a trace id opens that turn’s waterfall, a session id jumps between the trace and log views.

Four attributes become real columns through X-Greptime-Log-Extract-Keys. assistant/chunk is never exported — the assembled assistant/message carries the same content.
Metrics
The same activity as the traces, through PromQL — for longer retention and percentiles that survive sampling.
| Instrument | Type | Dimensions |
|---|---|---|
gen_ai.client.token.usage | Histogram | gen_ai.token.type (input/output only), 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 | — |
Configuration
A profile patch replaces the row’s whole config instead of merging into it, so restate every field you want to keep.
| Key | Default | Notes |
|---|---|---|
endpoint | required | OTLP base URL. The plugin appends each signal’s /v1/{traces,metrics,logs} suffix; a per-signal path is rejected at load. |
database | public | Sent as X-Greptime-DB-Name. |
username / password | none | Basic auth. Both or neither. |
signals | all three | Any subset of traces, metrics, logs. |
content | none | How much payload may leave the process. |
ttl | 180d | Retention for the log and trace tables this plugin creates, sent as x-greptime-hints. Also accepts forever. An existing table keeps its own until ALTER TABLE. |
Batching, timeouts, service name, and table overrides have sensible defaults; the full table is in the README.
What leaves the machine
content decides thisThe default withholds all payloads. Raise it deliberately, per profile.
| Mode | Exported |
|---|---|
none (default) | Structure and accounting: event types, turn and step numbers, token counts, tool names, durations, outcomes, error name and code. |
full | Adds user and assistant message content, tool arguments, tool results. |
full+prompt | Adds request/header: the complete system prompt and every tool schema. |
Three things never leave in any mode: a tool’s private meta payload, the internal error.message of a failed turn, and the message and stack of a failed request. The projection is a positive allowlist, so an event type the plugin does not know — including one a future DSH plugin declares — exports its identity and nothing else.
With TMA1
TMA1 proxies OTLP into a GreptimeDB it manages. Change one line and DSH shows up in its OTel GenAI view.
TMA1’s tma1_token_usage_1m, cost_1m, latency_1m, and status_1m flow tables derive from span_attributes.gen_ai.*, which this plugin populates by convention. Nothing else to configure.
Known limitations
It renames and repackages freely before its first tagged release. The peer range is the exact version CI runs against (0.1.1-rc.2); a new DSH release needs a tested bump here.
Names come from @opentelemetry/semantic-conventions/incubating and move with it. Spans carry both gen_ai.provider.name and the deprecated gen_ai.system.
Metrics land on the metric engine, where retention is a property of the physical table. The hint reaches the logical table, which stores and displays it but never enforces it (greptimedb#8951). Set it yourself with ALTER TABLE greptime_physical_table SET 'ttl' = '180d'.
There is no per-turn flush — export follows the batch processors’ cadence. Records still in flight when shutdownTimeoutMillis expires may be lost at exit.
They are not stitched into the parent’s.