核心概念
LoggerJS 围绕三个面向用户的概念组织:integrations、middleware/processors 和 transports。再加上一条边界规则:codecs 属于 transports。本页解释连接这些部分的管线。
管线
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
两者都是同步、有序且错误隔离的。区别在于看到的数据和运行时机:
| Middleware | Processor | |
|---|---|---|
| 输入 | LogRecord | LogEvent |
| 运行时机 | 在 id/message/error 工作之前 | 在投影之后 |
| 丢弃 | 返回 null | 返回 false |
| 修改 | 原地修改(替换字段) | 返回新 event |
| 丢弃成本 | 最低 | 已经付过投影成本 |
补充字段和提前过滤优先用 middleware。需要解析后的 event 结构时再用 processor,例如按 event 字段路由、对规范化后的错误计算指纹、为 fingers-crossed 投递缓冲 event。
只要配置了任何 processor,该 logger 的 record 快速路径就会关闭,因为每条日志都必须先投影为 event。需要 event 级别的行为时,这是合理的取舍;具体数字见 性能。
Transports
一个 transport 可以实现以下四类写入方法中的任意组合:
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 也可以摊薄序列化成本。
stdoutTransport({ codec: ndjsonCodec() });
browserHttpTransport({ url: "/api/logs", codec: fastEventJsonCodec() });codec 契约以及“默认快速”的安全语义见 编解码。
Integrations
Integration 是一个具名的 setup(api) 函数,负责 hook 平台表面并返回 teardown:
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:
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 是数字(
trace10 到fatal60)并带名称;自定义数字级别在各处可用。 - category 是字符串数组(
["api", "checkout"]),在 event 中拼接成以点分隔的 logger 名;registry 按 category 前缀匹配配置。 source区分应用日志和 integration 捕获日志,因此 console capture 可以从 console output 中排除,也能检测循环。
内部错误和 Meta Counters
管线永远不会把错误抛进应用代码。Middleware、processors、codecs 和 transports 中的失败会通过 onInternalError 上报,并计入 logger meta:
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 和应用日志共享字段命名。
