LoggerJS 架构
状态:当前面向 v1 的代码库实现架构。 来源输入:
DESIGN.md、log.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:
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。
不可谈判的设计规则
- Core 平台中立。
@loggerjs/core不得 import browser、Node、Bun、Deno、worker、filesystem、fetch 或 diagnostics APIs。 - 禁用日志几乎免费。 禁用级别调用必须只做一次数字 level 比较,并在 record allocation、message stringification、context merge 或 integration work 前返回。
- 序列化只发生在 transport 边界。 管线保留原始引用。
resolveMessage(record)是唯一允许 middleware 触发的 lazy evaluation。 - Middleware 同步。 热路径中没有 promises、没有 Koa-style
next、没有 async lookup。 - Integrations 使用与手动日志相同的管线。 自动 records 只是在
source上不同;它们仍经过 middleware、routing、batching、codec 和 transport policy。 - Integrations 显式且可逆。 任何 monkey patch 都必须 opt-in、idempotent、防重入,并能完全 teardown。
- Logger 错误永远不逃逸到应用代码。 内部失败通过 rate-limited meta logger 计数和报告。
- v1 不使用 object pool。 短生命周期 records 应保持 young-generation GC objects,除非 benchmark 证明需要其他方案。
端到端管线
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 优化:
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或 immutablectx中。 time是Date.now();同一 timestamp 内用seq排序。err与props分离,因为 error encoding、stack truncation、cause handling 和 dedupe 是专门逻辑。
当前 LogEvent 形状可以暂时保留为 codec projection 或兼容类型,但 v1 rewrite 开始后不应驱动热路径。
Logger API
LoggerJS 支持两种 acquisition models:
const log = createLogger({
category: "app",
level: "info",
transports: [consoleTransport()]
});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()]
});必需调用形态:
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:
- 显式 context:
logger.child(bindings)。Child bindings 在 child 创建时 flatten 并 freeze。 - 隐式 context:
withContext(bindings, fn)。Node/Bun/Deno 使用 AsyncLocalStorage 或等价 conditional exports。浏览器初期降级为 synchronous-scope context,直到 TC39 AsyncContext 可用。
Codec-level context optimization 替代 pino-style global chindings:
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
目标接口:
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-writesample:按 level/category/key sampling,error 和 fatal 默认完整保留rateLimit:按 category/level/source 的 token bucketdedupe:fingerprinted burst collapsefingersCrossed:由 error trigger 释放的低级别 ring bufferenrich:同步 props/context enrichmenttags和type:当前 processor 行为的 thin compatibility helperstraceContext:OTel 或用户提供的 trace/span injection
Middleware 不得调用 JSON.stringify、String(record.props) 或递归标准化整个 records。如果需要 message,必须有意调用 resolveMessage(record)。
Transports
目标接口:
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 实现应支持:
maxRecordsmaxBytesmaxWaitMsconcurrency- 带 exponential backoff 和 full jitter 的 retry
drop-old和drop-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 下不应序列化。它应把原始 msg、props 和 err 引用传给原始 console methods,让浏览器 devtools 保持对象检查能力。
它必须使用 unpatched console registry,才能和 console capture 共存而不形成 feedback loops。
HTTP Transport
HTTP transport 是带平台实现的共享抽象:
- Browser:
fetch、keepalive、pagehide/visibilitychange上的sendBeacon、可选 IndexedDB offline queue,以及围绕 64 KiB beacon budget 的严格 payload limits。 - Node:global
fetch/undici、retry/circuit breaker,不宣称 sync crash flush。 - Edge:
waitUntilhook,用于 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 移出主线程。首选路径:
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
目标接口:
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 使用 nativeJSON.stringify,只在失败 branches 上安全 fallback。structuredCodec:保留丰富值的 codec,可对 Error、cause chains、AggregateError、circular/shared references、BigInt、Date、RegExp、URL、Map、Set、TypedArray、ArrayBuffer、undefined、NaN、infinities 和-0做对称 decode。msgpackCodec:二进制 batch codec,可以是 benchmark-proven custom subset,也可以是msgpackr的小 adapter。projectorCodec:自定义 wire schemas 的 utility adapter。
Structured codec 应使用 flat value-pool format,不使用 eval、new Function 或 recursive revivers。Decode 应是 JSON.parse 加确定性的 pointer restoration pass,使它 CSP-friendly,并适合 browser replay tools。
Integrations
目标接口:
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 有三层:
- patch 前注册原始 console/fetch/XHR functions。
- Guard 同步 logger execution,让 reentrant capture 被丢弃并计数。
- 保留
record.source,让 transports 过滤 self-generated records。
必需 browser integrations:
- console capture for
log,info,warn,error,debug, andtrace - global script/resource errors
unhandledrejection- optional
securitypolicyviolation - fetch 和 XHR HTTP error collection
- page lifecycle flush hooks
- optional offline replay hooks
必需 Node integrations:
uncaughtExceptionunhandledRejectionwarningbeforeExit/exitflush 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。
性能预算
初始内部预算:
| Path | Target |
|---|---|
| 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 path | v1 前在等价输出下至少达到 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 字段外额外输出 id、seq 和 levelName(见 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 应朝以下方向移动:
@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