Skip to content

传输 ​

transport 负责把日志 record 或 event 投递到某个目的地。本页列出所有内置 transport,并说明如何编写自己的 transport。精确的选项类型以各包的 TypeScript 声明和 api-reports/ 为准。

每个 transport 与源码文件、公开入口和契约测试的对应关系见 传输契约矩阵。

运行时支持 ​

运行时支持的 transport说明
Core / 与运行时无关consoleTransport, memoryTransport, testTransport, batchTransport, retryTransport, fallbackTransport不依赖浏览器或 Node.js 专有 API。包装器可以包住当前运行时中可用的任何 transport。
Pretty / 开发体验prettyConsoleTransport, prettyStreamTransport, prettyStdoutTransport, prettyStderrTransport来自 @loggerjs/pretty 的浏览器 DevTools 和 Node 终端显示 transport,用于人类可读的输出,不用于持久的生产投递。
浏览器 / 前端browserHttpTransport、IndexedDB 队列/存储、WebSocket、service worker、BroadcastChannel、offline-first 重放使用 fetch、sendBeacon、IndexedDB、navigator.onLine、service worker 和 BroadcastChannel 等浏览器 API,并在可行处做特性检测和回退。
Node.js / 服务端stdoutTransport, stderrTransport, fileTransport, rotatingFileTransport, nodeHttpTransport, nodeSyslogTransport, workerTransport使用 Node.js 的流、文件系统、worker 线程、网络 socket 和 Node fetch。
厂商 / 可观测性OTLP, Sentry, Datadog, Elastic, Loki, CloudWatchHTTP 协议类 transport 可以运行在满足其 fetch/crypto/运行时要求的环境中;SDK/provider 适配器需要应用传入已初始化的 SDK 对象或 provider。厂商凭据通常更适合放在服务端或可信的 worker 中。
数据库 / 本地应用 / 后端databaseTransport, postgresTransport, sqliteTransportLoggerJS 层不绑定具体驱动,但应用必须提供数据库驱动;适用于 Node.js、Electron、CLI 或后端 worker。

稳定性分级 ​

transport 的稳定性描述的是公开 API 的承诺,而不是绝对的投递保证。浏览器存储、进程关闭、网络收集端和厂商后端都可能失败;投递契约以下方的可靠性表为准。

级别含义
Stable受 1.x 兼容承诺保护。选项名和高层语义受 API 报告、测试和文档保护。
Compatible公开且有测试,但具体运行时行为或消息结构仍可能在更早的 minor 中先弃用后再调整。文档中的注意事项符合你的部署场景时再使用。
Experimental公开且有测试,生产使用最少。名称、选项、payload 映射或批量建议可能在更早的 minor 中先弃用后再变化。
Runtime-dependent公开 API 稳定,但实际可靠性很大程度上取决于 LoggerJS 之外的浏览器、worker、存储、网络、SDK 或数据库行为。请在目标环境中验证。
Test-only为断言和测试夹具而设计,不用于生产投递。
Transport稳定性原因
consoleTransport()Stable与运行时无关的本地 sink,针对 console 采集做了防回环处理。
memoryTransport()Stable有界的内存诊断缓存,有意不做持久化。
testTransport()Test-only带等待和快照 API 的断言辅助工具。
batchTransport() / retryTransport() / fallbackTransport()Stable一方 transport 共同使用的 core 可靠性包装器。
Pretty transportsStable开发显示 API 稳定;具体颜色和布局属于展示细节。
stdoutTransport() / stderrTransport() / fileTransport()Stable生产用本地 sink,具备 drain 和崩溃路径处理。
rotatingFileTransport()Stable本地按大小轮转;每个文件只应有一个写入进程。
nodeHttpTransport()Stable内置批量包装的 HTTP 投递,使用共享的可靠性选项。
otlpHttpTransport()ExperimentalOTLP 映射公开且有测试,但可观测性适配包的生产使用最少。
nodeSyslogTransport()Compatible消息格式有测试覆盖,但在更多部署验证之前不属于 v1 内核;UDP/TCP 的可靠性遵循 syslog 传输本身的语义。
workerTransport()Compatible消息协议公开,但 ready/ack/回退等生命周期细节可能继续调整。
browserHttpTransport()Stable主要的浏览器远程 transport;pagehide 时的 Beacon 仍是尽力而为。
memoryBrowserHttpOfflineQueue()Stable用于短暂离线的稳定 API;刷新页面后不保留。
indexedDbBrowserHttpOfflineQueue() / indexedDbTransport() / offlineFirstTransport()Runtime-dependentAPI 稳定,但持久性取决于浏览器的 IndexedDB、配额、驱逐策略、隐私模式和存储策略。
browserWebSocketTransport()Compatible适合实时/调试通道;重连和最终持久性由调用方负责。
browserServiceWorkerTransport()Runtime-dependentAPI 公开,但投递取决于 service worker 的注册、激活和生命周期。
browserBroadcastChannelTransport()Compatible同源标签页之间的分发本身就可能丢失,并取决于是否有接收方。
Datadog / Elastic / Loki / CloudWatch transportsExperimental线上 payload 有测试,但厂商包的生产使用最少;生产环境需要在原始 transport 外包一层批量/重试才能保证持久性。
sentryTransport() / openTelemetryLogBridgeTransport()Experimental适配器契约公开且有测试,但 SDK/provider 映射仍可能在弃用之后变化。
databaseTransport() / sqliteTransport() / postgresTransport()Experimental适配器 API 公开且有测试,但驱动事务和表结构约定还需要更多真实用户验证。

导入边界 ​

根入口是便捷预设。公开的 transport 子路径都列在这里,方便用户选择更小的 bundle,也防止新的内置 transport 在没有文档的情况下悄悄扩大 API。

运行时公开的 transport 子路径
Core@loggerjs/core/transport-console, @loggerjs/core/transport-batch, @loggerjs/core/transport-reliability, @loggerjs/core/transport-test
Browser@loggerjs/browser/transport-http, @loggerjs/browser/transport-broadcast-channel, @loggerjs/browser/transport-service-worker, @loggerjs/browser/transport-websocket, @loggerjs/browser/transport-indexeddb, @loggerjs/browser/offline-first-transport
Node.js@loggerjs/node/transport-http, @loggerjs/node/transport-file, @loggerjs/node/transport-rotating-file, @loggerjs/node/transport-stdout, @loggerjs/node/transport-syslog, @loggerjs/node/transport-worker
Pretty@loggerjs/pretty/transport-console, @loggerjs/pretty/transport-stream
可观测性与数据@loggerjs/otel/transport-http, @loggerjs/sentry/transport, @loggerjs/datadog/transport, @loggerjs/elastic/transport, @loggerjs/loki/transport, @loggerjs/cloudwatch/transport, @loggerjs/database/transport

如果某个公开的 transport 子路径被导出却没有列在这里,pnpm verify:component-docs 会失败。新增条目时也应同步更新上方的稳定性表和可靠性表。

可靠性特性 ​

transport 默认是可组合的。有些 transport 内部自带批量或本地持久化;原始的厂商协议 transport 除非被包装,否则不会重试。请把下表当作生产投递契约:

Transport 或包装器默认行为生产说明
consoleTransport()立即本地输出面向人或开发环境;除 console 本身外没有重试或持久化。
prettyConsoleTransport() / prettyStdoutTransport() / prettyStderrTransport()立即输出人类可读的本地日志仅用于开发体验。生产投递请使用结构化 transport。
memoryTransport()内存环形缓冲区仅作诊断缓存;进程或页面退出即丢失。
testTransport()内存断言 sink仅用于测试,不是生产投递机制。
batchTransport(inner)批量队列,可选重试和熔断原始 I/O transport 需要队列上限、重试、退避或丢弃计数时使用。
retryTransport(inner)立即投递并重试内部 transport 已自带批量,或可以接受逐次调用重试时使用。
fallbackTransport(primary, fallback)主 transport 失败后改用备用 transport用于本地备份 sink,不能代替队列。
stdoutTransport() / stderrTransport()立即写入流,flush() 会等待 drain,可选 minLength 缓冲本地进程 sink;默认把 EPIPE 视为正常关闭。
fileTransport()共享的文件目的地,支持异步流模式、可选 sync: true、mkdir、append、minLength 和崩溃路径的 flushSync()本地持久化路径;每个文件最好只有一个写入进程。
rotatingFileTransport()带按大小轮转的同步共享文件目的地带轮转的本地持久化路径;写入时会阻塞调用方。
nodeHttpTransport()内置批量包装的 HTTP 投递基于 batchTransport;生产环境请调整队列、重试和熔断选项。
nodeSyslogTransport()立即通过 UDP/TCP 写入 syslogUDP 可能丢失;TCP 仍取决于 socket 状态和 close/flush 行为。
workerTransport()卸载到 worker,可选 ready/ack 生命周期默认只发不管;需要确认 worker 已接收时,配置 readyTimeoutMs、ackTimeoutMs、fallback 和 autoEnd;配置了 ready 握手后,ready() 会等待 worker 启动。
browserHttpTransport()批量 fetch,可选离线队列和 pagehide 时的 Beacon 模式需要在刷新后保留时使用 IndexedDB 队列;Beacon 模式是尽力而为,且有大小限制。
memoryBrowserHttpOfflineQueue()内存离线队列能扛过网络中断,但扛不过页面刷新或关闭标签页。
indexedDbBrowserHttpOfflineQueue()IndexedDB 离线队列在配额和存储可用时,刷新后仍然保留。
offlineFirstTransport(remote)远端投递,加持久化队列重放离线或远端失败时入队,之后重放。
indexedDbTransport()本地 IndexedDB 持久化本地支持/导出存储;持久性取决于浏览器存储策略和配额。
browserWebSocketTransport()socket 关闭时排队重连由调用方负责;有界队列满时会丢弃。
browserServiceWorkerTransport()排队直到有活跃的 service worker;target: "ready" 时 ready() 可以等待 serviceWorker.ready投递取决于注册、激活和 worker 生命周期。
browserBroadcastChannelTransport()可能丢失的标签页广播接收方必须已在监听;不持久化。
otlpHttpTransport()内置批量包装的 OTLP/HTTP 投递基于 batchTransport;生产环境请调整重试和熔断选项。
Datadog / Elastic / Loki / CloudWatch transports原始 HTTP 协议投递用 batchTransport() / retryTransport() 包装,以获得排队、重试和熔断能力。
sentryTransport() / openTelemetryLogBridgeTransport()SDK/provider 适配器可靠性取决于你传入的 SDK/provider。
databaseTransport() / sqliteTransport() / postgresTransport()批量写入数据库实际的事务和连接行为由适配器/驱动负责。

Core / 与运行时无关(@loggerjs/core) ​

Transport功能
consoleTransport()按级别输出易读的 console 日志,或在 pretty: false 时输出单行 JSON。通过未被 patch 的 console 输出,避免与 console 采集形成回环。默认过滤从 console 采集到的事件。
memoryTransport()保存最近事件的环形缓冲区(maxEvents,默认 1000)。适合诊断接口和测试。
testTransport()面向断言的 sink:快照、调用统计、waitFor()/waitForCount()、可注入的失败。
batchTransport(inner, options)为任意 transport 添加批量、重试和可靠性控制(见下文)。
retryTransport(inner, options)为任意 transport 添加重试、指数退避、可选熔断和可选回退。onDrop(event, reason) 会报告它放弃的每个事件:retry-exhausted(没有回退)、circuit-open(没有回退)或 fallback-failed。
fallbackTransport(primary, fallback, options)主 transport 抛错时改发到备用 transport。onDrop(event, "fallback-failed") 会报告两个 transport 都没有接收的事件。

batchTransport 可靠性选项 ​

所有基于批量的 transport 都共享这组选项:

ts
batchTransport(inner, {
  maxRecords: 100,          // 队列达到这个数量时 flush
  maxBytes: 64 * 1024,      // 每 batch 字节预算(仅设置时估算)
  maxWaitMs: 2000,          // flush timer
  maxQueueSize: 1000,       // backpressure bound
  dropPolicy: "drop-oldest" /* | "drop-newest" | "throw" */,
  concurrency: 2,           // 并行 in-flight batches
  maxRetries: 3,
  retryBaseDelayMs: 250,    // exponential backoff base
  retryMaxDelayMs: 5000,
  circuitBreakerFailureThreshold: 5,
  circuitBreakerResetMs: 30000,
  onDrop: (event, reason) => metrics.increment(`log_drop.${reason}`),
});

说明:

  • 字节估算需要遍历 payload;只有 maxBytes 为有限值时才会执行。
  • 丢弃总会计入 logger meta(transport.dropped.*);只有注册了 onDrop 回调时,才会为它把 record 转换成 event。
  • 失败的批次会重新放回队首;熔断器可以避免不断请求一个已经挂掉的端点。
  • 会遵守 Retry-After。当投递错误带有 retryAfterMs(HTTP transport 会从 429 或 503 响应的 Retry-After 头中解析出来)时,下一次尝试至少等待这么久。如果等待时间超过 retryMaxDelayMs,会改为结束本轮重试(计入 transport.retry.deferred):批次放回队列,等待结束前不发送任何内容,因此 flush() 和 close() 不会被它卡住。retryTransport() 遵循同样的规则,等待时间超过 retryMaxDelayMs 时直接放弃(retry-exhausted)。
  • 投递失败后放回队列的批次会原样重发:新事件排在它后面,不会并入这个批次,因此由批次事件推导出的内容(例如幂等键)保持不变。
  • close() 会最后尝试 flush 一次,然后停止 flush 定时器,并且无论成败都会关闭内部 transport。未能投递的记录以及关闭后写入的记录都计为 transport.dropped.closed;最后一次 flush 的错误仍会抛出。

retryTransport() 和 fallbackTransport() 没有队列:放弃投递时,这次投递会 reject,事件计入 transport.dropped.*,并逐个交给 onDrop。从该包装层的角度看这是最终的丢弃;如果外层 transport 自己还会重试被 reject 的投递(例如 batchTransport() 会把失败的批次重新入队),这些事件之后仍可能送达。请直接使用 batchTransport() 自带的重试选项,而不要在其中嵌套 retryTransport()。

Pretty / 开发体验(@loggerjs/pretty) ​

Transport / 辅助函数功能
prettyConsoleTransport()浏览器 DevTools 和本地 console 输出:级别标签、易读的详情、可选的 %c 浏览器样式、原始对象参数,并过滤 console 采集回环。
prettyStreamTransport({ stream })向任意可写的类流目标写入人类可读的行。按配置或自动检测决定是否使用 ANSI 颜色。
prettyStdoutTransport() / prettyStderrTransport()基于 process.stdout / process.stderr 的 Node 终端辅助函数;遵循 NO_COLOR 和 FORCE_COLOR,支持 minLevel,flush() 会等待 drain。
formatPrettyEvent()供自定义显示 transport 使用的共享格式化函数,返回纯文本、ANSI 文本、浏览器 console 参数和原始详情。

pretty transport 是显示用的 sink,不做批量、不重试、不持久化,也不实现收集端协议。示例和选项建议见 友好输出。

Node.js / 服务端(@loggerjs/node) ​

Transport功能
stdoutTransport() / stderrTransport()输出 NDJSON 行,跟踪写入背压,妥善处理 EPIPE,可选 minLength 缓冲;flush() 会等待未完成的写入。
fileTransport({ path })默认以追加方式把 NDJSON 写入文件;支持 mkdir、append: false、异步 minLength 缓冲、sync: true 和崩溃路径的 flushSync()。
rotatingFileTransport({ path, maxBytes, maxFiles })通过同一个文件目的地按大小轮转,生成带编号的归档文件。同步写入;每个文件只应由一个 logger 进程写入。如果轮转失败(例如 Windows 上另一个进程占用文件导致 EBUSY/EPERM),错误会以 operation: "rotate" 报告,日志继续写入当前文件,并在再写入一个 maxBytes 后重试轮转。
nodeHttpTransport({ url })基于 fetch 的 HTTP 投递,内部用 batchTransport 包装。timeoutMs(默认 10000,设为 0 关闭)会中止收集端接受连接却一直不响应的请求,避免 flush() 和 close() 卡住停机流程;超时的尝试按重试设置处理。收集端可能已经处理了超时的请求,因此重试可能让同一批次到达两次。设置 idempotencyKeyHeader(例如 "Idempotency-Key")后,每个请求都会带上一个幂等键;同一批次的每次重发都使用相同的键,收集端据此丢弃重复的请求。键以每个 transport 随机生成的前缀开头,因此不同进程之间不会出现相同的键。
nodeSyslogTransport()通过 UDP/TCP 发送 RFC 格式的 syslog 消息;formatSyslogMessage() 也单独导出。
workerTransport({ workerScript })用 codec 编码批次并发送到 worker 线程,可选转移 buffer;支持 ready 超时、等待批次 ack、回退和 autoEnd。

当写入错误(ENOSPC、EACCES、EISDIR)已经销毁了底层流时,文件和流目的地会以该流错误结束 flush() 和 close(),而不是一直等待永远不会到来的 drain 事件。

nodeHttpTransport() 接受 transformPayload,在 codec 编码之后对线上 payload 做转换。gzip、brotli 或 deflate 压缩使用 nodeCompressionPayloadTransform():

ts
import { nodeHttpTransport } from "@loggerjs/node";
import { nodeCompressionPayloadTransform } from "@loggerjs/node/payload-transforms";

nodeHttpTransport({
  url: "https://collector.example/logs",
  transformPayload: nodeCompressionPayloadTransform({ format: "brotli" }),
});

fileTransport().flushSync() 是崩溃路径专用的原语。在异步流模式下,它会通过同步文件描述符写出当前缓冲或待写的 payload,让致命错误日志在进程退出前落盘;如果进程继续运行,原来的异步流可能仍会完成写入。正常的“排空后继续运行”式关闭请使用 await flush();如果每次写入都必须同步完成,请配置 sync: true。

workerTransport() 仍然兼容只接收对象消息的简单 worker。生命周期协议需要主动开启:

  • 如果 worker 会发送 { type: "loggerjs:ready" },设置 readyTimeoutMs。超时后 LoggerJS 会把 worker 标记为失败,并把批次交给配置的 fallback,或计为 transport.dropped.worker-ready-timeout。显式调用 transport.ready() / logger.ready() 也会等待这个启动握手。
  • 如果 worker 会用 { type: "loggerjs:batch:ack", id } 确认每个批次,设置 ackTimeoutMs。flush() 会等待这些确认。
  • 主线程发送的消息是 { type: "loggerjs:batch", id?, codec, contentType, count, payload }。
  • worker 可以用 { type: "loggerjs:error", message, error } 报告失败;待确认的批次会交给 fallback 或计为丢弃。
  • autoEnd 默认为 true;如果 worker 是共享的、不应在 transport close() 时被终止,请设置 autoEnd: false。

worker 生命周期会更新标准的 transport 指标 transport.ready.<name> 和 transport.queue.depth.<name>;等待 ack 失败会计入 transport.worker.pending-dropped 和 transport.dropped.<reason>。

如果需要 Node 运行时诊断,可以调用 @loggerjs/node 的 installLoggerDiagnosticsChannel()。它会把被订阅的 LoggerJS 内部阶段发布到 Node diagnostics_channel 的这些频道:loggerjs.dispatch、loggerjs.transport、loggerjs.flush、loggerjs.encode 和 loggerjs.worker。

浏览器 / 前端(@loggerjs/browser) ​

Transport功能
browserHttpTransport({ url })批量 HTTP 投递,支持离线队列、恢复在线后带退避的重放,以及页面隐藏时的 sendBeacon(按 beaconMaxBytes 分块)。
memoryBrowserHttpOfflineQueue()内存离线队列适配器(刷新后丢失)。
indexedDbBrowserHttpOfflineQueue()基于 IndexedDB 的持久离线队列,刷新后仍保留。
offlineFirstTransport(remote)标准的“远端 + 持久化队列”包装器:离线或远端投递失败时入队,之后重放。
indexedDbTransport()把日志持久化到本地 IndexedDB,支持按 session 建索引、按 TTL/条数/字节清理、持久性提示、可选的 Storage Bucket 隔离、异步 query() API、sessions() 和 stats() 统计。
browserWebSocketTransport({ socket })通过 WebSocket 发送 codec 编码的批次;socket 关闭时排队(重连由调用方负责)。
browserServiceWorkerTransport()把事件发送给 service worker,在有活跃 worker 之前排队;target: "ready" 时,显式调用 ready() 会等待 serviceWorker.ready。
browserBroadcastChannelTransport({ channel })把日志分发到其他标签页(本身就可能丢失;接收方必须正在监听)。
exportLogsToZip(source) / createLogZipBlob() / downloadBlob()把日志(例如来自 indexedDbTransport().query())打包成 ZIP,包含 manifest、可选的按 session 拆分文件、可选的 recent.ndjson/recent.json 和 CRC,用于技术支持流程。

browserHttpTransport() 会遵守 429 和 503 响应中的 Retry-After:在等待结束前,定时发送、批量已满时的发送、显式 flush() 和离线重放都会暂停,而页面退出时的 Beacon flush 照常发出。跨域收集端必须把 Retry-After 列在 Access-Control-Expose-Headers 中,否则浏览器读不到它。browserHttpTransport() 会在 timeoutMs(默认 10000,设为 0 关闭)后中止 Fetch 投递。浏览器自身永远不会让请求超时,没有它时一个卡住的请求会挡住之后所有的 flush 和 close();配置了离线队列时,超时的批次会被存下来以便重放。超时的请求可能已经被处理,因此同一批次可能到达两次。设置 idempotencyKeyHeader 后,每个 Fetch 请求都会带上幂等键:重试、保留批次的重发和离线重放都使用该批次原来的键(离线条目会把它一并存下),收集端据此丢弃重复的请求。跨域收集端必须在 Access-Control-Allow-Headers 中允许这个请求头,页面退出时的 Beacon 请求无法携带它。close() 是终态:它无法投递或存储的事件,以及之后再记录的事件,都会通过 onDrop 和 transport.dropped.closed 报告。

indexedDbTransport() 在某个批次的 IndexedDB 写入失败时,会通过 onDrop 报告其中每个事件:QuotaExceededError 的原因为 quota,其他情况为 write-failed。indexedDbTransport() 和 indexedDbBrowserHttpOfflineQueue() 在另一个标签页需要升级或删除数据库(例如新版本应用)时会关闭连接,并在下次使用时重新打开。close() 之后再写入 indexedDbTransport() 的事件会以 closed 原因报告为丢弃。

browserHttpTransport() 在普通 Fetch 投递中使用 codec。如果 pagehide 或页面隐藏时的 Beacon 请求需要不同的编码或 content type,可以设置 beaconCodec;未设置时回退到 codec。配置了 transformPayload 时会跳过 Beacon 投递,生命周期 flush 改走普通 Fetch 路径,beaconCodec 也就不会生效。

Beacon 投递无法确认。在 pagehide 和页面变为隐藏时(除非 useBeaconOnPageHide 为 false),以及在 close() 中,browserHttpTransport() 会把队列中的事件交给 navigator.sendBeacon(),而它只报告浏览器是否接收了这个请求。浏览器可能在页面关闭后才发出请求,没有任何机制报告收集端是否收到。被接收的请求中的事件会离开队列并计入 transport.beacon.accepted;它们永远不会通过 onDrop 报告,这是投递计数中唯一的例外。浏览器会限制同时在途的 Beacon 数据量。被拒绝的请求中的事件计入 transport.beacon.rejected,并留在队列中等待下一次 Fetch 发送,close() 会用 Fetch 发送它们,或将其报告为丢弃。大于 beaconMaxBytes 的事件会以 beacon-too-large 原因丢弃。Beacon 请求不能携带自定义请求头,因此既没有幂等键,也不会带上 headers。如果每个事件都很重要,请在可控的时机(例如应用内导航之前)通过 Fetch flush,减少留到页面退出时的数据,并把收集端的接收记录与 transport.beacon.accepted 对照。

browserHttpTransport() 同样接受 transformPayload。在支持 CompressionStream 的浏览器中使用 browserCompressionPayloadTransform():

ts
import { browserHttpTransport } from "@loggerjs/browser";
import { browserCompressionPayloadTransform } from "@loggerjs/browser/payload-transforms";

browserHttpTransport({
  url: "/api/logs",
  transformPayload: browserCompressionPayloadTransform({ format: "gzip" }),
});

在较新的 Chrome 上做高吞吐的本地浏览器采集时,推荐使用独立的 IndexedDB 日志存储,并设置宽松持久性:

ts
indexedDbTransport({
  durability: "relaxed",
  localStorageSpill: {
    maxBytes: 512 * 1024,
    maxEntries: 200,
    namespace: "loggerjs-support",
  },
  storageBucketName: "loggerjs-logs",
  storageBucketDurability: "relaxed",
});

不支持 Storage Buckets 的浏览器会回退到普通的 IndexedDB 实例,transport API 保持不变。

indexedDbTransport() 默认会分配一个页面 session id,把它作为 IndexedDB 条目的顶层字段保存,并在事件没有 event.context.sessionId 时把同一个值写入 context。传入 session: false 可以关闭这个 session 字段;传入 session: { id, getId, contextKey } 可以让持久化的 session 与你自己的浏览器 context provider 保持一致。

localStorageSpill 是页面刷新或关闭前的最后一道保护,不是 IndexedDB 的替代品。正常情况下日志仍先在内存中批量,再异步 flush 到 IndexedDB。在 pagehide 或 visibilitychange: hidden 时,transport 会把尚未确认写入的尾部日志(pendingFlushBatch 加上当前内存缓冲)同步写入一个很小的 localStorage 临时条目。下一个 transport 实例会在第一次 flush 之前先把这个临时条目导入 IndexedDB,写入成功后才清除它。这能减少普通刷新和关闭标签页时的日志丢失,但无法防止进程被杀、浏览器崩溃、存储被禁用、配额耗尽或存储被驱逐。

浏览器的失败边界 ​

除非日志已经被你关心的目的地确认接收,浏览器投递都只是尽力而为。需要关注的丢失窗口:

路径失败边界 / 丢失窗口生产建议
browserHttpTransport()内存中的批次会在页面刷新、关闭标签页、进程被杀,或队列上限在投递前丢弃记录时丢失。页面跳转也可能中断 fetch。使用有界队列和重试选项;需要在刷新后保留时,加上 IndexedDB 离线队列。
browserHttpTransport({ useBeaconOnPageHide: true })sendBeacon 只发不管:被接收的请求永远得不到确认,其中的事件计入 transport.beacon.accepted,而不会通过 onDrop 报告。浏览器会限制在途的 Beacon 数据量,可能拒绝请求(计入 transport.beacon.rejected,事件留在队列中),也可能在关闭过程中丢失请求。beaconMaxBytes 设得保守些,在可控的时机通过 Fetch flush,把页面退出时的 flush 当作最后机会,而不是唯一的持久化路径;需要完整性时,把收集端的接收记录与 transport.beacon.accepted 对照。
memoryBrowserHttpOfflineQueue()只要页面进程存活,就能扛过临时离线。适合轻量应用或测试;支持/调试日志需要在刷新后保留时,改用 IndexedDB。
indexedDbBrowserHttpOfflineQueue()在刷新之间保存待重放的 payload,但配额、隐私浏览模式、存储驱逐、升级被阻塞或 IndexedDB 不可用仍可能导致无法持久化。监控队列和丢弃计数,保持 payload 有界;与 HTTP 重放和页面生命周期 flush 搭配使用。
offlineFirstTransport(remote)远端投递失败时入队,之后重放。如果本地存储失败或被驱逐,重放也无法保证。优先使用持久化的队列适配器;在可控的关闭或跳转时尽量调用 flush()。
indexedDbTransport()本地持久性取决于 IndexedDB 是否可用、配额、驱逐策略、持久性提示以及浏览器对 Storage Buckets 的支持。仍在内存缓冲中、尚未完成异步写入的日志可能丢失。可以接受时用 durability: "relaxed" 提高吞吐;用 TTL/条数/字节清理保持在配额以内。需要支持日志更可靠地扛过普通刷新时,启用有界的 localStorageSpill。
browserWebSocketTransport()页面退出、超出队列上限,或调用方始终没有重连 socket 时,排队中的事件可能丢失。在 transport 之外处理重连,并通过队列上限和丢弃计数发现背压。
browserServiceWorkerTransport()投递取决于 service worker 的注册、激活、消息传递和生命周期。被终止的 worker 会丢失正在处理的工作,除非它自己持久化队列。把它当作集中汇聚的手段,而不是持久化手段,除非 service worker 自己也写入持久存储。
browserBroadcastChannelTransport()BroadcastChannel 只能送达当前打开、同源且正在监听的标签页。消息不持久化,接收方启动期间可能错过消息。用于多标签页汇总和调试,不要作为主要的远程投递保证。

常见的生产浏览器组合是:HTTP 批量 + IndexedDB 离线队列 + 页面生命周期 flush。需要跨标签页集中汇聚时再加 service worker 或 BroadcastChannel;日志必须在刷新后保留时,投递路径上仍然要有持久化队列。

Payload 转换 ​

payload 转换在 codec 编码之后、协议类 transport 发送或存储 payload 之前运行。它可以返回替换后的 payload,也可以返回 { payload, headers, contentType };HTTP transport 会把这些 header 一并保存到离线队列,并在重放时保留。

ts
import {
  composePayloadTransforms,
  encryptionPayloadTransform,
} from "@loggerjs/core/payload-transforms";
import { browserHttpTransport } from "@loggerjs/browser";
import { browserCompressionPayloadTransform } from "@loggerjs/browser/payload-transforms";

browserHttpTransport({
  url: "/api/logs",
  transformPayload: composePayloadTransforms(
    browserCompressionPayloadTransform(),
    encryptionPayloadTransform({
      contentType: "application/octet-stream",
      headers: { "x-payload-encrypted": "1" },
      encrypt: async (payload) => encryptForCollector(payload),
    }),
  ),
});

encryptionPayloadTransform() 只提供挂载点;加密算法和密钥管理仍由应用负责。

厂商包 ​

厂商 HTTP transport 通过 fetch 直接实现线上协议。Sentry 和 OpenTelemetry bridge 这类 SDK/provider 适配器使用应用已经初始化好的 SDK 对象或 provider。otlpHttpTransport() 内部已用 batchTransport 包装;Datadog、Elastic、Loki 和 CloudWatch 只实现了 logBatch,需要排队、重试或熔断时,请用 core 的可靠性包装器包住它们。

生产环境中使用厂商 transport 时,应显式写出可靠性包装:

ts
import { batchTransport } from "@loggerjs/core";
import { datadogLogsTransport } from "@loggerjs/datadog";

const transport = batchTransport(datadogLogsTransport({ apiKey: process.env.DD_API_KEY }), {
  maxRecords: 100,
  maxWaitMs: 2000,
  maxQueueSize: 5000,
  maxRetries: 3,
  circuitBreakerFailureThreshold: 5,
});
包Transport目的地
@loggerjs/otelotlpHttpTransport({ url })OTLP/HTTP JSON 日志端点;同时导出 otlpJsonCodec() 和映射辅助函数。
@loggerjs/otelopenTelemetryLogBridgeTransport()桥接到 OpenTelemetry LoggerProvider。
@loggerjs/sentrysentryTransport({ sentry })Sentry 结构化日志、breadcrumbs、异常/消息捕获。
@loggerjs/datadogdatadogLogsTransport({ apiKey })Datadog Logs intake API。
@loggerjs/elasticelasticTransport({ url, index })Elasticsearch _bulk API,可按记录选择 index/pipeline/id。
@loggerjs/lokilokiTransport({ url })Grafana Loki push API,支持 stream 标签和结构化元数据。
@loggerjs/cloudwatchcloudWatchLogsTransport({ ... })CloudWatch Logs PutLogEvents,内置 SigV4 签名。
@loggerjs/databasesqliteTransport() / postgresTransport() / databaseTransport(adapter)通过不绑定驱动的适配器批量插入。

编写自定义 Transport ​

对于非 2xx 响应抛出 @loggerjs/core 中 httpStatusError(name, response) 的 HTTP transport,会自动获得 batchTransport() 和 retryTransport() 的 Retry-After 处理:这个错误带有 status 和解析出的 retryAfterMs。也可以单独用 parseRetryAfter(value) 解析这个响应头。

实现四种投递方法中的任意一种即可。最简单的 event transport:

ts
import type { Transport } from "@loggerjs/core";

const myTransport: Transport = {
  name: "my-sink",
  minLevel: "info",
  log(event) {
    push(JSON.stringify(event));
  },
};

支持 record 的 transport 可以走快速路径(logger 没有 processor 时不做 event 投影):

ts
import { fastEventJsonCodec } from "@loggerjs/codecs";
import { createPreparedRecordEncoder } from "@loggerjs/core/codec-prepared";

const codec = fastEventJsonCodec();
const encodeRecord = createPreparedRecordEncoder(codec);
const recordSink: Transport = {
  name: "record-sink",
  write(record, context) {
    push(encodeRecord(record));
    // 需要 event shape?context.toEvent(record) 转换一次并 memoize,
    // 其他 transports 会共享同一次 projection。
  },
};

注意事项:

  • 抛错(同步抛出或返回被拒绝的 promise)是安全的:错误会上报到 logger meta,其他 transport 继续运行。不要悄悄吞掉自己的错误,让它们暴露出来。
  • 如果调用方可能需要显式等待启动完成,实现 ready()。logger.ready() 需要主动调用;普通日志调用从不等待 transport 就绪。
  • 有缓冲时实现 flush();能在崩溃路径上同步排空时实现 flushSync();持有资源时实现 close()。
  • 如果实现了 close(),请在释放资源前自行尽力 flush。存在 close() 时 core 会调用它,只有没有 close() 的 transport 才会回退到调用 flush()。
  • 凡是涉及 I/O 的 transport,优先实现 logBatch/writeBatch 并配合 batchTransport;逐条发起网络请求扛不住生产流量。
  • 直接编码原始 record 会绕过 logger 的 idFactory,record 会使用文档中说明的 defaultRecordId。自定义 id 很重要时,请通过 context.toEvent() 转换。见 编解码。

相关链接 ​

基于 MIT License 发布。