编解码
codec 把日志 record 或 event 转换成字节,也可以选择支持反向解析。codec 属于 transport:管线内部始终保留原始值,这样脱敏和路由处理的是结构化数据,每个目的地也能选择自己的线上格式。
契约
interface Codec<TPayload = string | Uint8Array> {
name: string;
contentType: string;
encode(input: LogEvent | LogRecord | readonly (LogEvent | LogRecord)[], context?: EncodeContext): TPayload;
decode?(payload: TPayload): LogEvent | LogEvent[];
prepareRecordEncoder?(hints: RecordEncoderHints): PreparedRecordEncoder<TPayload>;
}encode可以接收单条或一批,可以是 event 也可以是 record。对于只理解 event 的 codec,可以用 core 的normalizeCodecInput()把 record 投影成 event。decode是可选的;内置 codecs 用JSON.parse实现对自身输出的对称 round trip。prepareRecordEncoder是可选的。Record-aware transports 可以调用createPreparedRecordEncoder(codec),让 codec 缓存稳定的 category/tags 片段,同时仍然由 transport 拥有序列化。
内置 Codecs
| Codec | Package | 行为 |
|---|---|---|
jsonCodec() | core | 输入标准化后直接 JSON.stringify。速度快,但 circular/BigInt 会抛错;只在 payload 保证干净时使用。 |
safeJsonCodec(options) | core | 每次都做完整的安全规范化:循环引用 -> "[Circular]"、BigInt -> 字符串、Error -> {name, message, stack}、按深度/数组长度/键数量截断、转换 Map/Set。consoleTransport({ pretty: false }) 的输出与它相同。 |
ndjsonCodec(options) | core | 每行一个 JSON 对象,遵循下文的默认快速契约。 |
safeJsonEncoder(options) / ndjsonEncoder(options) | core | 去掉 decode() 的 safeJsonCodec 和 ndjsonCodec。只发送日志的 transport 默认使用它们:浏览器和 Node 的 HTTP、WebSocket、worker 以及 database transport 使用 safeJsonEncoder,Node 的 stdout 和 file transport 使用 ndjsonEncoder。 |
fastEventJsonCodec(options) | @loggerjs/codecs | 面向性能的 codec:原生快速路径、片段缓存(level、logger、tags、time)、扫描式字符串转义、扁平数据直接写入,以及 lean 信封选项。 |
pinoCompatCodec(options) / pinoNdjsonProjector(options) | @loggerjs/codecs | 用于迁移的 Pino 格式 NDJSON:level、time、可选的 pid/hostname 基础字段、msg、err,以及可选的、带保留键保护的根级数据合并。 |
msgpackrCodec(options?) | @loggerjs/codecs | 基于 msgpackr 的内置 MessagePack codec;返回 Uint8Array。仍支持传入 { pack, unpack } 适配自定义 runtime。 |
projectorCodec(options) | @loggerjs/codecs | 通用的“投影 -> 序列化(-> 解析 -> 反投影)”适配器,用于自定义线上格式。 |
otlpJsonCodec(options) | @loggerjs/otel | 带 resource 属性的 OTLP/HTTP JSON 日志 payload。 |
decode() 会校验它返回的每个事件,这部分校验在小体积打包产物中占比不小。从不读回日志的 transport 默认使用只编码的形式,因此只记录日志的应用不会打包这部分代码。需要读回事件的地方请使用完整的 codec,例如 indexedDbTransport().query()。
默认快速契约
ndjsonCodec() 和 fastEventJsonCodec() 共享同一套文档化行为:
- 不设置任何选项:编码走原生快速路径,输出遵循原生
JSON.stringify的语义:data 中嵌套的原始Error会被序列化为{},也不做深度截断。对于会让原生序列化抛错的输入(循环引用、BigInt),会自动改用安全序列化重新编码;日志不会因为编码失败而丢失,每次回退都会让 meta 计数codec.fallback加一。 - 设置任意
SafeStringifyOptions字段(maxDepth、maxArrayLength、maxObjectKeys、includeStack、stable、space):codec 会对每一条都做完整的安全规范化,同时保留 data 中Error的 name/message/stack。
这需要显式选择:native-fast 加 throw-protection,或完整标准化。safeJsonCodec 始终是 always-safe。
Lean Envelope 选项
fastEventJsonCodec 可以裁剪 envelope,以获得最小 NDJSON 输出:
fastEventJsonCodec({
includeId: false, // 也会在 record path 上完全跳过 id 计算
includeSeq: false,
includeLevelName: false,
// includeData / includeError / includeContext / includeTrace / includeSource
})关闭三个 header flags 后,输出是 lean LoggerJS envelope:{"time":...,"level":30,"logger":"api","message":"...","data":{...}}。这是 基准 中头条数字使用的配置;record-aware custom transports 可以把它和 createPreparedRecordEncoder(codec) 搭配,走最快的稳定片段路径。需要 Pino 字段名(msg、err、pid、hostname)时使用 pinoCompatCodec()。
Pino 兼容
pinoCompatCodec() 输出 newline-delimited JSON,适合需要 Pino 形状输出的迁移路径:
import { pinoCompatCodec } from "@loggerjs/codecs";
pinoCompatCodec({
base: { pid: process.pid, hostname: "api-1" },
mergeData: true,
});把数据合并到根级需要主动开启。默认情况下,LoggerJS 把 payload 保留在 data 下;启用 mergeData: true 后,time、level、msg、pid、hostname、err、logger、data 等保留键会被嵌套存放,避免覆盖由 transport 控制的字段。如果希望在迁移测试时直接拒绝这类 payload,可以设置 collision: "throw"。
这个 codec 有意只支持编码:Pino 格式的 NDJSON 是迁移用的线上格式,而不是 LoggerJS 原生的 event 信封。
Records、Events 和 IDs
直接编码原始 LogRecord(快速路径)与编码 event 有一处语义差异:record 没有 id,所以 codec 会写入 defaultRecordId(record, levelName),即 time36-seq36-levelName 形式的字符串,与 recordToEvent() 分配的 id 一致。影响是:
- 使用默认 id factory 时,record-encoded 和 event-encoded 输出完全相同。
- logger 上自定义的
idFactory会被 record-direct encoding 绕过。如果自定义 id 很重要,transport 应通过context.toEvent(record)转换(memoized,会应用 id factory),而不是直接编码 record。
编写自定义 Codec
import { normalizeCodecInput, type Codec } from "@loggerjs/core";
export function csvCodec(): Codec<string> {
return {
name: "csv",
contentType: "text/csv",
encode(input) {
const events = normalizeCodecInput(input);
const list = Array.isArray(events) ? events : [events];
return list
.map((e) => `${e.time},${e.levelName},${JSON.stringify(e.logger)},${JSON.stringify(e.message)}`)
.join("\n");
},
};
}建议:
- 不要让
encode抛出。 包住风险路径,并 fallback 到 core 的safeJsonStringify;用incrementLoggerMetaCounter("codec.fallback")计数。抛错的 codec 会变成 transport failure;在 batch transport 内还会形成消耗 retries 的 poison batch。 - 除非你有意实现 record 快速路径,否则请使用
normalizeCodecInput()。 - 如果实现
prepareRecordEncoder,同一 record 的输出必须和encode(record)字节级一致,并保持相同 fallback 行为。 - 二进制格式返回
Uint8Array,并设置准确的contentType;HTTP transports 会发送它。 - 只有当你的功能需要对称 round trip(replay、本地查询)时才实现
decode;投递不要求它。
