tma1-ai/

DSH OTel

Telemetría de DeepSeek Harness, como OpenTelemetry puro.

Sin collector. Sin sidecar. Sin forkear DSH. Se instala como un plugin común de DeepSeek Harness, y cada turno, llamada al modelo y ejecución de herramienta pasa a ser una fila consultable en GreptimeDB.

INSTALACIÓN

dsh plugin --profile headless add @tma1-ai/dsh-plugin-greptimedb

El paquete trae un bundle patch, así que ese único comando lo conecta al profile. dsh plugin delega en el pnpm que esté en tu PATH, y un directorio de profile de dsh es su propio workspace root de pnpm — pnpm 9 se niega a instalar ahí e ignora la configuración de linker que escribe dsh, así que usá pnpm 10 o superior. Los valores por defecto ya apuntan a un GreptimeDB local.

Apuntalo a tu propia base de datos, o levantá una local
$DSH_HOME/profiles/<name>/cordis.patch.yml
- id: greptimedb-otel
name: '@tma1-ai/dsh-plugin-greptimedb'
config:
endpoint: https://<host>/v1/otlp
database: <dbname>
username: <user>
password: <password>
greptimedb local
docker run -p 127.0.0.1:4000-4003:4000-4003 \
-v "$(pwd)/greptimedb_data:/greptimedb_data" \
--name greptime --rm greptime/greptimedb:v1.2.0-beta.2 standalone start \
--http-addr 0.0.0.0:4000 --rpc-bind-addr 0.0.0.0:4001 \
--mysql-addr 0.0.0.0:4002 --postgres-addr 0.0.0.0:4003
greptimedb · sql
-- Las llamadas a herramientas más lentas, con el modelo que las pidió.
SELECT tool.span_name,
chat."span_attributes.gen_ai.request.model" AS model,
tool.duration_nano / 1000000 AS ms
FROM opentelemetry_traces AS tool
JOIN opentelemetry_traces AS chat
ON chat.trace_id = tool.trace_id
AND chat."span_attributes.dsh.step" = tool."span_attributes.dsh.step"
AND chat.span_name LIKE 'chat%'
WHERE tool.span_name LIKE 'execute_tool%'
ORDER BY tool.duration_nano DESC
LIMIT 10;

Los spans de chat y los de herramienta comparten trace y dsh.step, así que correlacionarlos es un join de SQL común. Cada timestamp viene del evento de sesión al que pertenece, no de leer el reloj mientras se procesa el evento.

Tres señales, un plugin

Traces, métricas y logs. signals acepta cualquier subconjunto — una señal desactivada no construye exporter alguno.

Por defecto no sale nada

El valor por defecto content: none exporta solo estructura y contabilidad. Sin prompts, sin mensajes, sin argumentos de herramientas, sin resultados.

Falla al cargar, no al exportar

Una configuración inválida falla cuando el plugin carga, nombrando el campo culpable — no en silencio durante la primera exportación.

Señales

Qué llega en cada señal

Traces para la forma, métricas para retención larga y percentiles a prueba de sampleo, logs para los eventos de sesión crudos.

[01]

Turno, chat, herramienta

Los spans de turno son raíces. Los de chat y herramienta cuelgan de ellos como hermanos, correlacionados por dsh.step.

árbol de spans
invoke_agent dsh turn/start → turn/end
├── chat deepseek-chat step/start → assistant/message
├── execute_tool bash tool/call → tool/result
└── chat deepseek-chat
[02]

Todo span de chat tiene un fin definido

Cuatro caminos cierran un span de chat, incluido el de caída. Ninguno deja un span colgado en un instante arbitrario.

SituaciónFin y estado
El modelo respondióassistant/message · OK
Stream interrumpidoassistant/message · OK, más dsh.response.interrupted
La petición fallóel step/end de ese step · ERROR, con el tipo de error
Sin evento de cierre (caída, apagado)último evento visto · UNSET, más dsh.span.unclosed
[03]

Contabilidad de tokens

Los conteos de DSH son disjuntos: inputTokens es solo entrada sin caché, y las lecturas y escrituras de caché son campos aparte. gen_ai.usage.input_tokens es el total facturado, así que el plugin los suma. La salida incluye los tokens de razonamiento.

atributos de tokens
gen_ai.usage.input_tokens = inputTokens
+ cacheReadTokens
+ cacheWriteTokens
gen_ai.usage.output_tokens = outputTokens
 
# el desglose sigue siendo consultable
dsh.usage.uncached_input_tokens
dsh.usage.cache_read_tokens
dsh.usage.cache_write_tokens
dsh.usage.reasoning_tokens
[04]

Cinco dashboards de Grafana

Vienen en grafana/ junto a un stack de compose que levanta GreptimeDB y Grafana a la vez. Cada consulta de panel se verifica contra una base real con node grafana/verify.mjs.

localhost:3000 · overview
Dashboard overview de DSH OTel: turnos, llamadas al modelo, tokens facturados, caché, latencia
[05]

Un turno, span por span

Cada tabla enlaza hacia adelante: un trace id abre el waterfall de ese turno, un session id salta entre la vista de traces y la de logs.

localhost:3000 · trace explorer
Dashboard trace explorer de DSH OTel
[06]

Una fila por evento de sesión

Cuatro atributos se vuelven columnas reales vía X-Greptime-Log-Extract-Keys. assistant/chunk nunca se exporta — el assistant/message ya ensamblado trae el mismo contenido.

greptimedb · sql
SELECT session_id, event_type, turn, step, body
FROM dsh_logs
WHERE session_id = '...' AND event_type = 'tool/result'
ORDER BY timestamp;

Métricas

Instrumentos

La misma actividad que los traces, vista con PromQL — para retención más larga y percentiles que sobreviven al sampleo.

InstrumentoTipoDimensiones
gen_ai.client.token.usageHistogramgen_ai.token.type (solo input/output), model, provider
gen_ai.client.operation.durationHistogramgen_ai.operation.name, model, provider
gen_ai.invoke_agent.durationHistogramgen_ai.operation.name
gen_ai.execute_tool.durationHistogramgen_ai.operation.name, gen_ai.tool.name
dsh.token.detailHistogramdsh.token.detail_kind (cache_read/cache_write/reasoning)
dsh.tool.invocationsCountergen_ai.tool.name, dsh.tool.outcome
dsh.turns / dsh.stepsCounter

Configuración

Las claves que realmente vas a tocar

Un patch de profile reemplaza todo el config de esa fila en vez de fusionarse con él, así que reescribí cada campo que quieras conservar.

ClavePor defectoNotas
endpointrequeridoURL base de OTLP. El plugin agrega el sufijo /v1/{traces,metrics,logs} de cada señal; una ruta por señal se rechaza al cargar.
databasepublicSe envía como X-Greptime-DB-Name.
username / passwordningunoBasic auth. Los dos o ninguno.
signalslas tresCualquier subconjunto de traces, metrics, logs.
contentnoneCuánto payload puede salir del proceso.
ttl180dRetención de las tablas de logs y traces que crea el plugin, enviada como x-greptime-hints. También acepta forever. Una tabla ya existente conserva la suya hasta un ALTER TABLE.

Batching, timeouts, nombre de servicio y overrides de tabla tienen valores razonables; la tabla completa está en el README.

Qué sale de la máquina

Lo decide content

El valor por defecto retiene todos los payloads. Subilo a propósito, por profile.

ModoSe exporta
none (por defecto)Estructura y contabilidad: tipos de evento, números de turno y step, conteos de tokens, nombres de herramientas, duraciones, resultados, name y code del error.
fullSuma el contenido de los mensajes de usuario y asistente, argumentos de herramientas y resultados.
full+promptSuma request/header: el system prompt completo y el schema de cada herramienta.

Tres cosas no salen en ningún modo: el payload privado meta de una herramienta, el error.message interno de un turno fallido, y el mensaje y stack de una petición fallida. La proyección es una allowlist positiva, así que un tipo de evento que el plugin no conoce — incluido uno que declare un futuro plugin de DSH — exporta su identidad y nada más.

Con TMA1

Apuntalo a TMA1

TMA1 hace de proxy OTLP hacia un GreptimeDB que él mismo administra. Cambiás una línea y DSH aparece en su vista OTel GenAI.

Las flow tables ya coinciden

Las flow tables tma1_token_usage_1m, cost_1m, latency_1m y status_1m de TMA1 derivan de span_attributes.gen_ai.*, que este plugin completa por convención. No hay nada más que configurar.

cordis.patch.yml
endpoint: http://localhost:14318/v1/otlp

Limitaciones conocidas

Leé esto antes

DSH es pre-release

Renombra y reempaqueta libremente hasta su primer tag. El rango de peer es la versión exacta contra la que corre CI (0.1.1-rc.2); una nueva release de DSH necesita un bump probado acá.

Las convenciones GenAI son experimentales

Los nombres vienen de @opentelemetry/semantic-conventions/incubating y se mueven con él. Los spans llevan tanto gen_ai.provider.name como el obsoleto gen_ai.system.

<code>ttl</code> no llega a las tablas de métricas

Las métricas caen en el metric engine, donde la retención es una propiedad de la tabla física. El hint llega a la tabla lógica, que lo guarda y lo muestra pero nunca lo aplica (greptimedb#8951). Ponelo vos con ALTER TABLE greptime_physical_table SET 'ttl' = '180d'.

La exportación es por lotes y el apagado es acotado

No hay flush por turno — la exportación sigue la cadencia de los batch processors. Los registros en vuelo cuando expira shutdownTimeoutMillis pueden perderse al salir.

Las sesiones de subagente tienen su propio trace

No se cosen dentro del trace padre.