Skip to content

finding(objectql): hook 层自带的 logger 接口把 error 声明成 (msg, meta?),与 Logger 契约的第二参是 Error 相反 —— 三处调用点在任何忠于契约的 Logger 下都会丢掉 meta #5637

Description

@os-zhuang

做 #5575 时核实 Logger.error 的类型契约扫到的,与那一单不同包、不同接缝,按 Prime Directive #10 单开。

事实

packages/spec/src/contracts/logger.ts:37 声明的契约是:

error(message: string, error?: Error, meta?: Record< string, any >): void;

第二参是 Error,meta 在第三位。packages/objectql 的两个模块各自声明了一份结构化 logger 形状,把 error 写成两参:

  • packages/objectql/src/hook-binder.ts:77-81
  • packages/objectql/src/hook-wrappers.ts:26-31
logger?: {
  debug: (msg: string, meta?: any) => void;
  info: (msg: string, meta?: any) => void;
  warn: (msg: string, meta?: any) => void;
  error: (msg: string, meta?: any) => void;   // ← 第二参是 meta,不是 Error
};

于是三个调用点按这份本地方言把 meta 放在第二位:

  • hook-wrappers.ts:341 — logger.error('[hook] handler failed (onError=log; suppressing)', { … })
  • hook-wrappers.ts:394 — logger.error('[hook] async handler error (fire-and-forget)', { … })
  • hook-binder.ts:243 — logger.error('[hook-binder] failed to bind hook', { … })

注入进去的是 ctx.logger(plugin.ts:290 / plugin.ts:1670)。契约类型能结构化地满足这份本地形状(参数少的一方可赋值,any 双向兼容),所以 tsc 一句话都不说 —— 方言与契约的冲突只在运行时体现。

为什么今天没坏,以及什么时候会坏

只因为宿主注入的实现恰好是 ObjectLogger,而它对第二参按形状分派(errorOrMeta instanceof Error),所以 meta 在第二位也能落进记录。这份宽容本身是契约没有声明的。

契约的另外两个实现 —— @objectstack/observability 的 ConsoleLogger / JsonLogger(loggers.ts:55 / 113)—— 忠实按契约来:

error(message: string, error?: Error, meta?: Record< string, unknown >): void {
    this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });
}

传进去的对象落在 error 位,error.message / error.stack 都是 undefined,meta 是 undefined —— 于是这三条诊断整块消失,只剩一句话。@objectstack/observability 正是为了「宿主换一个结构化 logger」而存在,今天仓库里没有任何地方 new JsonLogger(...)(除了它们自己的 child()),所以属于休眠漂移(observation-class),不是用户今天会撞到的 bug —— 但它会在第一个真正接入生产日志栈的宿主那里生效,而且症状是「日志少了字段」,极难归因。

建议(不预设结论,这里有两个方向)

  1. 把三个调用点改成契约形状(error(msg, undefined, { … })),并把两处本地 logger 接口的 error 改成三参 —— 或者干脆 import type { Logger } from '@objectstack/spec/contracts',别再自带方言。注意:ObjectLogger 在 finding(service-automation): connector 物化失败的 fail() 也是 ${err.message} 单行插值,同 #5048 的类别、另一个接缝 #5575 之前会丢弃契约的第三参,所以这个方向必须建立在 finding(service-automation): connector 物化失败的 fail() 也是 ${err.message} 单行插值,同 #5048 的类别、另一个接缝 #5575 的 packages/core/src/logger.ts 修复之上(已随 finding(service-automation): connector 物化失败的 fail() 也是 ${err.message} 单行插值,同 #5048 的类别、另一个接缝 #5575 落地)。
  2. 或者认为「meta 可以出现在 error 位」是我们真心想要的能力 —— 那就应该在契约里声明(error?: Error | Record< string, any >),而不是让一个实现私下宽容、另两个实现静默丢数据。这一步会给每个 Logger 实现加上按形状分派的义务,属于契约级决定,不该由实现方言既成事实地推动。

倾向 1:契约先行,消费端不要方言(Prime Directive #12);2 是把一处宽容升级成所有实现的义务,收益只是省掉一个 undefined。

关联

#5575(核实 Logger.error 契约时发现;同一 PR 修好了 ObjectLogger 丢弃第三参 meta 的缺陷,并统计出 metadata / metadata-protocol / client / core/security 约 15 处按契约书写、此前一直静默丢字段的调用点)、#5048 / PR #5572。

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions