Repository navigation
fix(diagnostics): 全 CLI 保留错误原因并完善 --debug 与跨进程诊断 #138
Description
问题与影响
用户执行 agr --debug --output json instance mobile connect <instance-id> 失败时,得到 Failure.Code=INTERNAL_ERROR、Message=internal error;debug 仅显示命令和配置摘要,没有底层原因、失败阶段或日志位置。用户只能通过额外网络工具和后台日志继续定位。
更严重的是,未分类错误的兜底 Hint 本身写着 rerun with --debug and share stderr diagnostics,但当前 --debug 在 JSON 和 text 模式下都拿不到原始原因——CLI 给出的排查建议实际不可用。
这是全 CLI 错误可诊断性问题,mobile connect 只是已观察到的入口。
期望:普通模式说明什么操作、哪一步失败及下一步如何检查;debug 模式提供脱敏后的错误链和必要上下文。即使错误尚未分类,也不能在 debug 中无声消失。
已确认机制与证据边界
以下来自当前 main 的源码检查;用户报告证明了 mobile connect 的可观察症状。尚未进行故障注入测试,客户二进制版本未确认。
- JSON 模式:命令包装层收到错误后先分类,写出失败 envelope 并返回
envelopeAlreadyWritten标记;根命令据此直接退出,跳过Debug: error=...打印。 - text 模式:包装层返回的已是分类后的
CLIError,该类型只有Failure/ExitCode,不保留 cause、不支持Unwrap,debug 只能打印出internal error。 - mobile connect 后台 tunnel:就绪协议只传
{"Status":"error","Message":"..."}字符串,父进程用普通fmt.Errorf重建,最终落入INTERNAL_ERROR;就绪失败时不返回日志路径(只有成功路径返回LogPath;日志文件创建本身允许失败,失败时路径为空、命令继续);父进程不向子进程传递--debug。子进程提前退出、就绪消息无法解析、30s 内未就绪这三类失败由父进程自己观察到,此时没有子进程 failure。 - 命令自带的错误压平:
instance mobile tunnel和instance login各有一个本地exitError,把任意 error 直接构造为Code=CLI_ERROR的CLIError,Kind 只由退出码反推,不经过统一分类器、不保留 cause。因此 tunnel 子进程当前并不分类错误;例如探测失败在前台agr instance mobile tunnel中一律表现为Kind=usage、退出码 2。探测包含两步:先通过云 API 获取 token(可能是鉴权错误),再做 WebSocket 握手(可能超时、取消或被拒绝),原因本不相同。 - NDJSON 流式命令:
instance exec、instance code run的 NDJSON 路径在命令内部分类错误、写出failed事件后返回StreamDone且 error 为 nil,原始 error 在命令内部即被丢弃;仅修改CLIError或根命令出口无法恢复。远端程序正常返回非零退出码属于业务结果,不涉及 error。 - WebSocket 探测:握手时丢弃了 HTTP response,拒绝状态码等信息丢失。
- 云 API 已满足:TencentCloud SDK 错误已保留原始 Code/Message/RequestId,本 Issue 对该边界只需补回归测试防退化,不需要重新实现。
- 客户端只能报告自身观察到的连接阶段、超时或 EOF,不能据此推断远端代理内部原因。
解决方案
分三步交付,每步可独立 review 和发布;第 1 步单独上线即可解决"--debug 看不到原因"的核心问题。
1. 保留 cause,并在唯一出口打印 debug 错误链(首个 PR)
CLIError增加可选 cause 并实现Unwrap;分类时保留原始错误,优先按类型及errors.Is/As分类,避免靠字符串猜测。- 统一分类器目前只用类型断言识别
CLIError:被外层fmt.Errorf("...: %w", cliErr)包装的CLIError会丢失外层操作上下文;增加Unwrap后,若其 cause 含 SDK error,还会被重新识别为云 API 错误并覆盖已确定的分类。分类器应先用errors.As识别已分类的CLIError并保留其分类,同时保留外层上下文供 debug 输出。 - 脱敏与 cause 恢复在同一个 PR 中交付,避免中间版本在 debug 下输出未脱敏的原始错误链。
envelopeAlreadyWritten携带原始 cause;debug 错误链只在根命令的错误出口打印一次,写 stderr,不改变 stdout 的 envelope / NDJSON 协议。以结构性收口保证覆盖和"只输出一次",而不是维护一份人工审计清单。- 移除
instance mobile tunnel、instance login中的本地exitError,改为保留 cause 并走统一分类,不再统一压成 usage,也不改为统一压成 network:token 获取的云 API 鉴权错误、超时、取消保留各自语义;只有统一分类器无法识别的握手失败(如非 101 响应)才由 tunnel 补充网络类信息;前台探测保留握手 HTTP 状态码(不默认输出原始响应正文)在本步完成。如有意用 usage 表达特定前置条件不满足,需拆出该条件单独返回 usage 错误。 - 流式错误传递:
instance exec、instance code run的 NDJSON 失败路径让流式完成结果携带原始 cause,由根命令出口在 debug 下打印错误链;failed事件仍只由命令写一次,出口不再重复写。远端程序正常非零退出保持既有业务语义。 - 对其余绕过公共出口、自行输出结果的路径(
Handled、StreamDone)逐一确认是否也经过该出口;不经过的修复或写明理由。 CLIError支持Unwrap会改变现有errors.Is/As的判断结果(如context.DeadlineExceeded),需逐一核对现有调用点行为不变。- 未知错误允许保留
INTERNAL_ERROR,但 debug 下必须带脱敏后的错误链。不新增默认堆栈转储或日志上传机制。
2. 修复后台 tunnel 诊断传递(跨进程回归场景)
- 就绪协议传结构化 failure(Code/Kind/Message/Details):收到合法的子进程 failure 时,父进程原样保留,不再折叠为普通字符串错误(父子进程为同一可执行文件,无需兼容旧格式);没有有效子进程 failure 时(提前退出、协议损坏、就绪超时、启动失败),由父进程按实际观察到的启动/协议/退出/超时阶段分类,不伪造子进程原因。
- 仅在日志文件成功创建时,就绪失败结果在
Failure.Details中返回LogPath;debug 下附带该日志的末尾若干行(限长、脱敏),不另开诊断通道。日志创建失败时省略LogPath,不影响错误本身的分类与输出。 - 父进程显式向子进程传递
--debug。子进程 stderr 固定写入持久化的 tunnel 日志,因此 debug 输出必须在写入日志前脱敏,而不只是在摘录时脱敏(父进程通过环境变量向子进程传递凭据)。 - 父进程自身的错误链只打印一次;日志摘录单独标注来源(如
tunnel log tail:),其中出现的子进程错误文本不视为重复输出。 - 第 1 步已采集的握手 HTTP 状态码等
Details随结构化 failure 从子进程传回父进程,不在传递中丢失。 - 覆盖子进程提前退出、无就绪消息、协议损坏和就绪超时,保证终止与清理有界。
3. 按调用边界补齐上下文(按边界逐个 PR)
| 边界 | 需要保留的信息 |
|---|---|
| HTTP/WebSocket | 目标、已观察到的阶段、超时预算、HTTP 状态及白名单请求 ID |
| 子进程 | 程序名、启动/就绪/退出阶段、退出码、脱敏且限长的诊断 |
| 本地配置/文件 | 操作、必要路径或字段名、权限/不存在/解析失败等原因,不输出敏感值 |
| 远程执行 | 区分连接/启动失败与远端程序非零退出,保留既有业务结果语义 |
复用现有 Failure.Details,按需增加 Stage、HTTPStatus、TimeoutMs、LogPath 等可选字段;无证据则省略,不填猜测值。业务命令只补操作和步骤上下文,不各写一套分类器。阶段只报告真实观察到的位置:只知道 WebSocket 握手整体失败就报告握手阶段,不根据耗时猜测 TCP/TLS,本 Issue 不引入连接级 trace。
脱敏与兼容性
- 脱敏范围限定为可验证的规则:当前生效配置中的 SecretId/SecretKey/Token 值精确替换;
Authorization/Cookie等请求头;URL 中的签名 query 参数与代理密码。适用于 debug stderr、tunnel 日志文件(写入时)、日志摘录和Details,诊断限长。 - 保持 agr.v1 envelope、stdout/stderr 分工和退出码体系。新增的
Details字段和将INTERNAL_ERROR细化为新错误码均属公开可观察行为变化:需同步 schema/explain/排查文档,PR 按实际影响填写Release impact;已有稳定错误码不重命名。 - 不把网络可重试等同于业务操作可安全重放;不引入自动重试。
- 本 Issue 不包含代理功能实现、远端证书生成修复或全量 HTTP 抓包。
验收标准
第 1 步:
- 注入一个未专门分类的 wrapped error:text、JSON、NDJSON 流式模式下,
--debug的 stderr 均包含脱敏后的原始原因且只出现一次;关闭 debug 时,未知错误的默认展示与现状一致。 - 本步明确列出的分类修复(移除
CLI_ERROR、tunnel 探测失败的 Kind 与退出码)允许改变普通模式输出,每项有独立测试断言新行为,并在 PRRelease impact中说明。 - JSON stdout 仍为单一可解析 envelope;NDJSON 流保持既有格式、终止语义和退出码,
failed事件只出现一次;远端程序正常非零退出的输出与现状一致。 - 结构性测试:任一经公共包装层返回 error 的命令,debug 下都会输出错误链(新增命令无需额外登记即被覆盖);绕过公共出口的路径已修复或有书面理由。
- 云 API 错误经包装后 Code/Message/RequestId 不丢失;无服务端 RequestId 时不伪造。
- 对外层包含操作上下文的 wrapped
CLIError,保留既有 Code、Kind、Details、ExitCode,debug 同时包含外层上下文与底层原因;cause 包含 SDK error 时,不覆盖已确定的分类(如自定义 Code 不被云 API 分类改写)。 - 本步新增的 debug 错误链输出在同一 PR 内已脱敏,用测试凭证和签名 URL 验证。
-
instance mobile tunnel、instance login不再产生CLI_ERROR;前台 tunnel 探测中,token 获取鉴权失败返回 auth 类;握手超时、取消保留统一分类器给出的 Kind,不被改写为 usage 或统一压成 network;握手非 101 响应返回网络类并带 HTTP 状态码。各自带对应退出码,debug 下包含原始原因。 - 现有
errors.Is/As调用点已核对,增加Unwrap后行为不变(有测试覆盖超时/取消判断)。
第 2 步:
- 通过真实父子进程路径验证:子进程报告就绪错误时,父进程输出的 Code/Kind 与子进程 failure 一致而非
INTERNAL_ERROR;子进程提前退出、协议损坏、就绪超时时,父进程按对应阶段分类且不伪造子进程原因。各情况均无重复输出、无进程泄漏;关键用例在旧实现上失败。 - 日志文件创建成功时失败结果包含
LogPath;模拟日志创建失败时省略LogPath,错误分类与输出不受影响。 - 子进程探测遇到 WebSocket 非 101 响应时,父进程输出的 failure 仍包含第 1 步采集的 HTTP 状态码。
- 用测试凭证、代理密码和签名 URL,在
--debug下验证 debug stderr、JSONDetails、tunnel 日志文件全文及其摘录均已脱敏。 - debug 下父进程错误链只出现一次,日志摘录有独立来源标注。
第 3 步(每个边界 PR 各自满足):
- 本地故障注入覆盖该边界的 DNS 失败 / 连接拒绝或超时 / 本地文件读取失败 / 远端非零退出中适用的场景,断言阶段与建议符合实际观察。
- 覆盖 stable/preview 共用执行路径;新增
Details字段已写入 schema/explain 文档,并列明本地与真实环境验证结果。
相关历史与范围区别
- feat(error): unified error classifier with inline fix suggestions #77 / feat: unified error classifier with inline fix suggestions #81 已完成统一分类和修复建议;本 Issue 补充分类前原因保留、debug 出口完整性及跨调用边界上下文。
- fix: send tunnel daemon error messages via stdout instead of stderr #16 已修复 daemon 错误消息写错通道;本 Issue 进一步解决字符串协议丢类型、父进程再次折叠及诊断转发。
- fix(deployment): harden proxy token and websocket handling #136 调整的是 deployment proxy 的 WebSocket 处理,与 mobile tunnel 无代码重叠;第 2、3 步处理 WebSocket 诊断时可参考其做法,保持一致。
已检查 OPEN/CLOSED Issue 和相关 PR,未发现覆盖上述完整缺口的现有跟踪项。
Activity