Skip to content

Commit dadf542

Browse files
os-zhuangclaude
andauthored
fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (#5661) (#5738)
* fix(service-automation): route the three startup seams' causes to structured meta (#5661) `registerRunObject` (warn), the boot `probe()` (error) and the wait-timer re-arm (error) each interpolated a FOREIGN error's text into the log MESSAGE. They are the fourth instalment of the family #5048 / #5575 / #5636 closed, and were out of all three scopes. `ObjectLogger.write()` emits one timestamp+level record head per call, so a message carrying newlines becomes several physical lines of which only the first is attributable. The two `error` seams exist precisely to be read — #4632 raised them so an operator could find them — and the `warn` one runs in `init()`, inside `serve`'s boot-quiet window, where `BootLogCapture.offer()` DROPS a head-less line rather than merely mangling it. All three now log a newline-free, self-sufficient message and hand the cause to `describeThrownForLog`, in the slot the `Logger` contract names: `warn`'s second argument, `error`'s third. #4632's required consequence-and-fix text stays in the first line; only the trailing interpolation is replaced by a pointer to the meta. `check:durability-log-level` stays green (24 seams; neither `error` demoted). Tests: new `plugin-startup-log-cause.test.ts` (13 cases) reads real bytes off a real `ObjectLogger` per the #5662 precedent, pinning per seam that a multi-line cause reaches meta and not the message, the argument slot, and that a cause-less boot writes zero bytes; two closing cases render the old and new shapes side by side and measure the difference. The #4420 probe case in `plugin-suspended-run-wiring.test.ts` is re-judged rather than re-spelled: it now asserts both directions, since a message-only assertion would also pass if the cause were dropped entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK * test(service-automation): drop the new fake engine's unused delete so check:engine-double-contract passes (#5661) `check:engine-double-contract` (a lint-job gate, not eslint) pinned the new `plugin-startup-log-cause.test.ts`: its `fakeDataEngine` declared a bare `async delete() { return true; }`, and an engine double whose delete does not route through `assertEngineDeleteDispatch` may accept a call the real `ObjectQL.delete` refuses — how #4434 shipped a dead REST route with a green suite. Removed the method instead of pinning it, and the choice is measured to the ledger's own standard rather than assumed. An `appendFileSync` marker as the first statement of that `delete` printed 0 times over this file's suite; the control — the same injection in `find` — printed 11 times in the same run, so the silence is evidence, not a broken probe. These seams drive `probe()`, `list()` and the boot flow pull only; nothing deletes a suspended run. `SuspendedRunStoreEngine` declares `delete?` optional, so omitting it is the faithful shape. Pinning would have certified a method that models nothing, and whose body was loose in a second way the pin does not address: it answered success while removing no row. A future case that needs deletion writes a real one and the gate requires the predicate then; the store also warns when the engine has no `delete()`, which the byte-exact assertions here would surface at once. The reasoning is recorded on the fixture so the next reader does not re-derive it. check:engine-double-contract: OK — 27 pinned, 65 in the DEBT ledger, 1 exempt (the shrink-only baseline is untouched — no entry added, none lowered). Package suite: 63 files / 754 tests passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK --------- Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 288e5a4 commit dadf542

4 files changed

Lines changed: 690 additions & 8 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation): 启动路径三条日志改用结构化 `meta`,message 保持单行 (#5661)
6+
7+
## 接缝
8+
9+
`AutomationServicePlugin` 里还有三处把**外来**错误的文本插进日志 message —— 与
10+
#5048(flow 绑定)、#5575(`reconcileDeclaredConnectors``fail()`)、#5636
11+
(`degradeConnectorInstance`)同一类,是那三单范围之外的第四组:
12+
13+
- **`registerRunObject`**(`warn`):`err` 来自内核服务注册表(`ctx.getService('manifest')`
14+
`manifest.register()` 的解析拒绝),文本不是我们的。
15+
- **启动 probe**(`error`):`err` 来自 `candidate.probe()`,即**数据源驱动**抛出的错误。
16+
- **重启后 wait-timer 重新挂载**(`error`):`err` 是从 `rearmSuspendedWaitTimers` 逃出来
17+
的任何东西。
18+
19+
## 为什么后两条尤其值得改
20+
21+
它们的**存在理由**就是可读性。代码自己写明后果 —— 「suspended runs will NOT survive a
22+
restart」「every wait/approval paused before this restart will hang indefinitely」—— 并被
23+
#4632 特意定为 `error` 级,好让运维能找到。而 `ObjectLogger.write()` 一次调用只加一个
24+
「时间戳 + 级别」记录头,所以带换行的 message 会变成多个物理行、只有第一行有头:文件 sink
25+
把其余行当成独立记录存,采集端读成无法归属的碎片,`grep ERROR` 只捞到那条不含任何事实的
26+
头行。这个 plugin 里最响的耐久性告警,恰好是最可能以读不懂的形态抵达的那一条。
27+
28+
第一条的危害是另一种,并且是测出来的:`warn`**stdout**,正是 `serve` 启动静默窗口包住
29+
的那条流,而 `BootLogCapture.offer()` 只在该物理行上找得到级别头时才保留它 —— 所以续行是
30+
**直接丢弃**,不只是难解析。`registerRunObject``init()` 里跑,正处于窗口开着的时候。
31+
32+
## 改法(零新词汇)
33+
34+
三处都复用同包 `thrown-cause-diagnostics.ts``describeThrownForLog`:message 是不含换行
35+
的自足句子,cause 走 logger 的结构化 meta。参数位按 `Logger` 契约区分 ——
36+
`warn(message, meta?)` 没有 `Error` 位,cause 在**第二**参;`error(message, error?, meta?)`
37+
的 cause 在**第三**参(第二参塞原始 error 会让记录额外附带堆栈)。#4632 要求的「后果 + 修
38+
法」仍然完整留在 message 的第一行里,只是末尾的 `: ${err.message}` / `Cause: ${err.message}`
39+
换成了指向 meta 的一句话。
40+
41+
`pnpm check:durability-log-level` 仍绿:24 个耐久性接缝,三处 `error` 未降级、未改成 rethrow。
42+
43+
## 测试
44+
45+
新增 `plugin-startup-log-cause.test.ts`:13 个用例全部让真 `ObjectLogger` 写真字节再读回来
46+
(照 #5662 的先例 —— spy 只能证明接缝**调用**了什么,证明不了按行消费者会**看到**什么,而
47+
后者才是 cloud#971 付掉一整条 rc 线的那一半)。三条接缝各自钉住「多行 cause 不进 message、
48+
进结构化 meta」、参数位、以及无 cause 时输出零字节;末尾两个用例把插值形态与结构化形态并排
49+
渲染、量出差别(`warn` 侧:一次调用多个物理行、启动缓冲只留下止于 Zod `[` 的那一行;`error`
50+
侧:一条记录散成三个碎片,后两行无记录头)。
51+
52+
`plugin-suspended-run-wiring.test.ts` 里那条 #4420 的 probe 用例做了重新裁决而不是重新拼写:
53+
它原来断言驱动文本出现在 message 里,现在双向断言 —— message 里**没有**、meta 里****
54+
单向的断言在 cause 被整个丢掉时也会通过。

0 commit comments

Comments
 (0)