Background
Alibaba Cloud OpenTelemetry documentation recommends writing traceId and spanId into application logs so that trace data and log data can be correlated in backends (e.g., SLS, Grafana). Currently, qwen-code debug log files (~/.qwen/debug/{sessionId}.txt) contain timestamps and log levels, but no trace context — making it impossible to join a log line to its corresponding OTel span.
Proposed Change
Inject [trace_id=xxx span_id=yyy] into every debug log line. Priority:
- Active OTel span — if a real span is active (non-zero traceId), use its
traceId/spanId
- Session-derived fallback — if no active span, derive a deterministic
traceId from sessionId via SHA-256 (same algorithm as LogToSpanProcessor), and generate a stable per-session spanId
- No context — omit the trace fields entirely
Example output:
2026-01-24T10:30:00.000Z [DEBUG] [OTEL] [trace_id=aabb...8899 span_id=1122334455667788] OpenTelemetry SDK started successfully.
2026-01-24T10:30:01.123Z [DEBUG] [tool.Bash] [trace_id=aabb...8899 span_id=deadbeef12345678] Tool executed
Implementation
New files
packages/core/src/telemetry/trace-id-utils.ts — shared deriveTraceId, randomSpanId, randomHexString; used by both LogToSpanProcessor and debugLogger to ensure consistent traceIds
packages/core/src/telemetry/session-context.ts — neutral singleton module to pass session root context from sdk.ts to tracer.ts without circular dependency
packages/core/src/telemetry/tracer.ts — withSpan (auto-lifecycle async wrapper), startSpanWithContext (manual lifecycle for async generators), createSessionRootContext
Modified files
debugLogger.ts — getTraceContext() with OTel-first priority; buildLogLine includes trace fields; fallback spanId cached per session
sdk.ts — call setSessionContext(createSessionRootContext(sessionId)) after SDK start; clear on shutdown
log-to-span-processor.ts — remove local deriveTraceId/randomHexString, import from trace-id-utils
loggingContentGenerator.ts — wrap generateContent in withSpan; use startSpanWithContext + runInContext for generateContentStream so the span covers the full stream lifetime
client.ts — wrap generateContent in withSpan
coreToolScheduler.ts — wrap executeSingleToolCall in withSpan; mark tool error/denial paths as SpanStatusCode.ERROR
Testing
All existing tests pass. New tests added for:
- Trace context injection in log lines
- OTel span priority over session fallback
- Zero-traceId fallback behavior
- Stable fallback spanId per session
deriveTraceId determinism and format
Background
Alibaba Cloud OpenTelemetry documentation recommends writing
traceIdandspanIdinto application logs so that trace data and log data can be correlated in backends (e.g., SLS, Grafana). Currently, qwen-code debug log files (~/.qwen/debug/{sessionId}.txt) contain timestamps and log levels, but no trace context — making it impossible to join a log line to its corresponding OTel span.Proposed Change
Inject
[trace_id=xxx span_id=yyy]into every debug log line. Priority:traceId/spanIdtraceIdfromsessionIdvia SHA-256 (same algorithm asLogToSpanProcessor), and generate a stable per-sessionspanIdExample output:
Implementation
New files
packages/core/src/telemetry/trace-id-utils.ts— sharedderiveTraceId,randomSpanId,randomHexString; used by bothLogToSpanProcessoranddebugLoggerto ensure consistent traceIdspackages/core/src/telemetry/session-context.ts— neutral singleton module to pass session root context fromsdk.tstotracer.tswithout circular dependencypackages/core/src/telemetry/tracer.ts—withSpan(auto-lifecycle async wrapper),startSpanWithContext(manual lifecycle for async generators),createSessionRootContextModified files
debugLogger.ts—getTraceContext()with OTel-first priority;buildLogLineincludes trace fields; fallback spanId cached per sessionsdk.ts— callsetSessionContext(createSessionRootContext(sessionId))after SDK start; clear on shutdownlog-to-span-processor.ts— remove localderiveTraceId/randomHexString, import fromtrace-id-utilsloggingContentGenerator.ts— wrapgenerateContentinwithSpan; usestartSpanWithContext+runInContextforgenerateContentStreamso the span covers the full stream lifetimeclient.ts— wrapgenerateContentinwithSpancoreToolScheduler.ts— wrapexecuteSingleToolCallinwithSpan; mark tool error/denial paths asSpanStatusCode.ERRORTesting
All existing tests pass. New tests added for:
deriveTraceIddeterminism and format