qwen-code Daemon HTTP 入站 Trace Context 传播设计:让 W3C traceparent 在服务端落地
qwen-code Daemon HTTP 入站 Trace Context 传播设计让 W3C traceparent 在服务端落地【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code导读qwen-code daemon守护进程对外提供 HTTP 服务serve模式时过往只负责记录请求 span每个qwen-code.daemon.requestspan 都会开启一条全新的 trace即使调用方企业代理、OTel 插桩客户端、ACP 网关已经在 HTTP 请求头中携带了标准 W3Ctraceparent服务端 span 也无法与调用方的 trace 建立父子关联跨服务排障只能退回到对时间戳的老路。本文基于 2026-08-18-daemon-http-inbound-trace-context.md 设计文档结合仓库源码完整讲解该能力的设计动机、抽取/解析实现、采样策略、日志关联含 telemetry 关闭场景、非目标与验证方法帮助读者理解 qwen-code 如何在 HTTP 服务边界上实现 W3C Trace Context 入站提取并让 daemon 日志在零遥测配置下也能按 traceId 关联。读完本文你将掌握traceparent/tracestate头从请求到 span 父上下文的完整链路、shouldForceSampled()采样决策矩阵的运作方式、telemetry 开关两种模式下 traceId 的落点span 前缀 vs 访问日志字段以及如何用一条curl验证整条链路。背景出站传播早已就位入站提取却缺席出站JSON-RPC_meta中的 traceparentdaemon 早已具备出站传播能力prompt 请求会在 JSON-RPC 的_meta中携带traceparentdaemon 通过extractDaemonTraceContext提取它以作为 bridge span 的父级。源码实现位于 packages/core/src/telemetry/daemon-tracing.tsexport const DAEMON_TRACEPARENT_META_KEY qwen.telemetry.traceparent; export const DAEMON_TRACESTATE_META_KEY qwen.telemetry.tracestate; export function extractDaemonTraceContext( source: unknown, ): Context | undefined { const meta (source as { _meta?: unknown } | undefined)?._meta; if (!meta || typeof meta ! object || Array.isArray(meta)) { return undefined; } const record meta as Recordstring, unknown; const traceparent record[DAEMON_TRACEPARENT_META_KEY]; if (typeof traceparent ! string || traceparent.length 0) { return undefined; } // _meta 路径可能来自两类调用方进程内 bridgeinjectDaemonTraceContext // 值已被 SAMPLED强制采样是 no-op以及直接 ACP 客户端其请求 _meta 与 // HTTP 头一样属于外部输入——因此两条边界获得同样的采样保护。 return forceSampledUnderSampler( contextFromTraceparentValues( traceparent, record[DAEMON_TRACESTATE_META_KEY], ), ); }对应地injectDaemonTraceContextdaemon-tracing.ts负责把当前活动 span 的 trace context 写入出站请求的_meta并先剥离被保留的 trace 元数据键stripReservedTraceMeta避免调用方伪造/污染。入站缺口HTTP 面只记录、不关联问题在于 HTTP 面qwen-code.daemon.requestspan 每次都是全新 trace 的根。设计文档 Motivation 明确指出The HTTP surface, however, onlyrecordsrequest spans — everyqwen-code.daemon.requestspan starts a new trace.结果是任何转发标准 W3Ctraceparent头的 HTTP 调用方都拿不到关联服务端 span 无法接回调用方的 trace跨服务调试只能靠时间戳对齐。好在基础设施已经具备withDaemonSpan接受显式的parentContext见 daemon-tracing.tsexport async function withDaemonSpanT( name: string, attributes: DaemonAttributes, fn: (span: Span) PromiseT, options: { autoOkOnSuccess?: boolean; parentContext?: Context; startTime?: Date } {}, ): PromiseT { ... return options.parentContext ? await tracer.startActiveSpan(name, spanOptions, options.parentContext, run) : await tracer.startActiveSpan(name, spanOptions, run); }W3C Trace Context 在 HTTP 服务端边界的提取本就属于 OTel HTTP 语义约定中SpanKind.SERVER邻近的标准行为——管线是现成的缺的只是从 HTTP 头提取并接线这一步。设计总览两处改动 一条轻量旁路设计文档把改动划分为两大块外加一条 telemetry 关闭时的日志关联旁路Coredaemon-tracing.ts把既有_meta提取逻辑抽成共享的contextFromTraceparentValues辅助函数全局propagation.extract优先W3CTraceContextPropagator实例兜底新增extractDaemonHttpTraceContext(headers)从 Node 风格小写键的 header 对象读取traceparent/tracestateDaemonRequestSpanOptions增加可选parentContext直接透传给withDaemonSpan。Serve 中间件daemonTelemetryMiddleware每个请求从req.headers提取失败时 fail-closed 返回undefined遥测绝不影响请求处理仅在提取成功时传入 context——没有合法头的请求保持与现在完全相同的 span 形态。提取行为受isTelemetrySdkInitialized()门控telemetry 关闭的部署在热路径上不会触发任何 OTel 机制。日志关联旁路telemetry 关闭默认时中间件用纯正则extractInboundTraceId解析traceparent把 traceId 存到响应对象的 telemetry context与 workspace hash 共用机制的独立 symbol访问日志的finish回调读出并输出为 camelCase 的traceId字段。下面逐一深入。Core 层实现共享提取 强制采样共享辅助函数contextFromTraceparentValues提取逻辑的核心是contextFromTraceparentValuesdaemon-tracing.tsfunction contextFromTraceparentValues( traceparent: string, tracestate: unknown, ): Context | undefined { const carrier: Recordstring, string { traceparent }; if (typeof tracestate string tracestate.length 0) { carrier[tracestate] tracestate; } const extracted propagation.extract(ROOT_CONTEXT, carrier); if (trace.getSpanContext(extracted)) return extracted; if (!daemonFallbackPropagator) return undefined; const fallback daemonFallbackPropagator.extract( ROOT_CONTEXT, carrier, defaultTextMapGetter, ); return trace.getSpanContext(fallback) ? fallback : undefined; }它保证无论是否注册了全局 propagator接受规则未来 traceparent 版本、tracestate、全零 id都完全一致——先走全局propagation.extract失败再走直接实例化的 W3C propagator 兜底。一个关键的工程决策是daemonFallbackPropagator的懒加载注入daemon-tracing.ts处于每次 CLI 启动的静态启动图上而opentelemetry/core是 CJS barreltree-shaking 无法瘦身即使 telemetry 关闭每次启动也有约 65 KB 开销。因此该模块不直接构造 W3C propagator而是由懒加载的 SDK chunksdk-impl.ts在 telemetry 真正启用后通过setDaemonFallbackPropagator注入SDK 初始化之前 holder 为空提取直接返回 undefined——这正是 telemetry 关闭状态无任何 OTel 副作用。extractDaemonHttpTraceContextHTTP 头 → 父 contextexport function extractDaemonHttpTraceContext( headers: Recordstring, unknown | undefined, ): Context | undefined { const traceparent headers?.[traceparent]; if (typeof traceparent ! string || traceparent.length 0) { return undefined; } const extracted contextFromTraceparentValues( traceparent, headers?.[tracestate], ); return forceSampledUnderSampler(extracted); }它接收 Node 风格小写键的 header 对象复用contextFromTraceparentValues最后过一遍forceSampledUnderSampler。注意与_meta路径的区别_meta路径用的是保留键qwen.telemetry.traceparentHTTP 路径用的是标准traceparent/tracestate头名。DaemonRequestSpanOptions.parentContextwithDaemonRequestSpan把parentContext原样透传给withDaemonSpandaemon-tracing.ts而 span 创建时startActiveSpan(name, spanOptions, parentContext, run)会以该 context 作为父级——由此 HTTP 请求 span 便接入了调用方的 trace。整棵子树随请求 span 一起迁移设计文档特别提醒迁移的是整棵子树不只是daemon.request本身。经由_meta传播的 session 子进程 spanprompt / model / tool也会加入调用方的 trace。这意味着任何按traceId聚合或告警的组件都会看到 session 侧 span 的归属随之改变——这是引入该能力时必须知会的下游影响。Serve 中间件门控提取与 fail-closeddaemonTelemetryMiddleware的接入中间件实现位于 packages/cli/src/serve/server/telemetry.ts。核心逻辑节选let parentContext: ReturnTypetypeof extractDaemonHttpTraceContext; if (isTelemetrySdkInitialized()) { try { parentContext extractDaemonHttpTraceContext(req.headers); } catch { // Telemetry must not affect request handling. parentContext undefined; } // present-but-invalid header 时输出 debug 日志限速 try { const inboundTraceparent req.headers?.[traceparent]; if ( !parentContext typeof inboundTraceparent string inboundTraceparent.length 0 acquireBreadcrumbToken() ) { emitDaemonLog( Rejected invalid inbound traceparent header., { http.route: route.route, http.request.header.traceparent: sanitizeLogText(inboundTraceparent, 128), }, { eventName: qwen-code.daemon.traceparent.invalid, severityNumber: 5, // SeverityNumber.DEBUG }, ); } } catch { // Telemetry must not affect request handling } }几个要点门控extractDaemonHttpTraceContext只在isTelemetrySdkInitialized()为真时执行telemetry 关闭的部署在热路径上只付出日志关联那条单正则 trace-id 捕获的代价不触碰任何 OTel 机制。fail-closed提取包在 try/catch 里任何异常都回落为undefined请求照常处理设计文档反复强调 telemetry never affects request handling。可诊断性头存在但非法时输出事件名为qwen-code.daemon.traceparent.invalid的 DEBUG 日志severityNumber: 5让被拒绝的头仅凭 daemon 日志即可诊断无需重放请求。日志内容经sanitizeLogText截断并中和控制字符防止伪造头结构traceparent 只含 trace-id/span-id/flags不含用户内容因此记录被拒绝的值在隐私上是安全的。限速防刷被拒绝头的面包屑按令牌桶限速TRACEPARENT_BREADCRUMB_BURST 60初始令牌TRACEPARENT_BREADCRUMB_REFILL_PER_SECOND 2/秒防止恶意客户端用大量非法 traceparent 头淹没 daemon 日志telemetry.ts。随后withDaemonRequestSpan在parentContext存在时才传入...(parentContext ? { parentContext } : {})保证没有合法头的请求保持与现在完全相同的 span 形状。注意session-subprocess span 的归属随之改变由于_meta转发injectDaemonTraceContext会把请求 span 的 context 继续带入 session 子进程子进程中的 prompt/model/tool span 也会一并归属到调用方 trace。文档特别注明任何按 traceId 聚合、告警或做 RED 指标的组件需要感知 session 侧 span 的 traceId 归属变化。采样策略不盲从调用方的 sampled 位这是本设计最精巧的部分。W3Ctraceparent的 flags 中含sampled位但HTTP 入站路径并不原样采纳调用方的 sampled 位。原因源码注释与设计文档一致在默认的parentbased_always_on采样器下daemon SDK 未配置 sampler一个远端未采样sampled0的父级会委托给AlwaysOffSampler从而静默删除请求 span、next()之下的一切以及——经由_meta转发——session 子进程 span。sampled0只是调用方自己的 head-based 比率采样不该成为丢弃 daemon 遥测的指令。因此入站 HTTP 父级通过与合成 session 根相同的shouldForceSampled()决策矩阵强制TraceFlags.SAMPLED。决策矩阵实现在 packages/core/src/telemetry/tracer.tsexport function shouldForceSampled(): boolean { const sampler process.env[OTEL_TRACES_SAMPLER]?.trim().toLowerCase() ?? ; if (!sampler || sampler.startsWith(parentbased_)) { if (sampler.includes(always_off)) return false; return true; } return sampler always_on; }矩阵语义如下表OTEL_TRACES_SAMPLER配置是否强制 SAMPLED理由未设置默认是等同parentbased_always_on远端未采样父级会委托 AlwaysOff 导致整条子树被丢弃parentbased_*除 always_off是同上parentbased_traceidratio下合成根必须带 SAMPLED 否则零导出parentbased_always_off否尊重运维显式关闭采样的意图always_on是忽略父级 flagsSAMPLED 无害且让决策矩阵保持显式非 parentbased 采样器如traceidratio、always_off否每个 span 独立评估保留调用方 flags 由采样器逐 span 决策强制采样的实现是forceSampledUnderSamplerdaemon-tracing.ts若shouldForceSampled()为真就用trace.wrapSpanContext把提取出的 span context 的traceFlags或上TraceFlags.SAMPLED再包回 context。_meta路径应用相同的强制逻辑对于进程内 bridgedaemon → 子进程父级是我们自己的 span在该策略下本来就已 SAMPLED强制是 no-op而直接 ACP 客户端也可以附带_meta并携带调用方控制的sampled0——这与 HTTP 头一样属于外部输入获得同样的保护。两条入站边界HTTP 头、_meta的采样处理由此保持完全一致。日志关联telemetry 开与关都能按 traceId 找telemetry 开启span 前缀闭环daemon 日志行在活动记录 span 内写出时已携带[trace_id… span_id…]前缀——实现于getActiveTraceContextpackages/cli/src/serve/daemon-logger.tsfunction getActiveTraceContext(): DaemonTraceContext | undefined { try { const span trace.getActiveSpan(); if (!span?.isRecording()) return undefined; const spanContext span.spanContext(); if ( !isSpanContextValid(spanContext) || (spanContext.traceFlags TraceFlags.SAMPLED) 0 ) { return undefined; } return { traceId: spanContext.traceId, spanId: spanContext.spanId }; } catch { return undefined; } }注意两个守卫span 必须isRecording()且必须带SAMPLED标志——未采样 span 不会产出前缀。telemetry 开启时请求 span 父级到调用方 trace于是该请求的每一条daemon 日志包括访问日志的request completed都会被前缀以调用方的 trace id实现端到端闭环。访问日志的finish监听器与日志器走同一个 AsyncLocalStorage 传播读取活动 span因此两个finish监听器的注册顺序对前缀无影响。telemetry 关闭默认纯正则旁路telemetry 关闭默认状态时没有 span前缀永不触发。但设计保留了一条轻量路径让基于日志的关联在零遥测配置、零 trace 后端的情况下依然成立daemonInboundTraceIdCaptureMiddlewaretelemetry.ts在认证、限流、body 解析之前挂载用纯正则extractInboundTraceId解析traceparent把 traceId 存到响应对象上独立的 symboldaemonInboundTraceIdContextexport function daemonInboundTraceIdCaptureMiddleware( req: Request, res: Response, next: NextFunction, ): void { try { const inboundTraceId extractInboundTraceId(req.headers); if (inboundTraceId ! undefined) { (res as InboundTraceIdResponse)[daemonInboundTraceIdContext] inboundTraceId; } } catch { // Telemetry must not affect request handling. } next(); }早挂载的意义即使请求在认证层401、限流层429、body 解析400或路由未命中404处被短路访问日志行依然能关联到调用方 trace。extractInboundTraceIddaemon-tracing.ts是纯格式检查不依赖任何 OTel 机制const TRACEPARENT_RE /^\s?([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?\s?$/; const ALL_ZERO_TRACE_ID 0.repeat(32); const ALL_ZERO_SPAN_ID 0.repeat(16); export function extractInboundTraceId( headers: Recordstring, unknown | undefined, ): string | undefined { const traceparent headers?.[traceparent]; if (typeof traceparent ! string || traceparent.length 0) { return undefined; } const match TRACEPARENT_RE.exec(traceparent); if (!match) return undefined; const [, version, traceId, spanId, , trailing] match; // 版本 00 必须恰好四个字段更高版本可携带解析器忽略的尾部扩展字段——与 propagator 一致。 if (version 00 trailing ! undefined) return undefined; if (version ff) return undefined; if (traceId ALL_ZERO_TRACE_ID || spanId ALL_ZERO_SPAN_ID) { return undefined; } return traceId; }其接受规则与内置 W3C propagator 完全镜像——同样的 shape/全零/ff拒绝、单个可选首尾空白、版本00之上允许尾部扩展字段——保证一个头要么两条路径都加入要么都不加入。tracestate留在 propagator 路径因为日志行只需要一个合理的 trace id。随后访问日志的finish回调通过getDaemonTelemetryInboundTraceId读回并输出为 camelCase 的traceId字段packages/cli/src/serve/server/access-log.ts// telemetry 开启时daemon 请求 span 已经给本行打上了 trace 前缀 // 该字段覆盖 telemetry 关闭的部署——在那里它是 daemon 日志行与发送 // traceparent 头的调用方之间唯一的 traceId 链接。 const inboundTraceId getDaemonTelemetryInboundTraceId(res); const ctx { route: route.value, ...(sessionId ? { sessionId: sessionId.value, ... } : {}), ...(clientId ? { clientId: clientId.value, ... } : {}), ...(inboundTraceId ? { traceId: inboundTraceId } : {}), status, durationMs: Math.max(0, Math.round(monotonicNow() - startMs)), }; if (status 400) daemonLog.warn(request completed, ctx); else daemonLog.info(request completed, ctx);为什么 camelCase 而不是 snake_case设计文档明确解释了命名选择camelCase 的traceId字段与 logger 保留的 snake_casetrace_id前缀键刻意区分这样调用方无法伪造span 派生的前缀。两个细节只要解析到合法头camelCase 字段在两种 telemetry 模式下都会被捕获因此一种日志查询形态适用于所有部署telemetry 开启时 snake_case 的 span 前缀会冗余携带同一个 id。没有合法头时字段是省略而非空值...(inboundTraceId ? { traceId: inboundTraceId } : {})以及访问日志测试中expect(traceId in (context ?? {})).toBe(false)且每一步都 fail-closed。存储 symbol 的独立性daemonInboundTraceIdContext使用独立 symbol而不是复用 telemetry response contexttelemetry-context.ts被捕获的调用方 trace id 放在自己的 symbol 下telemetry response context 的存在同时充当 handler-resolved workspace 归属的 opt-in 门见setDaemonTelemetryWorkspace因此捕获 trace id 绝不能创建它——否则调用方仅仅发送一个 traceparent 头就会静默改变 span 归属。这是daemonInboundTraceIdCaptureMiddleware与daemonTelemetryMiddleware解耦、独立挂载的根因。同时telemetry-context.ts保持 import-light访问日志位于 serve fast-path 的 pre-listen 静态闭包内不能触及 telemetry 中间件的 core import 图。非目标明确边界不改 span 种类与属性既有qwen-code.daemon.requestspan 保持SpanKind.INTERNAL与相同属性存在合法头时仅改变父级链接。不做traceparent响应注入也不支持 W3Ctracingresponse。不新增采样配置面入站策略复用现有shouldForceSampled()矩阵SDK 自身的 sampler 仍是唯一采样权威。备选方案与取舍设计文档记录了被否决的两个方案把请求 span 改为SpanKind.SERVER符合 HTTP semconv这是一个已知缺口——qwen-code.daemon.request是 daemon 唯一的 SERVER 邻近 spanHttpInstrumentation从不 patch 服务端因为 SDK 懒加载依赖 SERVER span 推导服务拓扑/RED 指标的后端Tempo service-graph、ARMS将无法在调用方 trace 内把 daemon 识别为服务。但切换会改变每个既有 daemon.request span 的形态、可能移动后端分组故推迟为后续工作。设计文档明确承认这是当前实现的已知缺口。在 core 的withDaemonRequestSpan内直接从一个原始 header bag 提取被否决因为 core 的 request-span 选项目前是传输原语transport-primitive只有中间件知道载体是 HTTP 头。测试与验证设计文档与源码给出了完整的三层验证矩阵单元测试头提取覆盖 packages/core/src/telemetry/daemon-tracing.test.ts 与 packages/cli/src/serve/server/telemetry.test.ts有效 / 缺失 / 畸形 / 全零 id / 数组值 / 版本ff/ 版本00带扩展字段 / 未来版本01/ 入站tracestate通过withDaemonRequestSpan的请求 span 父级化sampled 决策矩阵默认强制、parentbased_always_off与traceidratio原样、_meta路径默认采样器下强制 / opt-out 下原样中间件透传key 存在 vs 省略、telemetry 关闭跳过、被拒绝头的 debug 日志类型级守卫parentContext保留在DaemonRequestSpanOptions上单元测试telemetry 关闭的日志关联extractInboundTraceId有效 / 缺失 / 畸形 / 全零 / 版本ff/ 数组值 / 未来版本以及 fresh-module 状态无需 propagator中间件捕获telemetry 关闭有/无头、telemetry 开启时与 span 前缀同时输出 camelCase 字段、handler 解析的 context 初始化不覆盖已存 id访问日志输出/省略traceId字段见 access-log.test.ts手工冒烟dry run设计文档给出了可复现的验证步骤以QWEN_TELEMETRY_OUTFILE启动serve用固定traceparent的curl打一个请求——导出的 span 必须共享头的 traceId 并以头的 spanId 为父不带头的对照请求必须留在自己的 trace 上telemetry 关闭时同样的 curl 必须仍输出带traceId字段与头一致的request completed日志。实战建议与排查速查结合源码与设计给读者几条直接可用的操作结论想让 daemon HTTP 请求接入自己的 trace作为调用方在请求中携带标准 W3Ctraceparent和可选的tracestate头即可qwen-code.daemon.requestspan 及其子树含经_meta传播的 session 子进程 span会自动归属到你的 trace 下。telemetry 开启时daemon 日志行的[trace_id… span_id…]前缀与访问日志request completed的traceId字段同时携带调用方 trace id日志与 trace 后端可互相跳转。telemetry 关闭时默认无需任何遥测配置与 trace 后端访问日志request completed的traceId字段就是日志侧唯一的 trace 关联点——一条日志查询形态通吃所有部署。排障如果头被拒绝daemon 日志会出现qwen-code.daemon.traceparent.invalid事件DEBUG 级、限速其中http.request.header.traceparent属性记录被拒绝的原始值已 sanitize、截断 128 字符可据此判断是版本ff、全零 id、格式错误还是其他原因。采样注意默认配置下服务端会强制 SAMPLED调用方的sampled0不会丢弃 daemon 遥测若使用parentbased_always_off则尊重关闭意图若使用traceidratio等非 parentbased 采样器服务端保留调用方 flags 由采样器逐 span 决策。已知缺口请求 span 目前仍是SpanKind.INTERNAL依赖 SERVER span 推导服务拓扑的后端如 Tempo service-graph、ARMS在调用方 trace 内识别不到 daemon 服务节点切换为 SERVER 被列为后续工作。关键文件索引设计文档docs/design/2026-08-18-daemon-http-inbound-trace-context.mdCore 提取与传播实现packages/core/src/telemetry/daemon-tracing.tsCore 采样决策矩阵packages/core/src/telemetry/tracer.tsCore 单元测试packages/core/src/telemetry/daemon-tracing.test.tsServe 遥测中间件packages/cli/src/serve/server/telemetry.ts中间件单元测试packages/cli/src/serve/server/telemetry.test.ts响应侧 symbol 与读取器packages/cli/src/serve/server/telemetry-context.ts访问日志traceId字段输出packages/cli/src/serve/server/access-log.ts访问日志测试packages/cli/src/serve/server/access-log.test.tsdaemon 日志trace_id前缀packages/cli/src/serve/daemon-logger.ts【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →