Skip to content

传输器 ​

传输器(Transport) 是日志条目的最终落点——控制台、文件、IPC,或你自己的目标。Transport 接口刻意保持精简:

ts
interface Transport {
  /** 唯一名称,用于诊断与移除。 */
  readonly name: string;
  /** 可选的、覆盖管道默认值的每传输器格式化器。 */
  readonly formatter?: Formatter;
  /** 可选的最小级别;低于它的条目仅被该落点跳过。 */
  readonly level?: LogLevelInput;
  /** 输出一条已处理的条目(同步或异步均可)。 */
  write(entry: LogEntry, formatted: string): void | Promise<void>;
  /** 可选:刷新,由 `logger.flush()` 等待。 */
  flush?(): void | Promise<void>;
  /** 可选:资源释放。 */
  close?(): void | Promise<void>;
}

内置传输器 ​

ConsoleTransport ​

写入全局 console,将每个级别映射到对应的方法(warn → console.warn,error/fatal → console.error,……)。

ts
import { ConsoleTransport, createLineFormatter } from 'lograil';

new ConsoleTransport({
  name: 'console',
  formatter: createLineFormatter(),
  methodMap: { fatal: (...a) => console.error('FATAL', ...a) },
});

通过 `methodMap` 可把某个级别路由到不同的 `console` 方法,或为其输出加前缀。默认每个级别映射到同名的 `console` 方法,且 `fatal` 映射到 `console.error`(因此在多数终端里会出现在 `stderr`)。

### FileTransport

面向 Node.js 与 Electron 主进程、带多种模式的文件传输器。`appName` 必填,
且始终是文件名的一部分。模式是一个判别联合,所以每种模式只暴露自己的字段:

```ts
import { FileTransport, createJsonFormatter } from 'lograil';

// 1.1 / 1.2 — 单文件(一直追加,或超 maxSize 时截断)
new FileTransport({
  mode: 'single', // 或 'single-truncate'
  appName: 'my-app',
  maxSize: 10 * 1024 * 1024, // 'single-truncate' 必填
  formatter: createJsonFormatter(),
});

// 2.1 — 按体积轮转;归档名遵循 `${app}.${seq}.${ext}`
new FileTransport({
  mode: 'rotate-size',
  appName: 'my-app',
  maxSize: 10 * 1024 * 1024,
  maxFiles: 5,
  formatter: createJsonFormatter(),
});

// 2.2 — 按时间轮转;每天一个带日期的文件,命名为 `${app}.${stamp}.${seq}.${ext}`
new FileTransport({
  mode: 'rotate-time',
  appName: 'my-app',
  unit: 'day',
  formatter: createJsonFormatter(),
});

// 2.3 — 按自定义谓词轮转
new FileTransport({
  mode: 'rotate-custom',
  appName: 'my-app',
  shouldRotate: (entry) => entry.levelName === 'error',
  fileName: (app, seq, ext) => `${app}.${seq}.${ext}`,
  formatter: createJsonFormatter(),
});

全局容量上限(可选) ​

所有文件模式都可以加上 maxTotalSize 与 maxAge。它们独立于各模式自带的 maxFiles 限制,且在所有模式下生效,但只有真正产生多个文件的 rotate-* 模式才会有实际作用。当前正在写入的活动文件永远不会被删除。

  • maxTotalSize: number —— 从最旧的历史文件开始删除,直到 活动文件 + 历史文件 的总体积降到这个字节数以下。默认 Infinity(不限制)。
  • maxAge: number —— 删除修改时间早于该毫秒数的历史文件。
    • undefined 或 -1(默认):不限制。
    • 0:立即删除所有历史文件。
    • >0:毫秒数阈值。

rotate-size 示例:

ts
new FileTransport({
  mode: 'rotate-size',
  appName: 'my-app',
  maxSize: 10 * 1024 * 1024,
  maxFiles: 100,
  maxTotalSize: 100 * 1024 * 1024, // 总体积最多 100 MB
  maxAge: 30 * 24 * 60 * 60 * 1000, // 并丢弃 30 天前的文件
});
  • single — 单个 <appName>.log 文件,一直追加(直到磁盘写满)。
  • single-truncate — 同样是单文件,但一旦即将超过 maxSize,当前内容被改名为 <appName>.bak,原文件原地截断。
  • rotate-size — 当 size + bytes > maxSize 时,按 maxFiles 推移代际(app.log → app.1.log → …)。
  • rotate-time — 在每个 hour/day 边界开启新文件,并以时间戳命名。设置 maxSize 后,同一时间桶内的文件超大时会自动拆分(如 app.2026-08-31.0.log、app.2026-08-31.1.log)。
  • rotate-custom — shouldRotate(entry, ctx) 决定何时切分;fileName 为每个文件命名。对轮转拥有完全控制。

利用 filter 选项可把单个 logger 的输出拆分到多个文件——例如内置的 Electron 主进程运行时给主进程与渲染进程各分配一个 FileTransport(appName: 'main' / 'renderer'),从而分开两者日志。

导出日志文件 ​

FileTransport 提供了一组方法,用来查看和获取日志文件的信息,方便你实现「导出日志」功能:

ts
import { logger, FileTransport } from 'lograil';

// 从默认实例中找到 FileTransport
const fileTransport = logger.getTransports()
  .find((t): t is FileTransport => t instanceof FileTransport);

if (fileTransport) {
  // 日志文件存放的目录
  console.log(fileTransport.getDir()); // /var/log/myapp

  // 当前正在写入的文件
  console.log(fileTransport.getActiveFile()); // /var/log/myapp/myapp.log

  // 列出所有日志文件(包括之前的旧文件)
  const files = await fileTransport.getFiles();
  // [
  //   { name: 'myapp.log', path: '/var/log/myapp/myapp.log', size: 4096, mtime: 1725168000000, active: true },
  //   { name: 'myapp.2026-08-31.0.log', path: '...', size: 8192, mtime: 1725081600000, active: false },
  // ]

  // 只要本次启动后创建的文件(排除旧文件)
  const sessionFiles = await fileTransport.getFiles({ currentSessionOnly: true });
}

拿到文件列表后,你可以按需读取、打包、下载:

ts
import { readFile } from 'node:fs/promises';
import { ZipFile } from 'yazl'; // 或其他打包库

async function exportLogs() {
  const files = await fileTransport.getFiles();
  const zip = new ZipFile();
  for (const f of files) {
    const content = await readFile(f.path, 'utf-8');
    zip.addBuffer(Buffer.from(content), f.name);
  }
  zip.end();
  // ... 输出 zip 文件
}

ElectronIpcTransport ​

渲染进程侧的传输器,通过 IPC 将每条条目转发到主进程。electron 模块采用惰性加载,因此在 Electron 之外导入也是安全的。

ts
import { ElectronIpcTransport } from 'lograil';

const log = createLogger({
  runtime: createWebRuntime(), // 渲染进程没有文件系统
  transports: [new ElectronIpcTransport()],
});

在主进程中,启用 IPC 接收,以便渲染进程的日志被持久化:

ts
import { createElectronMainRuntime, registerIpcReceiver } from 'lograil';

const log = createLogger({ runtime: createElectronMainRuntime() });
// 或直接使用默认主进程运行时(已自动挂载接收器)

WorkerIpcTransport ​

Worker 侧传输器:通过 self.postMessage() 把每条条目转发到主线程 / 父线程。 在非 Worker 环境导入是安全的——self.postMessage 不可用时 write 为空操作。 主线程 / 父线程侧用 registerWorkerReceiver 解码条目,并把级别命令转发给 onLevelCommand。

ts
import { WorkerIpcTransport, registerWorkerReceiver } from 'lograil';

// worker.ts — createWebRuntime() 在 Worker 中会自动挂载
logger.info('hello from worker'); // → console + 转发到主线程

// main.ts — 自动接收 worker 日志
const off = registerWorkerReceiver((entry) => logger.ingestEntry(entry));

ClusterIpcTransport ​

Cluster worker 侧传输器:通过 process.send() 把每条条目转发到主进程。 在非 cluster worker 环境导入是安全的——process.send 不可用时 write 为 空操作。主进程侧用 registerClusterReceiver 解码条目,并把级别命令转发给 onLevelCommand。

ts
import { ClusterIpcTransport, registerClusterReceiver } from 'lograil';

// worker.ts — createNodeRuntime() 在 cluster worker 中会自动挂载
logger.info('hello from worker'); // → console + 转发到主进程

// primary.ts — 接收 worker 日志
const off = registerClusterReceiver((entry) => logger.ingestEntry(entry));

OtlpTransport ​

通过 OTLP HTTP/JSON 把日志转发到 OpenTelemetry Collector(或任意 OTLP 接收端)。 日志会被缓冲并按批次发送;在进程退出前调用 flush()(或开启 logger 的 autoFlushOnExit)以排空。需要全局 fetch(Node >= 18、现代浏览器、Electron 均支持)。

瞬态失败(网络错误或 5xx)在原地以指数退避重试(maxRetries、 retryInitialDelayMs、retryMaxDelayMs)。一次批次在重试后仍未投递(或被 4xx 直接拒绝)时,其记录计入 dropCount;这些记录在下一次 flush() / close() 只再投递一次,之后永久丢弃。因此一个始终失败的 Collector 对每个批次最多消耗 两个 flush 周期,而不会在每次退出时反复重发。

ts
import { OtlpTransport } from 'lograil';

new OtlpTransport({
  endpoint: 'http://localhost:4318/v1/logs', // OTLP HTTP 接收端
  serviceName: 'my-service',
  resource: { 'deployment.environment': 'prod' },
});

上下文中的 traceId / spanId(或 trace_id / span_id)会自动映射进 OTLP 的 链路关联字段,使日志能与其所在的分布式链路在后端关联。

借助按传输器 level,可以让一个 logger 分流——例如把 OtlpTransport 设为 error,文件传输器设为 info。

LiveTransport ​

一个内存型、可订阅的传输器,用于实时日志流——它不写入落点,而是把每条 条目转发给进程内的订阅者。它是调试面板、webview 日志查看器,或渲染日志流的 React/Vue Hook 的基石。零依赖、跨运行时(Web、Node、Electron 通用)。

消费日志流有两种方式,取决于你想怎么渲染:

按实体渲染(subscribe) ​

你拿到的是原始、已冻结的 LogEntry,可以随意渲染——级别徽标、可展开的 context/metadata 树、点击复制、按字段过滤等。当你的 UI 是结构化的 (按条目 key 的 React/Vue 组件)而非纯文本日志时,选这个。

ts
import { LiveTransport } from 'lograil';

const live = new LiveTransport({ bufferSize: 100 });
logger.addTransport(live);

// entry 已冻结——切勿修改它。
const unsubscribe = live.subscribe((entry) => {
  // 例如 <LogRow level={entry.levelName} msg={entry.message} ctx={entry.context} />
  renderRow(entry);
});

按格式化字符串渲染(onFormatted) ​

你拿到的是预格式化好的文本行(由传输器的 formatter 生成;未设置时取条目的 message),直接追加到文本视图即可。当你只需要一个类控制台的纯文本面板时,选这个。

ts
import { LiveTransport, createLineFormatter } from 'lograil';

const live = new LiveTransport({ formatter: createLineFormatter() });
logger.addTransport(live);

// line 已格式化;若需要,entry 也会一并传入。
const unsubscribe = live.onFormatted((line, entry) => {
  appendLine(line); // 例如 textarea / <pre> / 终端组件
});

两种模式是独立的订阅——可以只用其一,也可以同时使用。subscribe 始终给原始 条目;onFormatted 仅在确实挂了格式化订阅者时才惰性计算字符串。

回放、缓冲与释放 ​

ts
// 后加入的订阅者可回放缓冲(newestFirst 为 true 时最新在前)。
live.replay((entry) => backfill(entry), true);

console.log(live.subscriberCount); // 当前订阅者数量
live.clearBuffer(); // 清空缓冲
unsubscribe(); // 停止接收

关键行为:

  • 热路径安全。 订阅者抛出的异常会被捕获并记录,绝不会中断 logger 的 write() 或其他订阅者。
  • 共享引用。 订阅者拿到的是管道产出的同一个已冻结 LogEntry 引用——不要修改它。
  • 缓冲。 设置 bufferSize > 0 可为后加入的订阅者保留环形缓冲(通过 replay 回放)。0(默认)完全关闭缓冲。
  • 释放。 close() 会清空所有订阅者与缓冲。

对于跨进程流式传输(Electron 主进程 → 渲染进程/webview),可配合现有的 IPC 通道使用; 跨标签页的 Web 流式传输则使用 BroadcastChannelTransport。

BroadcastChannelTransport ​

跨标签页 / 跨窗口的 Web 流式传输。每个添加了该传输器的标签页会把每条条目 广播到一个共享的 BroadcastChannel;同一来源、相同频道名的其他标签页会 收到它。可在任意运行时导入——在没有 BroadcastChannel 的平台(Node、 Electron 主进程、旧浏览器)上 write 为空操作,订阅者永不被调用。

ts
import {
  ConsoleTransport,
  BroadcastChannelTransport,
  registerBroadcastChannelReceiver,
} from 'lograil';

// 同一来源、同一频道的每个标签页:
const log = createLogger({
  transports: [new ConsoleTransport(), new BroadcastChannelTransport({ name: 'my-app' })],
});
log.info('from tab A'); // 在标签页 B 同样可见

// 在标签页 B(接收侧)——把外部标签页的条目送回本地 logger,
// 使其享受同样的文件/控制台处理:
const off = registerBroadcastChannelReceiver(
  (entry) => log.ingestEntry(entry),
  { name: 'my-app' },
);
  • 频道名(name)是协调键:所有参与者必须使用相同的名称与来源。 传输器名(transportName)只是用于诊断 / 移除的 Transport.name。
  • write() 广播的是已冻结的条目(标签页内零拷贝);其他标签页通过 频道的结构化克隆拿到一个新的条目。
  • 不想经过 logger 直接渲染时,可用 transport.subscribe((entry) => render(entry)) 内联订阅——返回取消订阅函数。
  • 按标签页防循环。 标签页 A 广播的条目会被投递到标签页 B,但不会回显 到标签页 A(BroadcastChannel 只投递给该频道的其他实例)。

OS 原生传输器 ​

lograil 提供三个把日志交给操作系统自身日志基础设施的传输器。三者都在任意运行时 (Web / Node / Electron)中可导入,且在模块加载时绝不抛错;在缺少对应二进制或 设施的平台,传输器会优雅降级并通过 onError 上报失败,同时用断路器限制错误上报 频率,避免损坏的 sink 刷爆控制台。

  • SyslogTransport — Unix(BSD / Linux),经由 logger 工具。
  • JournaldTransport — Linux / systemd,经由一个常驻的 systemd-cat。
  • WindowsEventLogTransport — Windows,经由 eventcreate.exe。

三者共用 os-native.ts 里的级别 → 优先级映射(trace/debug→7, info→6,warn→4,error→3,fatal→2)。

SyslogTransport ​

ts
import { SyslogTransport, createJsonFormatter } from 'lograil';

const syslog = new SyslogTransport({
  name: 'syslog',
  tag: 'myapp',                 // logger -t tag
  facility: 3,                  // LOG_DAEMON(默认)
  protocol: 'local',            // 'local'(logger 二进制)| 'udp'(原始数据报)
  host: 'localhost',           // udp 模式
  port: 514,                   // udp 模式
  formatter: createJsonFormatter(),
});
log.addTransport(syslog);
  • protocol: 'local'(默认)每条记录都调用 logger -t <tag> -p <facility>.<pri> <msg>。 logger 自带于所有 BSD 及大多数 Linux 发行版。
  • protocol: 'udp' 用 nc(netcat)发送原始的 <pri>timestamp host tag: msg UDP 数据报——适用于目标是网络 syslog 服务器、且没有 logger 二进制的场景。
  • 非标准安装可用 loggerPath 覆盖二进制位置。

JournaldTransport ​

ts
import { JournaldTransport } from 'lograil';

const journal = new JournaldTransport({
  name: 'journald',
  fields: { UNIT: 'myapp.service', APP: 'myapp' },
  priority: 4,                  // 固定的 PRIORITY 日志字段(可选)
});
log.addTransport(journal);
  • 启动一个常驻的 systemd-cat 进程,并把换行分隔的记录持续写入其 stdin (整个生命周期不重复 spawn)。
  • fields 成为固定的 KEY=VALUE 日志字段;IDENTIFIER 默认取传输器名。
  • 若省略 priority,则应用默认 PRIORITY=6 字段。
  • 在缺少 systemd-cat 的宿主上,spawn 会失败,每条写入经 onError 上报 (连续 10 次失败 / 5 秒窗口触发断路器)。

WindowsEventLogTransport ​

ts
import { WindowsEventLogTransport } from 'lograil';

const wlog = new WindowsEventLogTransport({
  name: 'winlog',
  log: 'Application',          // 目标事件日志
  source: 'MyApp',            // 提供方 / 来源字符串
  eventId: 1,                 // 默认记录 ID
});
log.addTransport(wlog);
  • 使用 eventcreate.exe /t <type> /l <log> /s <source> /d <data> /id <id>, 内建于 Windows Vista 及之后。type 由条目级别推导 (error/fatal → error,其余 → information)。
  • 按条目覆盖:把 entry.metadata.wevtid 设为数字即可为该记录使用自定义事件 ID。
  • 自定义 source 值需先在 Windows 事件系统中预注册;写入 Application / System 日志需要管理员权限。二者未解决前都会经 onError 上报。
  • 在非 Windows 平台上,传输器可导入但每条写入都经 onError 上报 not available on this platform(触发断路器)。

平台门控。 每个传输器在写入时检查 os.platform(): SyslogTransport 与 JournaldTransport 需要 Unix / Linux 宿主及对应二进制; WindowsEventLogTransport 需要 win32。在浏览器构建中,shim 报告 platform === 'browser',传输器即降级为不可用路径。

自定义传输器 ​

只需实现 Transport 接口即可:

ts
import type { Transport, LogEntry } from 'lograil';

const httpTransport: Transport = {
  name: 'http',
  write(entry: LogEntry, formatted: string) {
    fetch('https://logs.example.com', {
      method: 'POST',
      body: formatted,
    });
  },
};

在运行时添加或移除传输器:

ts
log.addTransport(httpTransport);
log.removeTransport('http');

错误处理 ​

Transport.onError 在 write() 失败时被调用——logger 捕获错误后转发, 而不是直接抛出。FileTransport 还会通过同一个钩子上报非致命的内部错误: 轮转时文件删除失败、日志目录不可读、备份重命名失败等。

这些内部错误按严重程度分级:

  • 'warn' — 意外的最佳努力操作失败(例如容量检查时的 readdir 或 rm)。传输器继续运行,但需要排查原因。
  • 'debug' — 常规清理步骤失败(例如删除备份文件)。通常无害, 仅在深度调试时有用。

在传输器实例上设置 onError 即可观察这些错误:

ts
import { FileTransport } from 'lograil';

const ft = new FileTransport({
  mode: 'rotate-size',
  appName: 'my-app',
  maxSize: 10 * 1024 * 1024,
  maxFiles: 5,
});

ft.onError = (err, entry) => {
  console.error('[file-transport]', err.message, entry.levelName);
};

在 Electron 应用中,如果你调用了 redirectConsole(),请避免将 onError 路由到 console.*——使用 process.stderr 或专用 sink 以防止无限递归。 详见故障排除。

大多数传输器错误是暂时性的,会自行恢复(例如文件被临时锁定)。如果 onError 持续因相同原因触发,请检查磁盘空间、权限或杀毒软件干扰。

错误处理 ​

Transport.onError 在 write() 失败时被调用——logger 捕获错误后转发, 而不是直接抛出。FileTransport 还会通过同一个钩子上报非致命的内部错误: 轮转时文件删除失败、日志目录不可读、备份重命名失败等。

这些内部错误按严重程度分级:

  • 'warn' — 意外的最佳努力操作失败(例如容量检查时的 readdir 或 rm)。传输器继续运行,但需要排查原因。
  • 'debug' — 常规清理步骤失败(例如删除备份文件)。通常无害, 仅在深度调试时有用。

在传输器实例上设置 onError 即可观察这些错误:

ts
import { FileTransport } from 'lograil';

const ft = new FileTransport({
  mode: 'rotate-size',
  appName: 'my-app',
  maxSize: 10 * 1024 * 1024,
  maxFiles: 5,
});

ft.onError = (err, entry) => {
  console.error('[file-transport]', err.message, entry.levelName);
};

在 Electron 应用中,如果你调用了 redirectConsole(),请避免将 onError 路由到 console.*——使用 process.stderr 或专用 sink 以防止无限递归。 详见故障排除。

大多数传输器错误是暂时性的,会自行恢复(例如文件被临时锁定)。如果 onError 持续因相同原因触发,请检查磁盘空间、权限或杀毒软件干扰。

Released under the MIT License.