Skip to content

LoggerJS 架构

状态:当前面向 v1 的代码库实现架构。 来源输入:DESIGN.mdlog.md 和当前 monorepo skeleton。

LoggerJS 是面向浏览器、Node、Bun、Deno 和 edge runtimes 的同构结构化 logger。产品架构围绕三个面向用户的概念构建:

  • Integration:opt-in 自动采集,例如 browser console capture、global script errors、HTTP errors、page lifecycle flush、Node process errors 和 runtime diagnostics。
  • Middleware:同步 record transforms 和 filters,例如 redaction、sampling、tag/type enrichment、request correlation、dedupe 和 route-specific policies。
  • Transport:目的地边界,例如 console、stdout、file、HTTP batch、OTLP、Sentry、DB、worker-hosted delivery 或任意用户自定义 sink。

还有一个必须保持一等地位的技术边界:Codec。Codec 属于 transport,并拥有 serialization/deserialization。Middleware 不得序列化 records。Console transport 应保留原始值。HTTP/file/OTLP transports 选择各自需要的 codec。

当前仓库基线

当前仓库已经具备主要 v1 building blocks:

text
packages/core        Logger, LogRecord helpers, LogEvent projection, context, typed events, codecs, console/memory/batch transports
packages/browser     Browser HTTP transport, offline queue, beacon/page lifecycle flush, console/error/fetch/XHR integrations
packages/node        stdout/stderr/file/http/worker transports, AsyncLocalStorage context, process and diagnostics-channel integrations
packages/processors  redact/sample/tags/type/dedupe/trace processors
packages/codecs      fixed-shape JSON, built-in msgpackr, projector codec
packages/otel        OTLP JSON mapping, HTTP transport, active span trace processor
packages/sentry      Sentry structured logs, breadcrumbs, exception/message transport
examples/*           browser and node basic demos

剩余架构工作主要是打磨和 package topology:

  • Processor 仍作为兼容词汇支持,而 Middleware 是公开心智模型。
  • LogEvent 仍作为面向 transport 的兼容 envelope;热路径构造 LogRecord,只在需要时投影。
  • 粗粒度 browser/node packages 可以保留为 presets,但稳定 v1 packages 应把平台 transports 和 integrations 拆成更小的 installable units。
  • 当前双 ESM/CJS 输出保留用于兼容。Declaration output 兼容 NodeNext,并验证 public subpath exports。
  • Batch transports 已覆盖 bounded queues、byte limits、retry、drop counters、circuit breaking、pagehide/beacon behavior 和 runtime flush semantics。

不可谈判的设计规则

  1. Core 平台中立。 @loggerjs/core 不得 import browser、Node、Bun、Deno、worker、filesystem、fetch 或 diagnostics APIs。
  2. 禁用日志几乎免费。 禁用级别调用必须只做一次数字 level 比较,并在 record allocation、message stringification、context merge 或 integration work 前返回。
  3. 序列化只发生在 transport 边界。 管线保留原始引用。resolveMessage(record) 是唯一允许 middleware 触发的 lazy evaluation。
  4. Middleware 同步。 热路径中没有 promises、没有 Koa-style next、没有 async lookup。
  5. Integrations 使用与手动日志相同的管线。 自动 records 只是在 source 上不同;它们仍经过 middleware、routing、batching、codec 和 transport policy。
  6. Integrations 显式且可逆。 任何 monkey patch 都必须 opt-in、idempotent、防重入,并能完全 teardown。
  7. Logger 错误永远不逃逸到应用代码。 内部失败通过 rate-limited meta logger 计数和报告。
  8. v1 不使用 object pool。 短生命周期 records 应保持 young-generation GC objects,除非 benchmark 证明需要其他方案。

端到端管线

text
manual API / integration capture
        |
        v
  level gate
        |
        v
  create LogRecord
        |
        v
  global middleware
        |
        v
  transport router / fan-out
        |
        +--> per-transport middleware
        |          |
        |          v
        |     transport buffer
        |          |
        |          v
        |     codec.encode(batch)
        |          |
        |          v
        |     sink: console / stdout / file / HTTP / OTLP / worker / custom
        |
        +--> ...

在选定 transport 准备投递之前,record 绝不应被 stringify。这让 console 能保留可交互对象,HTTP 能选择 JSON 或 binary,file 能选择 NDJSON,OTLP 能选择自己的 wire mapping,而且不惩罚其他目的地。

Core Record Model

目标内部 record 是 LogRecord,不是当前 LogEvent envelope。它为稳定 hidden class 和 transport-owned projection 优化:

ts
export interface LogRecord {
  time: number;
  level: number;
  category: readonly string[];
  msg: string | null;
  lazy: (() => string) | null;
  props: Record<string, unknown> | null;
  err: unknown;
  ctx: BoundContext | null;
  source: string;
  stack: string | null;
  seq: number;
}

实现规则:

  • 通过单一 createRecord() 路径构造 records。
  • 以相同顺序分配每个字段,包括 null 字段。
  • delete 字段,不向 record 附加 ad hoc properties。
  • Extra data 放在 props 或 immutable ctx 中。
  • timeDate.now();同一 timestamp 内用 seq 排序。
  • errprops 分离,因为 error encoding、stack truncation、cause handling 和 dedupe 是专门逻辑。

当前 LogEvent 形状可以暂时保留为 codec projection 或兼容类型,但 v1 rewrite 开始后不应驱动热路径。

Logger API

LoggerJS 支持两种 acquisition models:

ts
const log = createLogger({
  category: "app",
  level: "info",
  transports: [consoleTransport()]
});
ts
const log = getLogger(["library", "parser"]);

await configure({
  middleware: [redact({ paths: ["password", "*.token"] })],
  transports: {
    console: consoleTransport(),
    http: httpTransport({ url: "/v1/logs", codec: jsonCodec() })
  },
  loggers: [
    { category: ["app"], level: "debug", transports: ["console", "http"] },
    { category: ["library"], level: "warn", transports: ["http"] }
  ],
  integrations: [consoleIntegration(), globalErrorsIntegration()]
});

必需调用形态:

ts
log.info("user logged in", { userId: 42 });
log.error(err, "save failed", { orderId });
log.debug(() => expensiveDebugMessage());
log.event(CheckoutCompleted, { orderId, amountCents });
log.child({ requestId }).warn("retrying");
await log.flush();

Overload 规则保持很小:

  • 第一个参数是 string:message
  • 第一个参数是 function:lazy message
  • 其他情况:error slot,后面可选 message 和 props

Core 不包含 printf-style formatting。结构化字段是一等公民;formatting 属于显示层。

Registry 和 Configuration

getLogger(category) 为库作者存在。配置前返回 void logger;configure() 后通过配置好的管线路由。

配置要求:

  • 按 category 做 prefix matching,例如 ["app"] 应用于 ["app", "checkout"]
  • named transports 和 named middleware
  • 显式 integration lifecycle management
  • 可选 early ring buffer,用于 pre-config logs
  • configure({ reset: true }) 替换旧 transports 并调用 async disposal hooks
  • immutable runtime snapshots,让热路径读取不遍历 mutable config structures

这个 registry 是战略功能:第三方库可以记录日志,而不耦合到任何 backend,也不强迫应用配置。

Context

有两类 context:

  • 显式 contextlogger.child(bindings)。Child bindings 在 child 创建时 flatten 并 freeze。
  • 隐式 contextwithContext(bindings, fn)。Node/Bun/Deno 使用 AsyncLocalStorage 或等价 conditional exports。浏览器初期降级为 synchronous-scope context,直到 TC39 AsyncContext 可用。

Codec-level context optimization 替代 pino-style global chindings:

ts
interface EncodeContext {
  levelName(level: number): string;
  ctxCache: WeakMap<object, unknown>;
  schemaCache: WeakMap<object, unknown>;
}

每个 codec 可以缓存 immutable bound contexts 的 encoded fragments。这样保留性能收益,同时不让 JSON serialization 成为全局 logger concern。

Middleware

目标接口:

ts
export interface Middleware {
  readonly name: string;
  process(record: LogRecord): LogRecord | null;
}

执行模型:

  • global middleware 在 fan-out 前运行一次,可以原地修改单一 record
  • per-transport middleware 在 fan-out 后运行,必须把 record 视为共享值
  • per-transport changes 使用 cloneRecord(record, patch) 来保留形状并避免跨 transport 泄漏
  • 返回 null 表示丢弃 record
  • middleware exceptions 会被捕获和计数,不会阻止剩余管线,除非 middleware 明确丢弃

内置能力应覆盖:

  • redact:安全 path/key redaction,在 matched branches 上 copy-on-write
  • sample:按 level/category/key sampling,error 和 fatal 默认完整保留
  • rateLimit:按 category/level/source 的 token bucket
  • dedupe:fingerprinted burst collapse
  • fingersCrossed:由 error trigger 释放的低级别 ring buffer
  • enrich:同步 props/context enrichment
  • tagstype:当前 processor 行为的 thin compatibility helpers
  • traceContext:OTel 或用户提供的 trace/span injection

Middleware 不得调用 JSON.stringifyString(record.props) 或递归标准化整个 records。如果需要 message,必须有意调用 resolveMessage(record)

Transports

目标接口:

ts
export interface Transport {
  readonly name: string;
  write(record: LogRecord): void;
  flush(): Promise<void>;
  flushSync?(): void;
  dispose(): Promise<void>;
  filter?(record: LogRecord): boolean;
  middleware?: Middleware[];
}

Transport 职责:

  • final routing filters
  • queue 和 backpressure policy
  • batching
  • retry 和 circuit breaking
  • codec selection 和 serialization
  • destination-specific delivery
  • drop/error counters
  • flush 和 disposal semantics

Batching Base

共享 batching 实现应支持:

  • maxRecords
  • maxBytes
  • maxWaitMs
  • concurrency
  • 带 exponential backoff 和 full jitter 的 retry
  • drop-olddrop-new
  • drop counters 和 hooks
  • 带 half-open recovery 的 circuit breaker
  • 队列为空时不保留 idle timer
  • ship 时做 encoded-size accounting

当前 batchTransport() 可以作为 bootstrap utility,但不是完整 v1 reliability layer。

Console Transport

Console transport 在 pretty mode 下不应序列化。它应把原始 msgpropserr 引用传给原始 console methods,让浏览器 devtools 保持对象检查能力。

它必须使用 unpatched console registry,才能和 console capture 共存而不形成 feedback loops。

HTTP Transport

HTTP transport 是带平台实现的共享抽象:

  • Browser:fetchkeepalivepagehide/visibilitychange 上的 sendBeacon、可选 IndexedDB offline queue,以及围绕 64 KiB beacon budget 的严格 payload limits。
  • Node:global fetch/undici、retry/circuit breaker,不宣称 sync crash flush。
  • Edge:waitUntil hook,用于 response-lifetime-safe delivery。

隐私默认值:

  • 不采集 request/response body
  • 不采集 headers,除非 allowlisted
  • 不启用 offline disk persistence,除非显式配置

File 和 Stdout Transports

Node stdout/stderr/file transports 默认应输出 NDJSON。File transport 需要真实 flushSync() 路径,使用 fs.writeSync 或等价 crash-safe primitive。仅 async stream writes 不足以覆盖 fatal process events。

Worker Transport

Node worker transport 应把 IO 和 retry state 移出主线程。首选路径:

text
main thread batch -> codec.encode(batch) -> Uint8Array -> postMessage(buffer, [buffer])

worker 失败时,transport 应降级到 inline mode 并发出 meta warning。跨 worker boundary 不提供 flushSync

OTLP 和 Sentry

OTLP/HTTP JSON 是第一方 transport,因为 LoggerJS 应接入现有 observability backends,而不是发明新的 logging backend protocol。

Sentry 支持应作为 adapter package。LoggerJS 把 records 映射成 Sentry structured logs,并可选择把 error records 捕获为 Sentry events。

Codecs

目标接口:

ts
export interface Codec<Out extends string | Uint8Array = string | Uint8Array> {
  readonly name: string;
  readonly contentType: string;
  encode(batch: readonly LogRecord[], ctx: EncodeContext): Out;
  decode?(data: Out): unknown[];
}

必需 codecs:

  • jsonCodec:默认 NDJSON/log JSON codec,固定 field ordering,对普通 props 使用 native JSON.stringify,只在失败 branches 上安全 fallback。
  • structuredCodec:保留丰富值的 codec,可对 Error、cause chains、AggregateError、circular/shared references、BigInt、Date、RegExp、URL、Map、Set、TypedArray、ArrayBuffer、undefinedNaN、infinities 和 -0 做对称 decode。
  • msgpackCodec:二进制 batch codec,可以是 benchmark-proven custom subset,也可以是 msgpackr 的小 adapter。
  • projectorCodec:自定义 wire schemas 的 utility adapter。

Structured codec 应使用 flat value-pool format,不使用 evalnew Function 或 recursive revivers。Decode 应是 JSON.parse 加确定性的 pointer restoration pass,使它 CSP-friendly,并适合 browser replay tools。

Integrations

目标接口:

ts
export interface Integration {
  readonly name: string;
  setup(api: IntegrationAPI): Teardown;
}

export interface IntegrationAPI {
  capture(input: CaptureInput): void;
  getLogger(category: string | readonly string[]): Logger;
  unpatched: UnpatchedRegistry;
  guard<T extends (...args: any[]) => any>(fn: T): T;
}

Loop prevention 有三层:

  1. patch 前注册原始 console/fetch/XHR functions。
  2. Guard 同步 logger execution,让 reentrant capture 被丢弃并计数。
  3. 保留 record.source,让 transports 过滤 self-generated records。

必需 browser integrations:

  • console capture for log, info, warn, error, debug, and trace
  • global script/resource errors
  • unhandledrejection
  • optional securitypolicyviolation
  • fetch 和 XHR HTTP error collection
  • page lifecycle flush hooks
  • optional offline replay hooks

必需 Node integrations:

  • uncaughtException
  • unhandledRejection
  • warning
  • beforeExit/exit flush handling
  • diagnostics_channel subscriptions for undici 和 Node HTTP where available

Node crash behavior 必须诚实。启用 exitOnUncaught 时,fatal capture 应 flush sync-capable transports,对其余 transports 尝试有界 async flush,然后保留 process exit semantics。Integration 不得静默把 fatal crashes 变成 zombie processes。

Routing

Routing 使用 category、level、source、tags/type 和显式 transport filters。

配置应支持:

  • category prefix rules
  • per-transport minimum levels
  • source exclusions,例如从 console transport 排除 integration:console
  • per-transport middleware
  • 可在 presets 中复用的 named routes

Routing 必须解析成 immutable runtime snapshots,避免每次 log call 做昂贵 dynamic config lookup。

性能预算

初始内部预算:

PathTarget
Disabled level call一次数值比较,零分配
Enabled record to queue, 3 middleware, no stack主流桌面 CPU 上 <= 1 microsecond per record
JSON/NDJSON codec对普通对象达到 million-records-per-second 级别
Node NDJSON full pathv1 前在等价输出下至少达到 pino 的 80%
Core size<= 4 KB min+gzip(理想目标,尚未达到,见下方说明)
Record allocation一个 record object;除非 middleware 显式 clone,否则不复制 data

截至 2026-06,上方 <= 4 KB core-size 行是尚未达到的 aspiration,不是当前状态。当前测量值(并由 pnpm size:check 强制):完整 @loggerjs/core barrel 约 18 KB gzip;最小 tree-shaken import(createLogger + 一个 consoleTransport)约 6 KB min+gzip。在预算真正达到前,公开 size 表述应锚定这些测量数字。

Benchmark 必须覆盖 Node 和真实浏览器,而不只是 synthetic Node loops。套件应比较 pino、winston、LogTape、native console、native JSON.stringify、当前 LoggerJS 和目标 LoggerJS paths。

决策:保留 record pipeline,通过 codec-owned preparation 优化

截至 2026-06,在参考机器(Apple M1 Max,Node v22.21.1)上,用 drift-canceling paired A/B harness 测得 lean Node NDJSON path 约为 pino 的 1.19x,codec-owned prepared lean path 约为 1.28x,即在等价输出下 快于 pino。Full-envelope path 约为 pino 的 0.9x,同时在 pino 字段外额外输出 idseqlevelName(见 docs/BENCHMARKS.md)。这个排序 依赖 CPU/Node-V8:pino 和 loggerjs 都使用手调 JSON hot paths,当前文档把差异视为经验 benchmark 结果,而不归因于低层机制。不同芯片或 Node/V8 构建上 pino 可能领先。重点是 loggerjs 在 不把序列化移入 logger 的前提下达到 pino 同级别。

达成这一点经历了 2026-06 的 profiling,也修正了早前“差距是结构性的,不是未优化代码”的过度表述。三个不改变架构的改动,把本机 lean ratio 从约 1.30x pino 推到约 0.84x:第一,未配置 ambient context 时,getContext 不再每次执行 addedProviders.map() + spread + mergeContext({});第二,fastEventJsonCodec 在 codec 创建时 bake includeX toggles,并用单个 template 输出 header;第三,codec-owned prepared record encoders 让 transports 复用 logger/category/tags fragments,而不让 logger 拥有 JSON serialization。

LoggerJS 仍然每条日志分配一个 LogRecord,让 middleware、processors、integrations 和多个 transports 观察同一个共享值;codec 仍拥有 never-throw safe-fallback contract,而且在已测硬件上已经追平或超过 pino。默认拒绝“当 logger 正好只有一个 sync transport 且没有 middleware 时绕过 record 的 fusion fast path”,理由更充分,因为它会:

  • 制造性能悬崖:新增第一个 middleware 会静默损失 30%+ 吞吐;
  • 把 serialization 移进 logger,破坏 codec-belongs-to-transport 边界;
  • 让每个语义变更都要维护两条 hot-path surface(2026-06 修复的 id-drift 和 source round-trip bugs 正是这种 dual-path 缺陷)。

剩余性能预算投向默认路径(batch enqueue、默认 codecs、prepared codec contracts)和 regression gating,而不是 fusion-only 峰值。只有真实生产用例证明单独语义 hot path 有价值时才重新讨论。

可靠性

默认语义是 best-effort at-most-once。LoggerJS 不会为了保证日志投递而无限期阻塞应用进度。

每条损失路径都必须可观测:

  • queue overflow
  • batch too large
  • retry exhausted
  • circuit breaker open
  • beacon failed
  • offline queue quota exceeded
  • flush deadline exceeded
  • integration loop guard drop
  • middleware/transport exception

这些 counters 应通过 meta logger 和可选 stats APIs 暴露。

隐私和安全

默认值:

  • redact 常见敏感 key:authorization、cookie、set-cookie、password、passwd、token、secret、apiKey、api_key 和 *_key
  • fetch/XHR integrations 不捕获 bodies
  • fetch/XHR integrations 不捕获 headers,除非 allowlisted
  • browser offline queue 默认关闭
  • 默认 builds 中无 eval 或 generated code
  • core 无 runtime dependencies

任何把 logs 写入 durable browser storage 的功能都必须显式启用,因为它改变应用隐私姿态。

测试策略

必需测试层:

  • core record construction、level gate、overloads、child context、middleware、router 和 transport errors 的单元测试
  • safe JSON 和 structured round-trip behavior 的 codec property tests
  • 同时启用 console transport 和 console integration 的 loop prevention tests
  • pagehide/beacon flush、fetch/XHR capture、global errors 和 offline queue behavior 的浏览器 Playwright tests
  • uncaught exception flush 和 exit semantics 的 Node child-process tests
  • Node、Bun、Deno 和 workerd/miniflare runtime smoke tests
  • core 和 integration packages 的 size-limit checks
  • 带显式阈值的 benchmark regression checks

任何 milestone 都不能在缺少 examples、tests,以及至少一个与变更层相关的 benchmark 或 size measurement 的情况下完成。

Package 方向

v0 package layout 可以支撑开发,但 v1 public layout 应朝以下方向移动:

text
@loggerjs/core
@loggerjs/transport-http
@loggerjs/transport-otlp
@loggerjs/transport-file
@loggerjs/transport-worker
@loggerjs/codec-structured
@loggerjs/codec-msgpack
@loggerjs/integration-console
@loggerjs/integration-global-errors
@loggerjs/integration-fetch
@loggerjs/integration-node
@loggerjs/otel
@loggerjs/sentry
@loggerjs/pretty
@loggerjs/browser    preset/meta package
@loggerjs/node       preset/meta package

允许 preset packages,但平台 API 的 ownership 应位于小包中,让用户只安装需要的 collection 和 transport surface。

v1 完成标准

LoggerJS 达到 v1 readiness 的条件:

  • core public API 由 API report 锁定
  • disabled hot path 和 enabled queue path 在 Node 与浏览器上达到预算
  • codec JSON、structured 和 msgpack paths 有 benchmark data
  • browser 和 Node integrations 有 loop 和 teardown tests
  • OTLP collector demo 能端到端工作
  • crash flush behavior 由 child process 测试覆盖
  • privacy defaults 被文档化并测试
  • examples 覆盖 browser、Node service、edge worker 和 OTLP collector
  • migration guide 覆盖 console.log、pino、winston 和 LogTape-style library logging

相关链接

基于 MIT License 发布。