Skip to content

fix(service-automation): flow 绑定失败的告警改用结构化 meta,不再把 Zod issue 数组塞进单行日志 (#5048) - #5572

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5048-flow-bind-log-issues
Aug 5, 2026
Merged

fix(service-automation): flow 绑定失败的告警改用结构化 meta,不再把 Zod issue 数组塞进单行日志 (#5048)#5572
os-zhuang merged 1 commit into
mainfrom
claude/issue-5048-flow-bind-log-issues

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5048

前提核对(rule 6)

issue 写于 8-04,本单在 origin/main = ddc2527e2 上逐点核对,前提完全成立,四个 ${err.message} 单行插值点一字未动:

位置 现状(改前)
plugin.ts:758 boot pull(ObjectQL registry) `[Automation] failed to register flow ${def.name}: ${msg}`
plugin.ts:766 boot pull 外层 catch `[Automation] flow pull from ObjectQL registry failed: ${msg}`
plugin.ts:1402 readFlowDefsFromProtocolgetMetaItems `… getMetaItems('flow'): ${(err as Error).message}`
plugin.ts:1460 metadata:reloaded re-sync `[Automation] flow re-sync: failed to register ${def.name}: ${(err as Error).message}`
plugin.ts:1504 kernel:ready cold-boot bind `[Automation] cold-boot flow bind: failed to register ${def.name}: ${(err as Error).message}`

其中 plugin.ts:766 就是 PM 让我找的第五处同形态(同一 registry-pull 代码块的外层 catch),按同一原则一并改掉。

症状也实测复现了。用一个节点级未知键 visibleIfFlowSchema.parse:

first line: "["
lines in message: 21

21 行的 message,第一行就是一个 [ —— 与 issue 描述逐字吻合。

机制:为什么余下 20 行会消失

issue 说「CLI 的 boot diagnostics 每条记录只打一行」,读码确认了确切机制,两级管线各占一半:

  1. packages/core/src/logger.tsObjectLogger.write() 每次调用只写一条 时间戳 + 等级 + message 记录。message 里带换行,就溢出成多行,而只有第一行带等级前缀
  2. packages/cli/src/utils/boot-log-capture.tsBootLogCapture.offer()逐行过滤的:if (!isBootDiagnostic(trimmed, this.floor)) return; —— classifyBootLogLine 认不出等级前缀的行直接丢弃。

于是每一条续行都被 drop。一次启动 24 个绑不上的 flow,给出的就是 24 条点了名字、然后说一个 [ 的告警。cloud#971 能横跨整条 rc.1 发布线没人发现,机制就在这里 —— 是第 2 个缺陷让第 1 个缺陷得以隐身

修法

不靠 err.message 的字符串形态传结构化信息:message 是不含换行的静态字符串,事实交给 logger 的 meta 第二参。仓库里每个 Logger 实现(ObjectLogger / ConsoleLogger / JsonLogger)都用 JSON.stringify 序列化 meta,值里的换行变成 \n 转义 —— 整条记录稳定占一行,正是启动缓冲会保留的形态。顺带的好处:非 Zod 的多行 message(#4277 描述符检查、DAG/表达式校验)同样不再溢出。

新增内部模块 packages/services/service-automation/src/flow-bind-diagnostics.ts,describeFlowBindError(err) 返回 { issues }{ error },恰好其一。

三个刻意的设计选择(都有实测依据,不是口味)

1. Zod issue 摊平重命名,而不是原样转发 err.issues

issue 建议的写法是 issues: err?.issues。实测发现这条路会丢掉读者唯一需要的那个事实:ObjectLogger.redactSensitive子串递归匹配,默认脱敏表是 ['password','token','secret','key'],而 'keys'.includes('key') 为真。原样转发的真实渲染结果:

{"time":"…","level":"warn","msg":"[Automation] cold-boot flow bind: failed to register flow",
 "flow":"campaign_enrollment","issues":[{"code":"unrecognized_keys","keys":"***REDACTED***",…}]}

哪个键被拒 —— 正是本单存在的理由 —— 被脱敏器吃掉了。所以键名放在 unrecognized 字段(不含任何脱敏子串),path 渲染成 nodes[0].config.x 的点分形式,(root) 表示 flow 文档本身。这一条专门写了 pin 测试:forwarding Zod issues VERBATIM would be redacted,将来谁想「顺手简化成 err.issues」会当场红,而不是静默地把诊断重新弄瞎。

顺带一说,这也让日志载荷不再是 Zod 内部 issue 结构的再导出 —— Zod 3 到 4 之间 unrecognized_keys 的 message 措辞已经变过一次。

2. issue 列表设上限,而且把上限「声明」出来

一个 flow 可能产出几十条 issue;启动缓冲对超出预算的行是整行丢弃,那会把同一个失效模式复活。所以 MAX_LOGGED_FLOW_BIND_ISSUES = 20,超出时附 issueCount 报总数 —— 与 #4632 同源:截断可以,但必须是声明过的截断。

3. ZodError 用鸭子类型(Array.isArray(err.issues))判定,不用 instanceof

抛出点是 @objectstack/spec 里的 FlowSchema.parse,错误可能由另一个 zod 模块实例构造(dual-package hazard),而本包也没有直接的 zod 依赖可供 instanceof 比对。这个接缝上「形状即契约」:我们渲染 issues,就检测 issues

测试

新增 packages/services/service-automation/src/flow-bind-diagnostics.test.ts,12 个用例:

  • cold-boot bind:喂一个带未知键的 flow def,断言首参不含换行、meta.flow 正确、meta.issuesunrecognized_keys 完整(unrecognized: ['visibleIf'] + path: 'nodes[0]' + message 含键名)、meta.error 为 undefined。
  • re-sync(metadata:reloaded)同形态断言。
  • 非 ZodError 分支:stub registerFlow 抛普通 Errormeta.error 有内容、meta.issues 为 undefined(不是一个幻影空数组)。
  • getMetaItems 读失败:meta.error === 'protocol offline'
  • 正常 flow 仍然绑上、且一条告警都不发(反向哨兵)。
  • 渲染层:捕获真实 process.stdout.write,断言一次 logger.warn 只产出一行、行内含 visibleIfnodes[0]、且不含 REDACTED;另有一条 pin 上面那个「原样转发会被脱敏」的事实;再一条断言多行非 Zod message 也只占一行。
  • helper 单测:路径格式化、issues/error 分支、上限与 issueCount、空 issues 数组仍算校验拒绝。
Test Files  1 passed (1)
      Tests  12 passed (12)

全包回归:

Test Files  58 passed (58)
      Tests  707 passed (707)

typecheck(本包无 typecheck script,#4311 DEBT 台账在册,直接跑 tsc --noEmit -p tsconfig.json):改动没有新增任何错误,只剩 engine.test.ts / nested-region-parity.test.ts 里我没碰的既有项;node scripts/check-type-check-coverage.mjsOK — 62/77 … EXIT=0

门:check:durability-log-level(#4632)→ ✓ 24 durability-critical catch seam(s), all loud or rethrowing;check:nul-bytescheck:startup-registry-verdictcheck:init-service-contract 全过。改动文件另做了越过 check:nul-bytes 扫描面的自查(grep -naP 覆盖 0x010x1f),无命中。

反向验证

方向预测(先说后跑):把 cold-boot 那一处还原成改前的 ${err.message} 插值形态,新用例应该变红,而且要红在实质上 —— 不是红在 message 前缀改名上。所以断言按新旧共有前缀 [Automation] cold-boot flow bind: failed to register 匹配 warn,这样还原后仍能匹配到那一条调用,失败点落在内容而非查找上。

跑出来正是如此,失败输出把 issue 的症状原样打了出来:

AssertionError: expected '[Automation] cold-boot flow bind: fai…' not to contain '\n'
+ [Automation] cold-boot flow bind: failed to register campaign_enrollment: [
+   {
+     "code": "unrecognized_keys",
+     "keys": [
+       "visibleIf"
+     ],
…
 Tests  1 failed | 11 passed (12)

11 个通过的用例里包含渲染层那三条 —— 它们直接调 helper,不经过 plugin 接缝,所以对本次还原不敏感,这是预期的。随后已恢复。

边界与刻意未改

fixture 扫荡:全仓 grep 了这五条 message 的所有片段,除本包新测试外没有任何测试/fixture 断言旧字符串,所以没有 fixture 需要改判。每一个可 grep 的前缀(cold-boot flow bind: failed to registerflow re-sync: failed to registerflow pull from ObjectQL registry failedflow read from protocol failed)都保留了,老的 grep 命令继续有效。

按认领评论申报的文件面,只动 service-automationplugin.ts + 新模块 + 新测试 + changeset。没碰 wait-node.ts / engine.ts 的 resume 面(#5529/#3823),也没碰 #4792 的 engine ctor/seal 面。

同包其余 ${err.message} 插值我逐个判过,不属于本单类别:engine.ts 的 1438/1590(trigger.stop 失败)、1579(trigger.start 失败)、3371(durable store 列举)接的都不是 schema 解析,ZodError 到不了;builtin/* 的那些是 NodeExecutionResult.error 字段,不是日志行,是另一个面。

唯一一处同类、但在另一个接缝的:materializeDeclaredConnectorsfail() 在软路径上做 ctx.logger.error(msg),msg 由 ${(err as Error).message} 拼出(plugin.ts 1122 / 1160)。connector provider factory 抛 ZodError 时会以同样方式退化。按 Prime Directive #10 另开 issue,不在本 PR 里扩面。


🤖 Generated with Claude Code

https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK


Generated by Claude Code

…meta, not a one-line err.message (#5048)

The five flow-bind/read failure seams in AutomationServicePlugin interpolated
`err.message` into a single-line `logger.warn`. `registerFlow` parses with the
closed (#4001) `FlowSchema`, so an unrecognized key THROWS, and a ZodError's
`.message` is a pretty-printed JSON dump of its issue array whose first line is
the single character `[`.

Two pipeline properties then destroyed the rest: `ObjectLogger.write()` emits
one `<ts> <LEVEL> <msg>` record per call, so a message carrying newlines spills
onto prefix-less lines; and `BootLogCapture.offer()` keeps a line only when
`classifyBootLogLine` finds that prefix. A boot with 24 unbindable flows
therefore printed 24 warnings that named the flow and then said `[`. cloud#971
survived an entire rc.1 release line behind exactly that unreadability.

The seams now log a static, newline-free message and hand the facts to the
logger's `meta` argument, which every Logger implementation serializes with
JSON.stringify -- newlines in a value become escapes, so the record stays on
one physical line, the shape the boot capture retains.

New internal module `flow-bind-diagnostics.ts` flattens each Zod issue to
`{ code, path, message, unrecognized }`. The key names go in `unrecognized`
rather than Zod's own `keys` because ObjectLogger redacts recursively by
SUBSTRING and its default list contains `key`: forwarding `err.issues`
verbatim renders `"keys":"***REDACTED***"`, losing the one fact the reader came
for. The issue list is capped and the cap DECLARED via `issueCount` rather than
silently applied. Non-ZodError failures fall back to an `error` string.

No public API change; every greppable message prefix is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 7:26pm

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 5, 2026 19:39
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 8108787 Aug 5, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5048-flow-bind-log-issues branch August 5, 2026 19:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(service-automation): flow 绑定失败的告警把 Zod issue 数组塞进单行日志,读者只拿到一个孤零零的 [

2 participants