Repository navigation
feat(observability): nested typed spans for Langfuse agent traces - #402
Conversation
Agent sanitized observations rendered flat at the Unix epoch in Langfuse: SpanRecords carried no start/end times and no parent linkage, so the UI showed no durations, no tree, and no observation types. - agentkit SpanRecord gains optional provider-neutral span_id/parent_span_id (OTel concepts, not a Core tracing rewrite per ADR-0049). - OtlpHttpObservationSink emits real spanId + parentSpanId so Langfuse renders the parent/child observation tree, plus non-zero start/end times. - Recorder emits a nested tree per turn: root agent.turn.sanitized (type=agent) with child generation spans (type=generation, gen_ai.usage.* token counts) and tool spans (type=tool) with real call->result durations. - Narrow int-only allowlist for gen_ai.usage.input_tokens/output_tokens; the no-leak boundary is otherwise unchanged (raw tape stays local; input/output still gated by the sanitized projection schema). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAgent observation recording now emits per-turn nested span trees with span identifiers and parent/child relationships. ChangesNested Span Tree for Agent Observations
Sequence DiagramsequenceDiagram
participant Agent
participant AgentObservationRecorder
participant ObservationSink
Agent->>AgentObservationRecorder: start_turn()
AgentObservationRecorder->>ObservationSink: SpanRecord(agent.turn.sanitized, span_id, parent_span_id=None)
Agent->>AgentObservationRecorder: observe_llm_usage()
AgentObservationRecorder->>ObservationSink: SpanRecord(agent.generation.sanitized, span_id, parent_span_id=turn_id)
Agent->>AgentObservationRecorder: observe_tool_call()
Agent->>AgentObservationRecorder: observe_tool_result()
AgentObservationRecorder->>ObservationSink: SpanRecord(agent.tool.sanitized, span_id, parent_span_id=turn_id)
Agent->>AgentObservationRecorder: complete_turn()
Note over ObservationSink: Nested structure:<br/>turn (root)<br/>├─ generation (child)<br/>└─ tool (child)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Problem
Agent sanitized observations were collected and exported, but rendered flat at the Unix epoch (1970) in Langfuse with no durations, no tree, and no observation types. Root cause:
SpanRecords carried nostart_time/end_timeand no parent linkage, so the OTLP payload sent0timestamps and a fresh randomspanIdwith noparentSpanId.This brings agent traces up to the standard nested-timeline Langfuse experience (parent/child tree, real duration bars, GENERATION/TOOL types, token counts).
Changes
SpanRecord): two optional, provider-neutral fieldsspan_id/parent_span_id. OTel concepts, backward compatible — a minimal model extension, not the "Core tracing rewrite" rejected in ADR-0049.OtlpHttpObservationSink): emit realspanId+parentSpanIdso Langfuse stitches the observation tree; non-zero start/end times. Narrow int-only allowlist forgen_ai.usage.input_tokens/gen_ai.usage.output_tokens(theoutputsubstring would otherwise drop them; integer counts cannot carry raw text).AgentObservationRecorder): nested tree per turn —agent.turn.sanitized(type=agent, real start/end)agent.generation.sanitized(type=generation,gen_ai.usage.*, start inferred from previous activity since the runtime only surfaces usage at completion)agent.tool.sanitized(type=tool, realcall→resultduration)No-leak boundary
Unchanged: raw tape stays local;
langfuse.observation.input/outputstill gated by the sanitized projection schema; new attributes are token integers + a type enum + random span IDs.Validation
141 passedacross observability suite, release contract, platform smoke, runtime, adapter, integration smoke. ruff clean,git diff --checkclean.parentSpanId/non-zero times, int usage allowed, non-int usage rejected._DEFAULT_PROMETHEUS_RECORDER, present onmain) is untouched.Not included
Live deploy + Langfuse UI/API verification of a real session (requires a Langfuse-connected environment).
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests