fix(service-automation): flow 绑定失败的告警改用结构化 meta,不再把 Zod issue 数组塞进单行日志 (#5048) - #5572
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 5, 2026
Closed
os-zhuang
marked this pull request as ready for review
August 5, 2026 19:39
This was referenced Aug 5, 2026
Closed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5048
前提核对(rule 6)
issue 写于 8-04,本单在
origin/main=ddc2527e2上逐点核对,前提完全成立,四个${err.message}单行插值点一字未动:plugin.ts:758boot pull(ObjectQL registry)`[Automation] failed to register flow ${def.name}: ${msg}`plugin.ts:766boot pull 外层 catch`[Automation] flow pull from ObjectQL registry failed: ${msg}`plugin.ts:1402readFlowDefsFromProtocol的getMetaItems`… getMetaItems('flow'): ${(err as Error).message}`plugin.ts:1460metadata:reloadedre-sync`[Automation] flow re-sync: failed to register ${def.name}: ${(err as Error).message}`plugin.ts:1504kernel:readycold-boot bind`[Automation] cold-boot flow bind: failed to register ${def.name}: ${(err as Error).message}`其中
plugin.ts:766就是 PM 让我找的第五处同形态(同一 registry-pull 代码块的外层 catch),按同一原则一并改掉。症状也实测复现了。用一个节点级未知键
visibleIf喂FlowSchema.parse:21 行的 message,第一行就是一个
[—— 与 issue 描述逐字吻合。机制:为什么余下 20 行会消失
issue 说「CLI 的 boot diagnostics 每条记录只打一行」,读码确认了确切机制,两级管线各占一半:
packages/core/src/logger.ts的ObjectLogger.write()每次调用只写一条时间戳 + 等级 + message记录。message 里带换行,就溢出成多行,而只有第一行带等级前缀。packages/cli/src/utils/boot-log-capture.ts的BootLogCapture.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.issuesissue 建议的写法是
issues: err?.issues。实测发现这条路会丢掉读者唯一需要的那个事实:ObjectLogger.redactSensitive按子串递归匹配,默认脱敏表是['password','token','secret','key'],而'keys'.includes('key')为真。原样转发的真实渲染结果:哪个键被拒 —— 正是本单存在的理由 —— 被脱敏器吃掉了。所以键名放在
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 个用例:meta.flow正确、meta.issues里unrecognized_keys完整(unrecognized: ['visibleIf']+path: 'nodes[0]'+ message 含键名)、meta.error为 undefined。metadata:reloaded)同形态断言。registerFlow抛普通Error→meta.error有内容、meta.issues为 undefined(不是一个幻影空数组)。getMetaItems读失败:meta.error === 'protocol offline'。process.stdout.write,断言一次logger.warn只产出一行、行内含visibleIf与nodes[0]、且不含REDACTED;另有一条 pin 上面那个「原样转发会被脱敏」的事实;再一条断言多行非 Zod message 也只占一行。issues/error分支、上限与issueCount、空 issues 数组仍算校验拒绝。全包回归:
typecheck(本包无
typecheckscript,#4311 DEBT 台账在册,直接跑tsc --noEmit -p tsconfig.json):改动没有新增任何错误,只剩engine.test.ts/nested-region-parity.test.ts里我没碰的既有项;node scripts/check-type-check-coverage.mjs→OK — 62/77 … EXIT=0。门:
check:durability-log-level(#4632)→✓ 24 durability-critical catch seam(s), all loud or rethrowing;check:nul-bytes、check:startup-registry-verdict、check:init-service-contract全过。改动文件另做了越过check:nul-bytes扫描面的自查(grep -naP覆盖0x01–0x1f),无命中。反向验证
方向预测(先说后跑):把 cold-boot 那一处还原成改前的
${err.message}插值形态,新用例应该变红,而且要红在实质上 —— 不是红在 message 前缀改名上。所以断言按新旧共有前缀[Automation] cold-boot flow bind: failed to register匹配 warn,这样还原后仍能匹配到那一条调用,失败点落在内容而非查找上。跑出来正是如此,失败输出把 issue 的症状原样打了出来:
11 个通过的用例里包含渲染层那三条 —— 它们直接调 helper,不经过 plugin 接缝,所以对本次还原不敏感,这是预期的。随后已恢复。
边界与刻意未改
fixture 扫荡:全仓 grep 了这五条 message 的所有片段,除本包新测试外没有任何测试/fixture 断言旧字符串,所以没有 fixture 需要改判。每一个可 grep 的前缀(
cold-boot flow bind: failed to register、flow re-sync: failed to register、flow pull from ObjectQL registry failed、flow read from protocol failed)都保留了,老的 grep 命令继续有效。按认领评论申报的文件面,只动
service-automation的plugin.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字段,不是日志行,是另一个面。唯一一处同类、但在另一个接缝的:
materializeDeclaredConnectors的fail()在软路径上做ctx.logger.error(msg),msg 由${(err as Error).message}拼出(plugin.ts1122 / 1160)。connector provider factory 抛 ZodError 时会以同样方式退化。按 Prime Directive #10 另开 issue,不在本 PR 里扩面。🤖 Generated with Claude Code
https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
Generated by Claude Code