feat(telemetry): implement ADR 0050 Level 3 content capture - #6429
feat(telemetry): implement ADR 0050 Level 3 content capture#6429dhshah13 wants to merge 14 commits into
Conversation
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
1 similar comment
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
Site previewPreview: https://701cc976-site.fullsend-ai.workers.dev Commit: |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
…tion Signed-off-by: Dharit Shah <dhshah@redhat.com>
- emit the schema-required finish_reason from the iteration outcome - budget keeps an ordered suffix so the final answer survives a cut; exact accounting includes tool-call name bytes, and names are redacted - treat sanitized-to-empty as redacted, never as unchanged - evict over-budget accumulation early so long sessions stay bounded - attach truncation markers even when the budget drops every part - bound free-text attributes (model, skip_reason, work_item_id) at their call sites now that the content gate lifts the provider-wide SDK cap Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
The eviction pre-trim cut raw bytes before redaction, violating the documented redaction-before-truncation invariant: a secret straddling the trim boundary lost its prefix, no pattern matched the surviving fragment, and it rode to both sinks unmasked with zero findings. The pre-trim now redacts first and trims the masked text, so a boundary-straddling secret is whole when scanned. Whole parts evicted during accumulation were never scanned, so fullsend.content.redactions undercounted against its documented contract (findings from parts the size budget later dropped). Evicted parts are now scanned before discard and their findings carried to Result. The evicted-counter comment claimed eviction happens without changing the outcome; eviction compares pre-redaction sizes while the Result budget runs post-redaction, so it can drop content the documented policy would keep. The comment now states the approximation honestly. Signed-off-by: Dharit Shah <dhshah@redhat.com>
When the Level 3 content gate is on, spanLimits lifts the SDK attribute value cap so the SDK cannot cut gen_ai.output.messages mid-value — but an operator's explicit OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT or OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT still wins. Under a finite explicit limit every over-limit content value was cut mid-JSON by the SDK, in both sinks, with no fullsend.content.truncated marker (it reflects only collector-side cuts) and no signal to the operator, silently breaking the documented parse-the-JSON consumer contract. Surface the collision at Setup: when the gate is on and the operator limit resolved to a finite value, warn on stderr that content will be cut mid-JSON without the truncation marker. An explicit -1 (unlimited) cannot cut and stays silent. The operator limit still wins — telemetry never fails a run, and the warning makes the consequence visible instead of altering precedence. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The admin guide said only that an explicit OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT still wins over the content-gate cap lift. State the consequence: a finite explicit limit cuts over-limit gen_ai.output.messages values mid-JSON in both sinks, the fullsend.content.truncated marker does not flag an SDK cut, and fullsend warns on stderr at startup about the combination. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
5a11d23 to
589a882
Compare
PR Summary by Qodofeat(telemetry): implement Level 3 content capture on agent spans
AI Description
Diagram
High-Level Assessment
Files changed (12)
|
Code Review by Qodo
1.
|
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Implements ADR 0050's Level 3 exactly as accepted: when the org sets
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, conversation content is included in spans. One environment variable, the Level 2 enablement pattern — no new ADR, no additional consent surface.The agent runtime's native content telemetry is never enabled. Content is assembled by fullsend's own runner from the normalized event stream the console already renders, redacted through
security.OutputPipeline()at assembly, and attached to the per-iterationagentspan. Fullsend reads the gate variable itself — it is a GenAI-instrumentation convention documented by opentelemetry-python-contrib; the pinned semconv v1.37.0 release does not define it, and Claude Code does not read it (0 occurrences in the shipped binary). Fullsend never sets the runtime's ownOTEL_LOG_*variables, so there is no second export pipeline and no redaction bypass.What ships
internal/telemetry/content.gointernal/cli/content_collector.gogen_ai.output.messagesattribute per iterationinternal/telemetry/telemetry.goboundedStringAttr;Setupwarns if a finite operator limit would cut content JSONinternal/cli/run.goRunParams.OnEventwith the console renderer preserved; gate off =OnEventnil = the pre-change path, byte-identicalGate values
true,span_only,span_and_event(case-insensitive)false,NO_CONTENT,event_only(we cannot honor "only"), anything unrecognizedContent shape. One assistant message per iteration, conformant to the v1.37.0 output-messages JSON schema including the required
finish_reason(stop/errorfrom the iteration outcome). Parts:text,reasoning(the schema's GenericPart extension point), andtool_callwith name + summary — no fabricatedarguments.Redaction and size. Redaction runs before any cut (truncating first could split a secret); hits are masked, counted in
fullsend.content.redactions, and warned on the console. The 256 KiB budget keeps an ordered suffix — the final answer survives a cut — with exact dropped-byte accounting surfaced asfullsend.content.truncated/fullsend.content.dropped_bytes, and early eviction keeps long sessions memory-bounded.Deliberately not captured (documented as dispositions)
gen_ai.input.messages— the CLI passes a constant literal; there is no runner-side input to record.Next in this series (PR B, starts after this merges)
Tool results complete the captured record. One PR, three parts:
ToolResultEventininternal/runtime, emitted from thetool_resultblocks the Claude stream parser currently discards, as a runtime-agnostic addition to the normalizedAgentEventcontract.tool_call_responsepart ({type, id, result}); the gate, redaction, suffix budget, and markers in this PR handle it with no new surface (still one env var).The parser half ships together with its consumer rather than ahead of it, so no event type lands without production callers.
Evidence (pilot MLflow, experiment 1)
tr-5802e956713c146edef284bca8a0d338finish_reason=stop, byte-identical inrun-telemetry.jsonland on the backend; console rendered normally through the teetr-a1703820bb276f1121a9f808d0bde07etr-4a4ce9d96307f239880d5e2ba1802f72Test plan
go test ./internal/telemetry/ ./internal/cli/ -race— green. (The twoTestDummyRuntime_*failures on the author's machine are pre-existing environment flakes present on bare main.)telemetry.Setup()file sink for both gate states.Notes for review
Relates to #5361 (content capture is the last telemetry level; the enrichment legs remain open). Relates to #6036 (eval scorers read
run-telemetry.jsonl; content-aware scorers become possible once this lands). Relates to #294 (retention; unchanged by this PR).