Skip to content

Codecs

A codec turns log records or events into bytes (and optionally back). Codecs belong to transports — the pipeline keeps values raw so redaction and routing operate on structured data, and each destination chooses its own wire format.

The Contract

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 accepts single items or batches, events or records. normalizeCodecInput() from core projects records to events for codecs that only understand events.
  • decode is optional; built-in codecs implement it with JSON.parse for symmetric round trips of their own output.
  • prepareRecordEncoder is optional. Record-aware transports can call createPreparedRecordEncoder(codec) to let the codec cache stable category/tags fragments while keeping serialization owned by the transport.

Built-in Codecs

CodecPackageBehavior
jsonCodec()coreBare JSON.stringify after input normalization. Fast, throws on circular/BigInt — pick it only when payloads are guaranteed clean.
safeJsonCodec(options)coreFull safe normalization every time: circular → "[Circular]", BigInt → string, Error → {name, message, stack}, depth/array/key truncation, Map/Set conversion. Default codec of consoleTransport({ pretty: false }).
ndjsonCodec(options)coreOne JSON object per line. Fast-by-default contract (below). Default codec of the Node stdout/file transports.
fastEventJsonCodec(options)@loggerjs/codecsThe performance codec: native fast path, fragment caches (level, logger, tags, time), scan-based string escaping, flat-data direct writer, lean envelope options.
pinoCompatCodec(options) / pinoNdjsonProjector(options)@loggerjs/codecsPino-shaped NDJSON for migration paths: level, time, optional pid/hostname base fields, msg, err, and opt-in root data merging with reserved-key protection.
msgpackrCodec(options?)@loggerjs/codecsBuilt-in MessagePack codec backed by msgpackr; returns Uint8Array. Passing { pack, unpack } is still supported for custom runtimes.
projectorCodec(options)@loggerjs/codecsGeneric project → serialize (→ parse → unproject) adapter for custom wire schemas.
otlpJsonCodec(options)@loggerjs/otelOTLP/HTTP JSON log payloads with resource attributes.

The Fast-by-Default Contract

ndjsonCodec() and fastEventJsonCodec() share one documented behavior model:

  • No options set — encode runs on a native fast path. Output matches native JSON.stringify semantics: nested raw Error values in data serialize as {}, no depth truncation. Inputs that make native stringify throw (circular references, BigInt) are transparently re-encoded with the safe stringifier instead — a log line is never lost to encoding, and each fallback increments the codec.fallback meta counter.
  • Any SafeStringifyOptions field set (maxDepth, maxArrayLength, maxObjectKeys, includeStack, stable, space) — the codec opts into full safe normalization for every item, which also preserves Error name/message/stack inside data payloads.

Choose explicitly: native-fast with throw-protection, or fully normalized. safeJsonCodec remains always-safe.

Lean Envelope Options

fastEventJsonCodec can trim the envelope for minimal NDJSON output:

ts
fastEventJsonCodec({
  includeId: false,        // also skips id computation entirely on the record path
  includeSeq: false,
  includeLevelName: false,
  // includeData / includeError / includeContext / includeTrace / includeSource
})

With the three header flags off, output is a lean LoggerJS envelope: {"time":...,"level":30,"logger":"api","message":"...","data":{...}}. This is the configuration behind the headline benchmark numbers in BENCHMARKS.md, and record-aware custom transports can pair it with createPreparedRecordEncoder(codec) for the fastest stable-fragment path. Use pinoCompatCodec() when you need Pino field names such as msg, err, pid, and hostname.

Pino Compatibility

pinoCompatCodec() emits newline-delimited JSON for migration paths that need Pino-shaped output:

ts
import { pinoCompatCodec } from "@loggerjs/codecs";

pinoCompatCodec({
  base: { pid: process.pid, hostname: "api-1" },
  mergeData: true,
});

Root data merging is opt-in. By default, LoggerJS keeps payloads under data; when mergeData: true is enabled, reserved keys such as time, level, msg, pid, hostname, err, logger, and data are nested instead of overwriting transport-owned fields. Set collision: "throw" if you prefer to reject those payloads during migration testing.

This codec is intentionally encode-only: Pino-shaped NDJSON is a migration wire format, not the native LoggerJS event envelope.

Records, Events, and IDs

Encoding raw LogRecords (the fast path) has one semantic difference from encoding events: records carry no id, so the codec stamps defaultRecordId(record, levelName) — a time36-seq36-levelName string identical to what recordToEvent() would assign. Consequences:

  • With the default id factory, record-encoded and event-encoded output are identical.
  • A custom idFactory on the logger is bypassed by record-direct encoding. If custom ids matter, have your transport convert via context.toEvent(record) (memoized, id factory applied) instead of encoding the record directly.

Writing a Custom 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");
    },
  };
}

Guidelines:

  • Never throw out of encode. Wrap risky paths and fall back to safeJsonStringify from core; count fallbacks with incrementLoggerMetaCounter("codec.fallback"). A throwing codec turns into a transport failure and, inside a batch transport, a poison batch that burns retries.
  • Use normalizeCodecInput() unless you deliberately implement a record fast path.
  • If you implement prepareRecordEncoder, its output must be byte-for-byte identical to encode(record) for the same record and must keep the same fallback behavior.
  • For binary formats return Uint8Array and set an accurate contentType — HTTP transports send it.
  • Implement decode only when symmetric round trips are part of your feature (replay, local query); it is not required for delivery.

Released under the MIT License.