传输器
传输器(Transport) 是日志条目的最终落点——控制台、文件、IPC,或你自己的目标。Transport 接口刻意保持精简:
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,……)。
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 示例:
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 提供了一组方法,用来查看和获取日志文件的信息,方便你实现「导出日志」功能:
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 });
}拿到文件列表后,你可以按需读取、打包、下载:
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 之外导入也是安全的。
import { ElectronIpcTransport } from 'lograil';
const log = createLogger({
runtime: createWebRuntime(), // 渲染进程没有文件系统
transports: [new ElectronIpcTransport()],
});在主进程中,启用 IPC 接收,以便渲染进程的日志被持久化:
import { createElectronMainRuntime, registerIpcReceiver } from 'lograil';
const log = createLogger({ runtime: createElectronMainRuntime() });
// 或直接使用默认主进程运行时(已自动挂载接收器)WorkerIpcTransport
Worker 侧传输器:通过 self.postMessage() 把每条条目转发到主线程 / 父线程。 在非 Worker 环境导入是安全的——self.postMessage 不可用时 write 为空操作。 主线程 / 父线程侧用 registerWorkerReceiver 解码条目,并把级别命令转发给 onLevelCommand。
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。
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 周期,而不会在每次退出时反复重发。
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 组件)而非纯文本日志时,选这个。
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),直接追加到文本视图即可。当你只需要一个类控制台的纯文本面板时,选这个。
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仅在确实挂了格式化订阅者时才惰性计算字符串。
回放、缓冲与释放
// 后加入的订阅者可回放缓冲(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 为空操作,订阅者永不被调用。
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
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: msgUDP 数据报——适用于目标是网络 syslog 服务器、且没有logger二进制的场景。- 非标准安装可用
loggerPath覆盖二进制位置。
JournaldTransport
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
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 接口即可:
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,
});
},
};在运行时添加或移除传输器:
log.addTransport(httpTransport);
log.removeTransport('http');错误处理
Transport.onError 在 write() 失败时被调用——logger 捕获错误后转发, 而不是直接抛出。FileTransport 还会通过同一个钩子上报非致命的内部错误: 轮转时文件删除失败、日志目录不可读、备份重命名失败等。
这些内部错误按严重程度分级:
'warn'— 意外的最佳努力操作失败(例如容量检查时的readdir或rm)。传输器继续运行,但需要排查原因。'debug'— 常规清理步骤失败(例如删除备份文件)。通常无害, 仅在深度调试时有用。
在传输器实例上设置 onError 即可观察这些错误:
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 即可观察这些错误:
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 持续因相同原因触发,请检查磁盘空间、权限或杀毒软件干扰。