Skip to content

Commit 02a8256

Browse files
os-zhuangclaude
andauthored
fix(service-automation): connector 降级路径的两条日志改用结构化 meta (#5636) (#5662)
`degradeConnectorInstance`(#3017 的降级/重试路径)有两条记录报告的是外来失败, 却把它插进了日志 message —— 与 #5048(flow 绑定)、#5575(`fail()`)同一类,是那 两单范围之外的第三个接缝: • husk 注册失败(`warn`):`err` 来自 `registerDegradedConnector` → `ConnectorSchema.parse`,catch 注释自己写着「the entry's def no longer parses」,即预期接到的正是多行 `ZodError.message`(第一行只有一个 `[`)。 • 降级公告(`error`):文本是 `ConnectorUpstreamUnavailableError.message`,由 第三方 provider factory 构造(ADR-0097 鼓励第三方去写),spec 不约束其文本。 这条 `warn` 的下游与 #5575 的 `error` 不同,且是实测的:`warn` 走 stdout,`serve` 的启动静默窗口只包了 `process.stdout.write`,而冷启动的 `materializeDeclaredConnectors(ctx, { fatal: true })` 遇到上游不可达是降级不抛错, 所以它在窗口内就跑。`BootLogCapture.offer()` 只保留能被 `classifyBootLogLine` 找到 等级头的物理行 —— 对一份 13 行的插值 dump 实测:保留 1 行(止于 `[` 的头行)、丢弃 12 行,唯一留下的那行不含任何事实。即 cloud#971 的原始形态。 两条都复用同包 `describeThrownForLog`:message 单行自足,cause 走结构化 meta。位置 按 `Logger` 契约核实后区分 —— `warn(message, meta?)` 用第二参, `error(message, error?, meta?)` 用第三参。 `degradedReason`(`GET /connectors` 与 `connector_action` 被拒时的文本)刻意逐字不变: 它是人透过 JSON 读的字段。因此调用点同时传 `reason`(那段文本)与 `cause`(抛出值), 测试双向钉住这个分离。 Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa25a81 commit 02a8256

3 files changed

Lines changed: 512 additions & 2 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): connector 降级路径的两条日志改用结构化 `meta`,message 保持单行 (#5636)
6+
7+
## 接缝
8+
9+
`degradeConnectorInstance`(#3017 的降级/重试路径)有两条记录报告的是**外来**失败,却把
10+
它插进了日志 message —— 与 #5048(flow 绑定)、#5575(`reconcileDeclaredConnectors`
11+
`fail()`)同一类,是那两单范围之外的第三个接缝:
12+
13+
- **husk 注册失败**(`warn`):`err` 来自 `engine.registerDegradedConnector`
14+
`ConnectorSchema.parse`,catch 自己的注释就写着「the entry's def no longer parses」,
15+
也就是说这里预期接到的正是 `ZodError` —— 它的 `.message` 是 issue 数组的多行 JSON
16+
dump,第一行只有一个 `[`
17+
- **降级公告**(`error`):文本是 `ConnectorUpstreamUnavailableError.message`,由第三方
18+
provider factory 构造(ADR-0097 明确鼓励第三方去写)。spec 只定义错误类、不约束文本,
19+
所以上游 SDK 的多行失败会原样落在这里。
20+
21+
## 危害:这条 `warn` 的下游与 #5575`error` 不同(实测)
22+
23+
`ObjectLogger``warn` 送 stdout、`error`/`fatal` 送 stderr,而 `serve` 的启动静默窗口
24+
只包了 `process.stdout.write`#5575 的接缝全是 `error`,所以那一单的结论是「启动缓冲根本
25+
看不到」;这一条不同,而且差别是**测出来**的,不是推的:
26+
27+
- 它是 `warn` → stdout,缓冲**确实**看得到;
28+
- 它在**冷启动**就会跑 —— `materializeDeclaredConnectors(ctx, { fatal: true })` 遇到上游
29+
不可达是降级、不是抛错 —— 而窗口此时正开着(`serve` 在 config 加载前接管 stdout,直到
30+
banner 打印才恢复);
31+
- `BootLogCapture.offer()` 只在 `classifyBootLogLine` 能在该物理行上找到 `<ts> <LEVEL>`
32+
头时才保留它,所以插值 dump 的每一条续行是被**直接丢弃**,不只是难解析。
33+
34+
对一份 13 行的插值 ZodError 实测:写出 13 行物理行,缓冲保留 **1** 行(那条止于 Zod `[`
35+
的头行)、丢弃 **12** 行 —— 唯一被留下的那行不含任何事实。这正是 cloud#971 的原始形态。
36+
`error` 那一条走 stderr,不经缓冲,危害是 #5575 那一串按行消费者(文件 sink、
37+
`docker logs`/journald 送采集、`grep ERROR`):一条诊断散成 N 个无法归属的碎片。
38+
39+
## 改法
40+
41+
两条都复用同包 `thrown-cause-diagnostics.ts``describeThrownForLog`(#5572/#5575 落地):
42+
message 是不含换行的自足句子,cause 走 logger 的结构化 meta。位置按 `Logger` 契约区分,
43+
并且是核对源码后确认的而非照抄:`warn(message, meta?)` 没有 `Error` 位,cause 就在**第二**
44+
参;`error(message, error?, meta?)` 的 cause 在**第三**参(第二参塞原始 error 会让每次重试
45+
的记录都附带完整堆栈)。
46+
47+
## 刻意没有改的一件事
48+
49+
`degradedReason` —— `GET /connectors` 展示的、以及 `connector_action` 被拒时引用的那段文本
50+
—— 仍然逐字保留 provider 自己的 message,包含换行。它是人透过 JSON 读的字段,不经按行切分
51+
的消费者;重塑它属于另一次契约变更。因此调用点同时传 `reason`(那段文本)与 `cause`(抛出值
52+
本身):前者喂 husk 与重试簿记,后者只喂日志记录。测试双向钉住了这个分离。

0 commit comments

Comments
 (0)