传输
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, CloudWatch | HTTP 协议类 transport 可以运行在满足其 fetch/crypto/运行时要求的环境中;SDK/provider 适配器需要应用传入已初始化的 SDK 对象或 provider。厂商凭据通常更适合放在服务端或可信的 worker 中。 |
| 数据库 / 本地应用 / 后端 | databaseTransport, postgresTransport, sqliteTransport | LoggerJS 层不绑定具体驱动,但应用必须提供数据库驱动;适用于 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 transports | Stable | 开发显示 API 稳定;具体颜色和布局属于展示细节。 |
stdoutTransport() / stderrTransport() / fileTransport() | Stable | 生产用本地 sink,具备 drain 和崩溃路径处理。 |
rotatingFileTransport() | Stable | 本地按大小轮转;每个文件只应有一个写入进程。 |
nodeHttpTransport() | Stable | 内置批量包装的 HTTP 投递,使用共享的可靠性选项。 |
otlpHttpTransport() | Experimental | OTLP 映射公开且有测试,但可观测性适配包的生产使用最少。 |
nodeSyslogTransport() | Compatible | 消息格式有测试覆盖,但在更多部署验证之前不属于 v1 内核;UDP/TCP 的可靠性遵循 syslog 传输本身的语义。 |
workerTransport() | Compatible | 消息协议公开,但 ready/ack/回退等生命周期细节可能继续调整。 |
browserHttpTransport() | Stable | 主要的浏览器远程 transport;pagehide 时的 Beacon 仍是尽力而为。 |
memoryBrowserHttpOfflineQueue() | Stable | 用于短暂离线的稳定 API;刷新页面后不保留。 |
indexedDbBrowserHttpOfflineQueue() / indexedDbTransport() / offlineFirstTransport() | Runtime-dependent | API 稳定,但持久性取决于浏览器的 IndexedDB、配额、驱逐策略、隐私模式和存储策略。 |
browserWebSocketTransport() | Compatible | 适合实时/调试通道;重连和最终持久性由调用方负责。 |
browserServiceWorkerTransport() | Runtime-dependent | API 公开,但投递取决于 service worker 的注册、激活和生命周期。 |
browserBroadcastChannelTransport() | Compatible | 同源标签页之间的分发本身就可能丢失,并取决于是否有接收方。 |
| Datadog / Elastic / Loki / CloudWatch transports | Experimental | 线上 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 写入 syslog | UDP 可能丢失;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 都共享这组选项:
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():
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 是共享的、不应在 transportclose()时被终止,请设置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():
import { browserHttpTransport } from "@loggerjs/browser";
import { browserCompressionPayloadTransform } from "@loggerjs/browser/payload-transforms";
browserHttpTransport({
url: "/api/logs",
transformPayload: browserCompressionPayloadTransform({ format: "gzip" }),
});在较新的 Chrome 上做高吞吐的本地浏览器采集时,推荐使用独立的 IndexedDB 日志存储,并设置宽松持久性:
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 一并保存到离线队列,并在重放时保留。
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 时,应显式写出可靠性包装:
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/otel | otlpHttpTransport({ url }) | OTLP/HTTP JSON 日志端点;同时导出 otlpJsonCodec() 和映射辅助函数。 |
@loggerjs/otel | openTelemetryLogBridgeTransport() | 桥接到 OpenTelemetry LoggerProvider。 |
@loggerjs/sentry | sentryTransport({ sentry }) | Sentry 结构化日志、breadcrumbs、异常/消息捕获。 |
@loggerjs/datadog | datadogLogsTransport({ apiKey }) | Datadog Logs intake API。 |
@loggerjs/elastic | elasticTransport({ url, index }) | Elasticsearch _bulk API,可按记录选择 index/pipeline/id。 |
@loggerjs/loki | lokiTransport({ url }) | Grafana Loki push API,支持 stream 标签和结构化元数据。 |
@loggerjs/cloudwatch | cloudWatchLogsTransport({ ... }) | CloudWatch Logs PutLogEvents,内置 SigV4 签名。 |
@loggerjs/database | sqliteTransport() / postgresTransport() / databaseTransport(adapter) | 通过不绑定驱动的适配器批量插入。 |
编写自定义 Transport
对于非 2xx 响应抛出 @loggerjs/core 中 httpStatusError(name, response) 的 HTTP transport,会自动获得 batchTransport() 和 retryTransport() 的 Retry-After 处理:这个错误带有 status 和解析出的 retryAfterMs。也可以单独用 parseRetryAfter(value) 解析这个响应头。
实现四种投递方法中的任意一种即可。最简单的 event transport:
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 投影):
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()转换。见 编解码。
