feat(telemetry): per-prompt traceId for bounded, renderable traces#4661
Conversation
📋 Review SummaryThis PR refactors telemetry tracing to use per-prompt traceIds instead of a single session-level traceId, solving the ARMS/Jaeger rendering issue for long sessions (#4554). The implementation is clean and well-tested, introducing a 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
There was a problem hiding this comment.
Pull request overview
This PR updates the core OpenTelemetry tracing model so each interaction/prompt becomes its own trace root (new traceId per prompt) to keep traces bounded/renderable, while preserving cross-prompt correlation via a session.id span attribute applied broadly across spans.
Changes:
- Make interaction spans start from
ROOT_CONTEXT(fresh traceId per prompt) and simplify parent context resolution to rely oncontext.active()rather than a synthetic session root. - Add
SessionIdSpanProcessorintended to stampsession.idonto all spans (including auto-instrumented HTTP spans). - Update debug logger trace correlation fallback to derive traceId from
sessionIdwhen no active span exists, and align tests with the new behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/utils/debugLogger.ts | Switch fallback trace correlation from session root span context to deriveTraceId(sessionId). |
| packages/core/src/utils/debugLogger.test.ts | Update fallback trace context test to match derived traceId behavior. |
| packages/core/src/telemetry/tracer.ts | Simplify parent context selection to always use context.active(); deprecate session-root-based parenting. |
| packages/core/src/telemetry/tracer.test.ts | Update parent context selection tests for the new context.active()-only behavior. |
| packages/core/src/telemetry/session-tracing.ts | Make interaction spans trace roots (ROOT_CONTEXT) and simplify parent resolution logic. |
| packages/core/src/telemetry/session-tracing.test.ts | Update interaction span parenting assertions and add coverage for session.id correlation semantics. |
| packages/core/src/telemetry/sdk.ts | Add SessionIdSpanProcessor and stop creating/storing a synthetic session root context at init/refresh. |
| packages/core/src/telemetry/sdk.test.ts | Adjust session context refresh tests to reflect removal of synthetic session root context creation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Previously all spans within a session shared one traceId derived from SHA-256(sessionId). Long sessions produced unbounded traces that ARMS and Jaeger could not render. This change makes each interaction span a trace root with a fresh SDK-generated traceId. Cross-prompt correlation uses the session.id span attribute (already present on interaction spans, now stamped on all spans via SessionIdSpanProcessor). Key changes: - startInteractionSpan uses ROOT_CONTEXT instead of session root - withInteractionSpan defaults to ROOT_CONTEXT when no parentContext - SessionIdSpanProcessor stamps session.id on every exported span - resolveParentContext / getParentContext simplified (no session root fallback) - debugLogger falls back to deriveTraceId(sessionId) for log-line grep - LogToSpanProcessor unchanged (naturally adapts) - createSessionRootContext marked @deprecated Closes #4554 (per-prompt traceId sub-item)
baa5efc to
beb62ed
Compare
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] startToolBlockedOnUserSpan and startHookSpan don't set session.id explicitly in their span attributes, unlike the other five span creators in this file (startInteractionSpan, withInteractionSpan, startLLMRequestSpan, startToolSpan, startToolExecutionSpan) which all use resolveSessionId(). These two functions rely entirely on SessionIdSpanProcessor reading the process-global getCurrentSessionId(). In daemon mode with concurrent sessions, this could stamp the wrong session ID on these spans. Consider adding resolveSessionId(parentCtx) to both functions for consistency with the established pattern.
— qwen3.7-max via Qwen Code /review
Review response — 6d43586
|
chiga0
left a comment
There was a problem hiding this comment.
Review: feat(telemetry): per-prompt traceId for bounded, renderable traces
PR: #4661 | Author: doudouOUC | +104/-144 across 8 files, 2 commits
Summary
Each interaction/prompt now gets its own trace root with a fresh SDK-generated traceId, replacing the old model where all spans in a session shared one traceId derived from SHA-256(sessionId). This prevents unbounded traces that ARMS/Jaeger can't render. Cross-prompt correlation uses the session.id span attribute stamped on all exported spans via SessionIdSpanProcessor.
Review Coverage
Reviewed all 8 files: sdk.ts (SessionIdSpanProcessor, init/refresh simplification), session-tracing.ts (ROOT_CONTEXT parenting, simplified resolveParentContext), tracer.ts (simplified getParentContext, deprecated createSessionRootContext), debugLogger.ts (cached deriveTraceId fallback), and all 4 test files.
Findings
0 blocking findings. Clean telemetry architecture change.
Highlights:
SessionIdSpanProcessorcorrectly checksattributes['session.id']before writing — doesn't clobber explicitly set values fromresolveSessionId()startInteractionSpanusesROOT_CONTEXT— each interaction is a trace root with fresh traceIdresolveParentContextsimplified from 4-level priority (explicit parent → active span → session root → active context) to 2-level (explicit parent → active context)withInteractionSpandefaults toROOT_CONTEXTwhen noparentContext— correct for daemon cron paths- Debug logger caches
deriveTraceId(sessionId)— avoids synchronous SHA-256 on every log call (commit 2 fix) refreshSessionContextcorrectly simplified — try/catch removed (no longer needed)createSessionRootContextproperly deprecated, retained for backward compatibility- All test files updated: session-context mocks removed, ROOT_CONTEXT assertions added, session.id stamping tested
wenshao review status: 7 suggestions — cache deriveTraceId + remove vestigial try/catch fixed in commit 2; SessionIdSpanProcessor unit tests, resetDebugLoggingState cache clearing, setAttribute try/catch, dead code cleanup, setSessionContext signature — deferred to follow-up PRs (agreed by author).
LGTM.
— QoderWork automated review
… in resetDebugLoggingState
Review response — 0a9a3a1
|
Verification Report — PR #4661Commit: Test Results
Test File Breakdown
Execution Environment
VerdictAll 5 checks pass. 210 telemetry-related tests cover the changed files (tracer, session-tracing, sdk, debugLogger). No regressions detected. |
wenshao
left a comment
There was a problem hiding this comment.
Clean per-prompt traceId refactor. All R1/R2 feedback addressed. 170 tests pass, CI green. No blocking findings. — qwen3.7-max via Qwen Code /review
chiga0
left a comment
There was a problem hiding this comment.
Re-approve on new HEAD 0a9a3a13 (commit 3: "guard SessionIdSpanProcessor.onStart with try/catch, clear cache in resetDebugLoggingState").
Changes since last approval:
SessionIdSpanProcessor.onStartnow wrapped intry/catch— OTel processor errors won't break span creation (addresses wenshao)resetDebugLoggingState()now clearscachedSessionId/cachedTraceId— test isolation fix (addresses wenshao)
All original review findings still hold — 0 blocking issues. wenshao also approved at this HEAD.
— QoderWork automated review
Summary
SHA-256(sessionId)SessionIdSpanProcessorto stampsession.idattribute on all exported spans (including auto-instrumented HTTP spans), enabling cross-prompt correlation via attribute querywithInteractionSpandefaults toROOT_CONTEXTwhen noparentContextis provided (daemon passes its HTTP span as parent)resolveParentContext/getParentContext) by removing the now-unnecessary session root fallbackderiveTraceId(sessionId)for log-line grep correlationMotivation
Long sessions produce thousands of spans under a single traceId — ARMS and Jaeger cannot render such traces. Daemon mode is worse: the entire process lifetime shares one traceId. This was identified as a remaining sub-item of #4554.
What doesn't change
session.idattribute (already present on interaction spans) — now stamped on ALL spansLogToSpanProcessorfallback — orphaned logs still usederiveTraceId(sessionId)as catch-allcreateSessionRootContext— retained as@deprecatedfor backward compatibilityresolveSessionId— daemon multi-session attribution unchangedMigration
traceId = SHA256(sessionId)[:32]→attribute.session.id = <sessionId>Test plan
OTEL_TRACES_EXPORTER=console TELEMETRY_ENABLED=true npx qwen --print "hello"— verify distinct traceIds per interaction🤖 Generated with Qwen Code