Skip to content

编解码 ​

codec 把日志 record 或 event 转换成字节,也可以选择支持反向解析。codec 属于 transport:管线内部始终保留原始值,这样脱敏和路由处理的是结构化数据,每个目的地也能选择自己的线上格式。

契约 ​

ts
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 ​

CodecPackage行为
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 输出:

ts
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 形状输出的迁移路径:

ts
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 ​

ts
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;投递不要求它。

相关链接 ​

基于 MIT License 发布。