Files
deepseek-harness/packages/telemetry/session-telemetry-otel/README.md
kingwl 6adb14b56d feat(telemetry): adopt from the construction boundary — constructor seeds never re-export
A cursor-less adoption (process restart + resume, fork, seam-module
reload) replayed the session's full log from seq 0, re-exporting
history that already left the process — a resume re-billed its entire
stored log on every restart, and a fork re-shipped the parent's prefix
under the child's id, doubling query-time counts on OTLP backends with
no native ingest dedupe.

dsh-session now exposes the fact the constructor already validated but
discarded: Session.firstLiveSeq, the constructor-seed length — the
first seq appended in this process. header.seedLength cannot serve
here: it is the durable fork-lineage boundary, and a resumed session's
constructor seed is its full stored log while the header keeps the
original fork value (llm-replay and session-query-sqlite depend on
that meaning). Constructor seeds also never publish on the
session/event firehose, so adoption replaying them was inconsistent
with the system's own publication semantics.

Adoption's cursor-less fallback starts at firstLiveSeq; seed events
still feed the chunk projection, so mid-step continuations re-drop
after a resume. Fork streams are no longer self-contained: records now
carry session.seed_length (with the existing session.parent_id) so
receivers stitch the child's stream onto the parent's. Accepted cost,
consistent with at-most-once delivery and recorded in the revival
Agent Note: a resume no longer backfills records a previous process
failed to deliver — a deployment with that requirement needs the
deferred outbox, not replay.

Pinned red-first: seeded adoption exports nothing (assertion reversed
from the prior seed-readback test, obsolete behavior changed with its
test), resume-shaped seed rebuilds the projection without exporting,
and fork records carry the stitch attributes.
2026-07-27 18:35:03 +08:00

4.4 KiB

@deepseek-ai/dsh-session-telemetry-otel

English | 中文

The OpenTelemetry backend for the telemetry seam — the only entry a deployment loads. It composes the OTel JS SDK as-is (LoggerProvider → BatchLogRecordProcessor → OTLP/HTTP log exporter) and maps each record the seam hands over onto logger.emit(), under two instrumentation scopes: ledger records on @deepseek-ai/dsh-session-telemetry-otel, operational records on @deepseek-ai/dsh-session-telemetry-otel/ops. Resource identity (service.name/service.version) comes from dsh-llm's APP_IDENTITY, the same source the attribution headers use.

Config

- id: telemetry-otel
  name: '@deepseek-ai/dsh-session-telemetry-otel'
  config:
    exporter:                # passed verbatim to the SDK's OTLP/HTTP log exporter
      url: https://collector.example.com/v1/logs
      headers:
        authorization: !!js `Bearer ${process.env.OTLP_TOKEN}`
    processor: {}            # optional; passed verbatim to BatchLogRecordProcessor

exporter.url is the one field this package validates itself — required, no default, must parse as http(s) — so a missing endpoint fails at plugin load. Everything else is the SDK's option shape, owned and documented by the SDK, and both blocks pass through whole: every OTLPExporterNodeConfigBase field (headers, timeoutMillis, compression, keepAlive, …) reaches the exporter, and batching, export cadence (scheduledDelayMillis), retry, queue bounds, and loss policy under sustained failure are the SDK's documented behavior, tuned through the processor passthrough. The backend deliberately implements no flush(): the batch processor is the only flusher in the process, which is what makes shutdown()'s drain complete. Removing this block from cordis.yml is the opt-out: no residual state, no enabled flag.

What leaves the machine

Records carry the complete event.data as the seam's telemetry/redact waterfall returns it — user and assistant message content, tool arguments and results (command output, file contents), the full system prompt and tool schemas (request/header), todo text, compaction summaries, hook stderrSummary, and the session cwd (a local path). The seam ships no redaction rules: with no telemetry/redact listener mounted, that is the raw captured copy, so a deployment exporting beyond a trusted boundary mounts its own rules (see the seam README). Provider credentials never appear regardless: adapter API keys are constructor parameters, not session events, so they are structurally absent from the log and therefore from telemetry.

Field mapping

Seam record → SDK log record: time → timestamp/observedTimestamp; severity → severityNumber/severityText (INFO 9 / WARN 13 / ERROR 17); body → the structured log body; attributes verbatim. Receivers dedupe on (session.id, event.seq), alert on severity, and detect crashes by shutdown-record absence (a session with activity, no shutdown ops record, gone stale ended uncleanly). The marker means telemetry stopped observing the session cleanly — emitted at the session's own disposal, or at application teardown for sessions still running then; a marker followed by more of that session's events is a telemetry reload, not a session restart. Streams are not self-contained across lineage: a resumed session continues its own id's stream from where the previous process left off, and a forked session's stream starts at its inherited boundary — its prefix lives in the parent's stream, stitched via session.parent_id + session.seed_length.

Model Experience

None, as the backend only forwards the seam's redacted records into the OTel SDK pipeline; it never contributes to a model request.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • Upstream experimental tree — @opentelemetry/sdk-logs is still published from the upstream experimental tree; SDK API churn lands here and only here — the seam contract does not move.
  • No live-collector coverage — every test exports to a local mock collector; the keyless Loader-composition e2e (tests/loader-composition.e2e.ts) covers the wire shape on every run, and behavior against a real OTLP deployment (auth, TLS, throttling) is the SDK exporter's documented territory.