Skip to content

fix(diagnostics): 全 CLI 保留错误原因并完善 --debug 与跨进程诊断 #138

Closed

Description

@jo-hnny

问题与影响

用户执行 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 与退出码)允许改变普通模式输出,每项有独立测试断言新行为,并在 PR Release 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、JSON Details、tunnel 日志文件全文及其摘录均已脱敏。
  • debug 下父进程错误链只出现一次,日志摘录有独立来源标注。

第 3 步(每个边界 PR 各自满足):

  • 本地故障注入覆盖该边界的 DNS 失败 / 连接拒绝或超时 / 本地文件读取失败 / 远端非零退出中适用的场景,断言阶段与建议符合实际观察。
  • 覆盖 stable/preview 共用执行路径;新增 Details 字段已写入 schema/explain 文档,并列明本地与真实环境验证结果。

相关历史与范围区别

已检查 OPEN/CLOSED Issue 和相关 PR,未发现覆盖上述完整缺口的现有跟踪项。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions