架构
lograil 采用分层设计,让每个关注点彼此隔离、便于测试。一次日志调用会按可预测的顺序流经这些层。
分层
log.info(message, ...args)
│
▼
┌───────────────┐
│ Logger │ 构建结构化的 LogEntry(时间戳、作用域、
│ (门面) │ 上下文、元数据、error、级别)
└───────────────┘
│
▼
┌───────────────┐
│ Pipeline │ 1. 过滤器 —— 丢弃条目(如按级别/作用域)
│ │ 2. 处理器 —— 转换/增强/脱敏/序列化
│ │ 3. 格式化器 —— 序列化为字符串/JSON
└───────────────┘
│
▼
┌───────────────┐
│ Plugins │ 异步拦截每条条目(可丢弃/修改),
│ │ 并可在运行时添加传输器 / 重构管道
└───────────────┘
│
▼
┌───────────────┘
│ Transports │ 最终落点:控制台、滚动文件、IPC、OTLP、自定义
└───────────────┘
▲
│
┌───────────────┐
│ Runtime │ 隔离环境差异(时钟、pid、文件系统、默认值)
│ Adapter │ —— 自动探测,或显式传入
└───────────────┘
Context —— 附加到每条日志中的持久化结构化字段(另含可选的
环境异步上下文)。- Logger:把调用或外部接收的条目转换为统一的
LogEntry处理流程。第一个参数可以是string、Error或任意值(对象会被保留为结构化数据)。条目携带context(持久化、可继承的请求级数据)和metadata(单条条目级,通常由处理器注入——详见 上下文与元数据 了解区别)。 - Pipeline:先运行
Filter(返回false即丢弃),再依次运行Processor(增强/脱敏/序列化……),最后由Formatter生成线上形态。内置处理器createRedactProcessor、createSerializeProcessor即插于此。 - Plugins:在管道之后通过每条条目的异步
intercept钩子运行。可丢弃条目(返回null)或改写,并通过PluginContext在运行时重新配置 logger。默认脱敏也会在插件之后再次执行,避免插件重新引入敏感字段后直接写出。 - Transports:最终落点(控制台、
rotating file、IPC、OTLP或自定义)。write可以是异步的;同一个传输器的异步写入按日志顺序串行启动,彼此独立的传输器互不阻塞。flush()/destroy()会等待队列完成。每个传输器还可声明自己的level做独立过滤。 - Runtime Adapter:抽象环境差异(Web / Node / Electron),因此同一套 logger 代码处处可用。
- Context:合并进每条日志的
context字段;作用域 logger 会获得隔离的子上下文。此外还有环境(异步)上下文——基于AsyncLocalStorage的请求级字段,在await之间自动继承,会被叠加在 logger 上下文之上(浏览器中为无操作)。
异步与生命周期
从调用方视角看,日志记录是同步的,但写入会经由内部队列排空,因此异步传输器与插件拦截都不会阻塞你的代码:
ts
logger.info('done'); // 立即返回
await logger.flush(); // 等待队列(含异步写入)清空
await logger.destroy(); // 先 flush,再释放插件、关闭传输器由于插件的 onEntry 钩子会被 await 并链式执行,且异步传输写入会进入队列,因此在进程退出前调用 flush()(或 destroy())能保证缓冲区中的日志不丢失。Logger 通过运行时的 lifecycle 钩子来接线 flush / 崩溃行为:所以 autoFlushOnExit(或 attachExitHandlers)会在宿主退出时 flush——Node 的 beforeExit / SIGINT / SIGTERM、Electron 主进程的 app before-quit / will-quit、Web 的 pagehide / visibilitychange;watchUncaughtErrors 会把未捕获异常 / 未处理的拒绝以 fatal 记录后再退出;redirectConsole 把 console.* 桥接进 logger。
过滤发生在哪里
有三道相互独立的闸门:
- logger 级别闸门:
setLevel在条目进入管道前丢弃低于阈值的条目,因此被廉价丢弃的条目不会消耗处理成本。 - 管道过滤器:任意谓词(
createLevelFilter、createScopeFilter、createSampler、combineFilters、自定义)在管道内运行。 - 按传输器级别:每个传输器可声明自己的
level;低于它的条目仅被该传输器跳过,从而让一个 logger 分流(例如error及以上发往远端,其余写入文件)。
控制日志量与采样
当某个级别日志量大(如高负载下的 debug/info)时,用内置采样器控制日志量。它用逻辑 AND 结合两种正交策略,并且只对你列出的级别名生效——其余级别原样通过:
- 概率采样(
rate,0..1):按概率rate保留每条条目。 - 限速(
maxPerSecond+burst):令牌桶限制吞吐量,允许不超过burst条的短突发。
由于采样器是管道里的过滤器,被采样的条目不会到达处理器、格式化器或传输器——它也是高负载下削减成本最廉价的方式。参见示例:控制日志量。
ts
import { createSampler } from 'lograil';
logger.getPipeline().addFilter(
createSampler({ levels: ['debug', 'info'], maxPerSecond: 100, burst: 200 }),
);为何如此分层
- 关注点分离:格式化与目标解耦——同一条目可以用不同格式同时进入控制台和文件。
- 可组合:过滤器/处理器/传输器都是普通函数或对象,易于单元测试与复用。
- 可扩展:插件可在运行时重构管道、添加落点,而无需改动业务代码。
- 可移植:运行时适配器使同一套 API 在 Web、Node 与 Electron 上通用,包括 Electron 中渲染进程→主进程的日志汇聚。