可观测性与调试
概述
qwen serve 目前内置了 OpenTelemetry span 插桩、结构化文件日志(DaemonLogger)、按请求的访问日志、调试 stderr 日志、结构化预检单元(preflight cells)以及内存中的权限审计环(permission audit ring)。本页是关于当前可观测性能力以及排查问题时需注意的盲区的实用指南。
当前已有的能力
| 观测点 | 位置 | 用途 |
|---|---|---|
QWEN_SERVE_DEBUG stderr 日志 | bridge.ts 及调用点 | 环境变量值为 1 / true / on / yes(不区分大小写)时,会将 qwen serve debug: ... 行打印到 stderr。 |
| OpenTelemetry span 插桩 | server.ts daemonTelemetryMiddleware | 到达遥测中间件的已分类守护进程 API 请求被包裹在 withDaemonRequestSpan 中;属性包括规范 route、解析后的 workspace hash、sessionId、clientId 和状态码。权限路由有专属的 span。Prompt 生命周期被端到端追踪。配置位于 settings.json 的 telemetry 中。 |
| OpenTelemetry daemon 性能指标 | telemetry/*event-loop-lag*, daemon-metrics | daemon 和 ACP 子进程的事件循环延迟 gauge,以及 daemon 与子进程管道消息字节数的 histogram。 |
DaemonLogger 结构化文件日志 | serve/daemon-logger.ts | 追加到稳定的、按大小轮转的 daemon.log。在活跃、正在录制且已采样的 OTel span 下发出的 Caller info / warn / error 记录包含 trace_id 和 span_id;文件记录还包含 runId 和 PID。启动时打印选定的稳定/回退路径;完整状态暴露健康状态、问题和文件拷贝丢失计数器。 |
| 按请求的访问日志中间件 | server/access-log.ts | 在每个请求后记录 method/path、status、duration、session 和第一个原始 client ID。60-token 突发 / 每秒 2 个的桶将超额流量聚合为五个固定状态计数器。保留 health、heartbeat 和成功的 SSE 排除。 |
/health | server.ts 路由 | 存活探针;?deep=1 返回扩展详情。 |
/capabilities | server.ts 路由 | 预检功能发现。参见 11-capabilities-versioning.md。 |
/workspace/preflight | 路由 -> DaemonStatusProvider | 结构化就绪单元(readiness cells):Node 版本、CLI 入口、ripgrep、git、npm,以及子进程存活后的 ACP 级别单元。 |
/workspace/env | 路由 -> DaemonStatusProvider | Daemon 进程环境变量快照。Secret 环境变量仅报告其存在;代理 URL 凭据会被剥离。 |
/workspace/mcp | 路由 -> bridge extMethod | 池、预算和拒绝快照。 |
/workspace/skills, /workspace/providers | 路由 | ACP 侧的实时快照;当不存在 session 时返回空的空闲数据。 |
| 按 session 的 SSE | GET /session/:id/events | 实时事件流。 |
| Web Shell UI | GET / (packages/cli/src/serve/web-shell-static.ts) | 从打包的 Web Shell 资源提供的浏览器 UI:聊天、会话列表、工作区检查器和权限 UX。在环回地址上,http://127.0.0.1:4170/ 是无需编写 SDK 代码即可进行最快端到端验证的路径。注册规则在 02-serve-runtime.md 中。 |
PermissionAuditRing | permission-audit.ts | 包含 512 个权限决策的内存 FIFO 队列。 |
Mediator decisionReason 审计 | permissionMediator.ts | 内部结构化记录,解释权限请求为何以该方式解析。 |
当前尚不支持的能力
- 无 Prometheus / metrics 端点。 OTel 指标可以导出,但 daemon 不暴露 Prometheus 抓取端点。
PermissionAuditRing无外部审计 sink。 环存在,但未连接向 SIEM 或外部存储扇出的 hook。
调试指南
1. daemon 是否存活?
curl -s http://127.0.0.1:4170/health
# {"status":"ok"}
curl -s 'http://127.0.0.1:4170/health?deep=1' | jq
# {"status":"ok","workspaceCount":N,"sessions":N,...}Deep health 汇总所有受管工作区运行时,包括仍在 drain 中的运行时。它是一个信息性的计数器快照,而非每个工作区的就绪状态;当需要单个工作区或 transport 诊断时,请使用 /daemon/status。
在环回地址上收到 401 意味着可能启用了 --require-auth。在启动时使用 QWEN_SERVE_DEBUG=1 以查看启动日志。
2. 宣告了哪些功能?
curl -s http://127.0.0.1:4170/capabilities | jq检查 mcp_workspace_pool(F2 pool 是否开启?)、require_auth(是否加固?)、permission_mediation.modes(支持的策略)以及 policy.permission(当前生效的策略)。
3. daemon 宿主就绪状态是否健康?
curl -s http://127.0.0.1:4170/workspace/preflight | jqstatus: 'not_started' 的单元是 ACP 级别的,仅在第一个 session 附加后才会填充。status: 'fail' 的单元包含一个闭合的 errorKind;请根据 18-error-taxonomy.md 渲染结构化修复方案。
4. 实时查看 session SSE 流
curl -N -H 'Accept: text/event-stream' \
-H 'Authorization: Bearer XYZ' \
-H 'X-Qwen-Client-Id: debug-tail' \
-H 'Last-Event-ID: 0' \
'http://127.0.0.1:4170/session/<sid>/events'-N 禁用 curl 输出缓冲。Last-Event-ID: 0 请求重放 id > 0 的环事件。
5. 为什么权限请求会这样解析?
PermissionAuditRing 存在于内存中,目前没有 HTTP 接口。启用 QWEN_SERVE_DEBUG=1 并复现问题;mediator 会为每次投票和决策打印结构化日志,包括 decisionReason.type。后续的 PR 可以通过 HTTP 暴露该环。
6. 哪个消费者较慢?
当队列达到 75% 时,每次溢出事件会触发一次 slow_client_warning。订阅 session SSE 流并查找合成帧;payload 包含 queueSize、maxQueued 和 lastEventId。重复的警告表明消费者卡住,通常是阻塞的 SDK for await 循环。
7. 为什么 MCP server 被拒绝?
结合 /workspace/mcp 中每个单元的 disabledReason: 'budget'、refusedServerNames 列表以及 mcp_child_refused_batch SSE 事件。将它们与 /capabilities 中的 mcp_guardrails.modes(enforce 是否激活?)以及通过 getReservedSlots() 可见的实时 --mcp-client-budget 状态进行比较。
8. daemon 无法关闭
第一个信号触发优雅关闭(参见 02-serve-runtime.md)。如果挂起超过 10 秒,请检查:
- ACP 子进程未响应优雅关闭。
- 长 SSE 连接导致 HTTP
server.close()在SHUTDOWN_FORCE_CLOSE_MS(5 秒)后仍处于打开状态。
第二个 SIGTERM/SIGINT 会故意触发 bridge.killAllSync() + process.exit(1)。
9. daemon 事件循环、prompt 队列或 ACP 管道是否过载?
当生产环境 daemon 运行时注入性能快照 provider 时,GET /daemon/status 可能包含 runtime.perf:
{
"runtime": {
"perf": {
"eventLoop": { "meanMs": 1.2, "p50Ms": 1.0, "p99Ms": 9.5, "maxMs": 25 },
"promptQueueWait": {
"count": 3,
"meanMs": 12.5,
"maxMs": 35,
"lastMs": 4
},
"pipe": {
"inbound": { "count": 42, "totalBytes": 100000, "maxBytes": 12000 },
"outbound": { "count": 41, "totalBytes": 90000, "maxBytes": 11000 }
}
}
}
}状态 payload 仅限 daemon。promptQueueWait 汇总了在 daemon 进程中观察到的 prompt FIFO 队列等待样本。ACP 子进程事件循环延迟故意不聚合到 /daemon/status 中;它可以通过 OTel gauge qwen-code.acp.event_loop.lag 以及转发到 daemon 日志的 stderr 停顿行来查看。
10. 文件日志是否降级或丢失了记录?
使用完整的 daemon 状态:
curl -s 'http://127.0.0.1:4170/daemon/status?detail=full' | \
jq '{status, issues, daemon: {runId: .daemon.runId, logMode: .daemon.logMode, logHealth: .daemon.logHealth, logPath: .daemon.logPath, logIssues: .daemon.logIssues, droppedRecords: .daemon.logDroppedRecords, droppedBytes: .daemon.logDroppedBytes}}'stable 是正常的所有者,fallback 表示另一个 daemon 拥有该稳定系列,stderr-only 表示文件日志被禁用或不可用。fallback/ok 在有意的并发情况下是预期行为。daemon_log_degraded 警告不包含路径;请求完整详情以获取实际路径和 logger 问题代码。使用 runId 区分稳定文件内的重启。
新的 OTel 指标名称:
qwen-code.daemon.event_loop.lag,以毫秒为单位的 gauge,包含stat=mean|p50|p99|max。qwen-code.acp.event_loop.lag,以毫秒为单位的 gauge,包含stat=mean|p50|p99|max。qwen-code.daemon.prompt.queue_wait,以毫秒为单位的 histogram。qwen-code.daemon.pipe.message_bytes,以字节为单位的 histogram,包含direction=inbound|outbound。
11. daemon 是否处于内存压力下?
curl -s 'http://127.0.0.1:4170/daemon/status' | \
jq '.runtime.memory.pressure'level 为 normal / soft / hard / critical,根据 ratio 分类——取 rssRatio(RSS 相对于检测到的 cgroup/主机内存,即 OOM killer 监控的指标)和 heapRatio(V8 堆已用相对于此进程的 heap_size_limit——整个堆,而不仅是 --max-old-space-size 命名的老年代)中更严重的一个。source 表示是哪一个产生的。在采取行动前请检查 source:unknown 表示 daemon 无法测量两侧的值,因此此处的 normal 表示缺少读数,而非健康的证据。只有当分子和分母都可用时才会报告对应的一侧,因此 source 也能区分零值 rssBytes / heapUsedBytes 和真实值。
rssRatio 的准确性取决于其分母,而 limits.memory.availableMemorySource 决定其可信度。 在 cgroup 下(constrained)它正是 OOM killer 执行的限制,因此 ratio 含义明确。在裸机上(host)它是整台机器的大小,而 daemon 实际上在_机器_内存耗尽时才会死亡——这取决于机器上的所有其他进程。一个在 64 GB 主机上占用 20% 的 daemon,旁边有一个 55 GB 的邻居,会一直报告 level: normal, source: rss 直到被杀死。在 source: 'host' 下,将 rssRatio 视为实际压力的下界。这与阈值未校准是两个问题:没有任何阈值选择能修正测量对象错误的分母。
还有两点不覆盖的内容。它仅监控 daemon 根进程,因此如果 qwen --acp 子进程才是增长的,daemon 可能全程报告 normal——请同时查看 runtime.memory.children,它汇总了活跃子进程自身的 RSS(并通过 sampled 说明实际有多少子进程报告了数据)。而且没有任何自动修复措施:离开 normal 会触发 daemon_memory_pressure 警告,但不会改变任何行为。
在 --memory-pressure-mode off 下,上述所有数据仍会被报告,但不会触发 issue,因此顶层 status 保持原本的值。在校准阈值与实际工作负载时使用 off,或者如果你根据 status 进行告警且不希望未校准的信号影响它。
流程
典型排查流程
状态与生命周期
QWEN_SERVE_DEBUG在每次检查时通过debug-mode.ts中的isServeDebugMode()读取;切换它不需要重启。除非在启动时设置了该环境变量,否则无法获取启动日志。PermissionAuditRing限制为 512 个 FIFO 条目;较旧的记录会被静默丢弃。DaemonStatusProvider按请求重建单元且不做缓存;避免不必要的高频轮询。
依赖
- 使用
process.stderr.write进行调试 stderr 输出。 - 使用
DaemonLogger生成结构化文件日志。 - 通过
initializeTelemetry和createDaemonBridgeTelemetry使用 OpenTelemetry SDK。 - 使用
node:perf_hooks.monitorEventLoopDelay监控 daemon 和 ACP 的事件循环延迟指标。 - 使用
node:process检查环境变量和信号。
配置
| 配置项 | 作用 |
|---|---|
QWEN_SERVE_DEBUG | 启用详细的 stderr 日志。请参阅 17-configuration.md。 |
settings.json telemetry | 控制 OTel 行为:enabled、otlpEndpoint、otlpProtocol 以及各信号的端点。 |
DaemonLogger 日志路径 | 稳定的 debug/daemon/daemon.log,或在启动时选择的特定于运行的回退路径。 |
PermissionAuditRing 大小 | 目前硬编码为 512。 |
slow_client_warning 阈值 | 0.75 / 0.375,在 eventBus.ts 中硬编码。 |
注意事项与已知限制
- DaemonLogger 文件日志是结构化文本,其
trace_id、span_id、route、sessionId和clientId字段可以通过正则表达式搜索或提取。Callerinfo/warn/error记录仅在日志调用在活跃、正在录制且已采样的 OTel span 下运行时才包含 trace 字段。raw和启动记录、文件丢弃摘要以及访问日志抑制摘要故意省略这些字段。关联是尽力而为的:导出器故障可能导致已采样的 trace 在后端不可用。这些高基数标识符用于诊断查找,而非指标标签或聚合。QWEN_SERVE_DEBUG的 stderr 日志仍为非结构化文本。 - 已接受的 prompt、continuation 和 cancellation 变更具有生命周期日志。
prompt enqueued、continuation enqueued和cancel sent包含sessionId、适用时的promptId以及提供时的clientId;prompt 内容不会被记录。为每个独立的控制器使用不同的稳定 client ID。故意共享 ID 的控制器在这些记录中是无法区分的。 - DaemonLogger 保留策略基于大小,而非基于时间。 活动文件和四个归档按每个系列限制;活跃的 fallback 所有者永远不会被删除。
- 访问摘要有意的丢失统计。 WARN 级别的
access logs suppressed表示从 stderr 和文件中省略的单独访问记录;它不表示丢弃的 HTTP 请求。 - 外部 logrotate 不得修改活动系列。 使用读取/拷贝并在替换后重新打开稳定路径名的发送器。
- OpenTelemetry span 包含按请求关联的信息。 通过 bearer 认证、速率限制和 body 解析的分类守护进程 API 请求携带规范 route、sessionId、clientId 以及(唯一解析时)
qwen-code.workspace.hash属性。被较早中间件门控拒绝的请求没有这些请求 span。 - HTTP 指标是守护进程全局的。 OpenTelemetry HTTP 请求指标和 Web Shell 状态指标环不包含工作区维度。成功的 session SSE 连接有请求 span,但不包含在普通请求计数/持续时间指标中,因为其生命周期不是请求延迟;失败的 SSE 握手正常计数。
runtime.perf仅适用于 daemon。 设计上不在此处报告子进程的事件循环延迟;对于 ACP 子进程卡顿,请使用 OTel 或转发的 stderr 卡顿警告。- ACP 级别的
/workspace/preflight单元格需要活跃会话。 在空闲的 daemon 上,auth / MCP / skills / providers 可能会显示status: 'not_started';这是预期行为。 /workspace/env仅报告 secret 是否存在,不报告具体值。 如果 secret 的存在本身属于敏感信息,请勿暴露该响应。- 审计环是进程本地的,daemon 重启时历史记录会丢失。
- 此处未记录负载测试方案。 性能基线位于
test/perf-daemon-baseline分支。
参考资料
packages/cli/src/serve/daemon-status-provider.tspackages/cli/src/serve/daemon-logger.ts(DaemonLogger、buildDaemonLogLine)packages/cli/src/serve/debug-mode.ts(isServeDebugMode)packages/acp-bridge/src/permissionMediator.ts(PermissionDecisionReason)packages/cli/src/serve/server.ts(daemonTelemetryMiddleware、access-log 中间件)- 配置:
17-configuration.md - 错误分类:
18-error-taxonomy.md - 用户操作指南:
../../users/qwen-serve.md