Skip to content

核心概念 ​

LoggerJS 围绕三个面向用户的概念组织:integrations、middleware/processors 和 transports。再加上一条边界规则:codecs 属于 transports。本页解释连接这些部分的管线。

管线 ​

text
logger.info("msg", data)
  │
  ├─ level gate            一次数值比较;禁用级别到这里直接停止
  │
  ├─ LogRecord built       lazy message 暂不求值,保留原始 error,
  │                        附加 category/type/tags/context
  │
  ├─ middleware            同步、有序,可以修改或丢弃 record
  │
  ├─ processors?           如果存在任何 processor,record 会投影为
  │   │                    LogEvent(分配 id、解析 message、标准化 error),
  │   │                    然后在 event 上运行 processors
  │   │
  │   └─ no processors:    record 直接进入 transports
  │                        (“record fast path”,没有投影成本)
  │
  └─ transports            每个 transport 接收 record(write/writeBatch)
      │                    或 event(log/logBatch);转换只发生一次并复用
      │
      └─ codec             transport 用自己的 codec 序列化

Integrations 位于这个流程外部:它们 hook 平台行为(console 调用、错误、fetch、process 事件),再通过 api.capture() 把捕获到的输入送进同一条管线。

LogRecord 与 LogEvent ​

LogRecord 是热路径形状。它保留原始值,确保在真正需要之前不做工作:

  • lazy:尚未求值的消息函数,最多解析一次。
  • err:原始错误值,尚未标准化。
  • props:用户数据对象;除非 middleware、processor 或 transport 显式 clone,否则按引用共享。
  • ctx:冻结的绑定上下文对象,按引用共享。
  • tags:可能是 logger 的冻结 tags 对象,按引用共享。
  • 没有 id:id 计算延迟到 event 投影阶段。

LogEvent 是面向 transport 的兼容形状:id、time、seq、level、levelName、logger(点分 category)、message(解析后的字符串)、type、tags、data、error(标准化后的 SerializedError)、context、trace、source。

recordToEvent() / eventToRecord() 可以互相转换。转换存在文档化的信息损失:runtime source 会折叠为 integration source,标量 event data 会包装成 { value }。对象 data 默认不会快照;如果后续修改不能影响延迟 transport,请在记录前 clone。

修改约定 ​

Middleware 可以修改 record,但有一条规则:替换字段,不要原地修改共享对象。record.ctx 和 logger 级别的 record.tags 是冻结且跨 record 共享的;应写 record.tags = { ...record.tags, extra },不要写 record.tags.extra = ...。原地修改冻结字段会抛错,并作为 middleware error 上报,不会污染其他 records。

Middleware 与 Processors ​

两者都是同步、有序且错误隔离的。区别在于看到的数据和运行时机:

MiddlewareProcessor
输入LogRecordLogEvent
运行时机在 id/message/error 工作之前在投影之后
丢弃返回 null返回 false
修改原地修改(替换字段)返回新 event
丢弃成本最低已经付过投影成本

补充字段和提前过滤优先用 middleware。需要解析后的 event 结构时再用 processor,例如按 event 字段路由、对规范化后的错误计算指纹、为 fingers-crossed 投递缓冲 event。

只要配置了任何 processor,该 logger 的 record 快速路径就会关闭,因为每条日志都必须先投影为 event。需要 event 级别的行为时,这是合理的取舍;具体数字见 性能。

Transports ​

一个 transport 可以实现以下四类写入方法中的任意组合:

ts
interface Transport {
  name?: string;
  minLevel?: LoggerLevel;
  ready?(): void | Promise<void>;
  write?(record: LogRecord, context: TransportContext): void | Promise<void>;
  writeBatch?(
    records: LogRecord[],
    context: TransportContext,
  ): void | Promise<void>;
  log?(event: LogEvent, context: TransportContext): void | Promise<void>;
  logBatch?(
    events: LogEvent[],
    context: TransportContext,
  ): void | Promise<void>;
  flush?(): void | Promise<void>;
  flushSync?(): void;
  close?(): void | Promise<void>;
}
  • 支持 record 的 transport(write/writeBatch)可以走快速路径,并直接编码 record。
  • Event transports(log/logBatch)接收投影后的 events。
  • context.toEvent(record) 按需转换;结果按 record memoize,所以多个 transports 共享一次投影,id 在多次转换间保持稳定。
  • transport 抛出的同步或异步错误都会被捕获并上报到 logger meta;一个失败 transport 不会阻塞其他 transport。
  • ready() 是显式、可选的。普通日志调用不会等待 transport 启动;需要确认启动完成的调用方使用 logger.ready()。
  • 当 write/log 返回 promise 时,logger.flush() 和 logger.close() 会先等待在它们之前发起的所有此类写入(包括 child logger 发起的写入)完成,再调用各 transport 自己的 flush()/close()。
  • close() 必须在释放资源前自行尽力 flush。存在 close() 时 core 会调用它;只有 transport 没有 close() 时才回退到调用 flush()。

Codecs 属于 Transports ​

序列化由 transport 拥有,并通过自己的 codec 配置。Middleware 和 processors 保持原始值,不要在管线内部提前 stringify。这样结构化脱敏仍然有效,每个目的地可以选择自己的 wire format,batching 也可以摊薄序列化成本。

ts
stdoutTransport({ codec: ndjsonCodec() });
browserHttpTransport({ url: "/api/logs", codec: fastEventJsonCodec() });

codec 契约以及“默认快速”的安全语义见 编解码。

Integrations ​

Integration 是一个具名的 setup(api) 函数,负责 hook 平台表面并返回 teardown:

ts
interface Integration {
  name: string;
  setup(api: IntegrationSetupContext): void | Teardown;
}

setup 上下文提供日志 API 和三个安全工具:

  • api.capture(input):把捕获信号送入管线,并标记 source: "integration:<name>"。
  • api.guard(fn):重入保护。如果 patched 代码路径调用 logger,而 logger 又调用到 patched 代码,内层调用会被丢弃并计数,避免无限循环。
  • api.unpatched:原始函数注册表(console.*、fetch、XMLHttpRequest),让 transports 和 integrations 在 patch 存在时仍能调用真实实现。

Integrations 会在 logger 构造时或 addIntegration() 时安装;每个 integration instance 只 setup 一次,并在 close() 时按反向顺序 teardown。

路由 ​

Processors 可以把 event 固定到命名 transports:

ts
import { routeProcessor } from "@loggerjs/processors";

routeProcessor([{ minLevel: "error", transports: ["alerts"] }]);

路由信息以不可枚举的 event 元数据形式附加,并在分发时使用。record 快速路径不做路由过滤:路由只能由 processor 附加,而 record 路径只在 logger 没有 processor 时才会运行。

同时存在多个 core 副本 ​

getLogger() 通过它所导入的那个 @loggerjs/core 副本的 registry 解析。一个进程可能加载多个副本:ESM 应用加 CJS 库,或者安装了两个版本。默认情况下每个副本都有自己的 registry、环境 context 和 meta 计数器,configure() 发现加载了多个副本时会警告一次。

  • configure({ shareAcrossCopies: true }) 让所有副本使用同一个进程级存储,其他副本中的库也会走应用的配置。
  • configure({ shareAcrossCopies: false }) 保持各副本隔离,并关闭警告。同一页面上各自打包的微前端需要这样,避免一个应用的 configure() 替换另一个应用的配置或关闭它的 transport。

开启共享后,无论哪个副本调用 getLogger(),registry 中的 logger 都由最后调用 configure() 的副本创建。因此,通过该副本安装的诊断 sink(@loggerjs/core/diagnostics 中的 setLoggerDiagnosticSink())能看到所有这些 logger。sink 本身仍按副本区分,这样从不安装 sink 的打包产物可以去掉诊断代码。在其他副本中用 createLogger() 创建的 logger 会上报给该副本自己的 sink;如果需要这些诊断数据,请在那个副本中也安装 sink。

同一个已安装版本的子路径入口(例如 @loggerjs/core/context)不算独立副本,它们与根入口共享状态。整个 1.x 期间默认都保持隔离,应用需要显式开启共享。

Levels、Categories、Sources ​

  • Levels 是数字(trace 10 到 fatal 60)并带名称;自定义数字级别在各处可用。
  • category 是字符串数组(["api", "checkout"]),在 event 中拼接成以点分隔的 logger 名;registry 按 category 前缀匹配配置。
  • source 区分应用日志和 integration 捕获日志,因此 console capture 可以从 console output 中排除,也能检测循环。

内部错误和 Meta Counters ​

管线永远不会把错误抛进应用代码。Middleware、processors、codecs 和 transports 中的失败会通过 onInternalError 上报,并计入 logger meta:

ts
import { getLoggerMetaStats } from "@loggerjs/core";

getLoggerMetaStats();
// { "transport.errors": 1, "transport.dropped.queue-full": 2, "codec.fallback": 1, ... }

用这些 counters 监控静默退化:队列丢弃、codec fallback、integration 重入丢弃等。getLoggerSelfMetrics() 同时返回 counters 和 gauges,包括共享 transport helpers 暴露的 queue depth 和 circuit-breaker 状态。

Trace 和语义事件 ​

trace-propagation helpers 负责解析/格式化 W3C traceparent 和 baggage headers;addContextProvider() 允许 integrations 附加 ambient context,而不替换应用自己的 context provider。semanticEvents 定义常见事件家族(error、http、db、job、ui、action、security、performance),让 integrations 和应用日志共享字段命名。

延伸阅读 ​

相关链接 ​

基于 MIT License 发布。