How it works(1) winston3源码阅读(A)
发布日期:2021-07-30 08:08:25 浏览次数:7 分类:技术文章

本文共 10900 字,大约阅读时间需要 36 分钟。

winston 是我在 nodejs 下最常用的日志框架,那么他到底是如何工作的呢?

winston 的运行核心

winston 中有两个关键词: 记录器(logger)和传输器(transport).

记录器负责收集/修饰分配进入的每一条日志,传输器则负责最终把日志记录到的哪,

整个 winston 其实就是多个流(stream)的链式调用.记录器继承了交换流(transform),而传输器则继承自可写流(writeable).

为什么使用流式架构呢?

面对大量的日志,流式架构有类似于消息队列的消费模式,可以自行调节流动速度.日志来的太快,传输器消费不动,就在缓冲区先存一下,等待压力小了再消费.不会在不必要的地方浪费内存,保证了稳定性和速度.

况且,最终的传输器如日志文件,接口调用,控制台输出,这些本质上都是流的实现,winston 也使用流的话编写起来也更方便.

winston 的组织结构

结构图

生成的 winston 组织结构图.

winston.js.

整个框架对外暴露的对象:

  • 引用 package.json,获取当前的版本信息.
  • 引用 container.js,用于管理所有创建的记录器
  • 引用 common.js,用于获取通用工具
  • 引用 config/index.js,加载了默认的配置项
  • 引用 create-logger.js,用于创建默认的日志方法.
  • 引用 exception-handle.js,默认添加了拦截意外错误的处理器
  • 引用 transports/index.js,加载了默认内置的所有传输器(transports)

container.js

winston 可以直接使用默认生成的一个记录器,也可以自由添加命名的记录器.

命名的记录器就通过 container 管理.

  • 引用 create-logger,用于生成记录器

create-logger.js

生成记录器的类.

  • 引用记录器基类(logger.js),create-logger 继承了这个基类

logger.js

winston 的核心之一,记录器基类.接收具体的日志消息,对接传输器.

  • 引用 common.js,获取工具函数,用于废弃方法警告(3.x 版本,还兼容一些 2.x 的 API,将会在 4.x 移除)
  • 引用 config/index.js,加载默认的配置
  • 引用 profiler.js,可以测试日志生成时间
  • 引用 exception-handle.js,可以拦截意外错误

exception-handle.js

拦截并处理未被捕获的错误

  • 引用 exception-stream.js,对捕获的错误进行处理.

transport/index.js

传输器作为 winston 的另一核心,对从记录器收到的日志进行最终的存储/处理/展示

  • 引用 console.js,用于打印在控制台.
  • 引用 file.js,用于存储到文件
  • 引用 http.js,用于将日志以 json 形式发送到对应地址
  • 引用 stream.js,将日志输送到指定可写流中

transport/file.js

最常用的传输器,将日志打印到日志文件中

  • 引用 tail-file.js,用于对日志进行查询

各模块源码阅读

winston 的源码其实阅读起来并不方便,因为里面又引用了作者封装的一些 winston 专用的库,阅读 winston 还需要把这些相关库的功能搞清.

winston.js

const logform = require("logform");const {
warn } = require("./winston/common");

在一开始引用了两个需要多次调用的库.

  • logform 是作者将对日志进行格式化的一些方法(比如 json 处理,添加时间等),又封成一个独立的库,这里不展开了,以后有机会可能会拿出来详细的看一下.
  • warn 是定义的公共函数,用于警告用户.一些历史遗留的方法,函数(winston 1.x,2.x 版本)已经不可用了

整个文件基本上为了实现其两个职能:

创建自定义记录器

可以利用 winston 对象创建自定义的记录器

const winston = exports;winston.version = require('../package.json').version;winston.transports = require('./winston/transports');...winston.Transport = require('winston-transport');//初始化了一个默认的记录器容器.winston.loggers = new winston.Container();

代码中,winston 对象直接指向了 module.exports.每一行挂载一个模块到对应的功能上,避免在文件头部占据大部分空间.当然,像某些强类型语言是有引用全部在头部写的规则的.

挂载的模块都是创建自定义记录器可能用到的.

使用默认的记录器

winston 对象本身也是一个有默认参数的记录器,对于最一般需求,可以直接拿来就用.

//创建了一个默认的记录器,将一个记录器应有的方法和属性都挂到winston对象上.const defaultLogger = winston.createLogger();Object.keys(winston.config.npm.levels).concat(['log','query','stream','add','remove','clear','profile','startTimer','handleExceptions','unhandleExceptions','configure']).forEach(method =>  (winston[method] = (...args) => defaultLogger[method](...args))  );Object.defineProperty(winston, 'level', {
get() {
return defaultLogger.level; }, set(val) {
defaultLogger.level = val; }});Object.defineProperty(winston, 'exceptions', {
get() {
return defaultLogger.exceptions; }});//这里可能是以前数组里有若干个属性,后来就剩这一个了,所以还保留这种数组形式['exitOnError'].forEach(prop => {
Object.defineProperty(winston, prop, {
get() {
return defaultLogger[prop]; }, set(val) {
defaultLogger[prop] = val; } });});...

这里定义的几个属性是通过 Object.defineProperty 定义的.定义的几个属性都有保护,不会被删除或者修改指向.

猜测是因为要保护默认定义的这个记录器,防止其他也引用了 winston 的第三方包修改这些引用,造成难以预估的问题.而自定义的记录器因为相互之间是独立的,所以一般不会和第三方包冲突.

container.js

如果需要在同一个项目的若干个模块里都采用同一个记录器,那么 container 就很方便了.定义一个拥有名称的记录器,在任何模块都可以使用记录器名称从 container 中取出来.

如果只在一个模块内使用,或者有其他方法能在模块间传递某个记录器的引用,那么可以不使用 container.

container 本质上是维持了一个字典.不过并没有用普通的对象作为字典,而是使用了 ES6 的 Map.

在这里使用 Map 的原因,我觉得是 Map 的语义更能表达一个字典的功能(如 has,get,set,delete),同时相比普通的对象效率更高.

create-logger.js

一开始先引用了一个作者自己写的包 triple-beam 中的 LEVEL 变量,其实相当于使用了 es6 的 symbol

const {
LEVEL } = require("triple-beam");const LEVEL = Symbol.for("level");

create-logger 本身是对 logger 类的扩展.相比 logger 本身,他多做了一件事:

将所有的日志级别绑定在记录器上.

传输器是绑定级别的,所以记录器将收集所有级别的日志,再派发给各个传输器.

_setupLevels() {
/* this.levels默认是npm日志的7个级别,即: error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 */ Object.keys(this.levels).forEach(level => {
this[level] = function (...args) {
//新式API,使用时直接调用log.info(msg) if (args.length === 1) {
const [msg] = args; const info = msg && msg.message && msg || {
message: msg }; info.level = info[LEVEL] = level; this.write(info); //这里的this因为没有使用箭头函数,所以指向的是具体的实例,否则就会挂载到原型链上 return this; } //旧式API,log('info',msg) return this.log(level, ...args); }; //挂载了一个判断该级别下本方法是否可用的函数,后期没有用到 this[isLevelEnabledFunctionName(level)] = () => this.isLevelEnabled(level); }); }

其中的特别点,是添加了一个 Symbol 变量 LEVEL 作为属性名的属性,防止该 msg 本身也有一个名为 level 的属性在某些情况下覆盖掉.

关于 symbol 类型,可以看

logger.js

logger 本质上是一个 transform 流的实现,不过他实现自’readable-stream’模块,而不是 node 自带的 stream 模块

const {
Stream, Transform } = require("readable-stream");

文档是这样描述这个模块的:

This package is a mirror of the streams implementations in Node.js.

Full documentation may be found on the Node.js website.
If you want to guarantee a stable streams base, regardless of what version of Node you, or the users of your libraries are using, use readable-stream only and avoid the “stream” module in Node-core, for background see this blogpost.

大意是,这个模块不会因为切换不同的 nodejs 版本而产生兼容问题,如果你的产品需要兼容多个 nodejs 版本,尽量选择这个模块而不是原生的 stream 模块.

记录器在初始化方法里,通过调用 add 方法,将用户自定义的传输器都通过管道连到了自身上,因为每种传输器都是相互独立的,所以并非是链式调用,本质上更接近于用户定义的若干个传输器都监听了记录器的 data 事件,一旦来了数据,就会向各个传输器写入.

add(transport) {
//确保输入的传输器是规定的对象(LegacyTransportStream也来自于作者自定义的模块:winston-transport/legacy) //如果是自定义的传输器,就用LegacyTransportStream包装,如果是自带的传输器,如file,console就直接传输 const target = !isStream(transport) || transport.log.length > 2 ? new LegacyTransportStream({
transport}) :transport; if (!target._writableState || !target._writableState.objectMode) {
throw new Error('Transports must WritableStreams in objectMode. Set { objectMode: true }.'); } //将传输器的error和warn事件传递到记录器上 this._onEvent('error', target); this._onEvent('warn', target); //pipe到传输器上 this.pipe(target); if (transport.handleExceptions) {
//拦截意外错误 this.exceptions.handle(); } return this; } _onEvent(event, transport) {
function transportEvent(err) {
this.emit(event, err, transport); } if (!transport['__winston' + event]) {
transport['__winston' + event] = transportEvent.bind(this); transport.on(event, transport['__winston' + event]); } }

既然继承了 transform,本模块就需要实现_transform 方法:

记录器的_transform 方法本质很简单,仅是把接到的已被各种格式化器(format)处理好的对象调用自身的 push 方法,发给了自己的 readable.

同时,还实现了_final 方法.

_final 方法会在写完所有数据时调用,调用完成后会发送’finish’事件.在这里利用_final 会等待所有传输器都处理完毕后才触发事件

_transform(info, enc, callback) {
if (this.silent) {
return callback(); } //确保info是合法的info if (!info[LEVEL]) {
info[LEVEL] = info.level; } if (!this.levels[info[LEVEL]] && this.levels[info[LEVEL]] !== 0) {
console.error('[winston] Unknown logger level: %s', info[LEVEL]); } if (!this._readableState.pipes) {
console.error('[winston] Attempt to write logs with no transports %j', info); } try {
this.push(this.format.transform(info, this.format.options)); } catch (ex) {
throw ex; } finally {
callback(); } } _final(callback) {
const transports = this.transports.slice(); asyncForEach(transports, (transport, next) => {
//这里用setImmediate而不是直接调用next,是等待前面未完成的transport完成才调用next if (!transport || transport.finished) return setImmediate(next); //否则就等待他结束再处理下一个 transport.once('finish', next); transport.end(); }, callback); }

file.js

以最常用的文件日志传输器为例.

要想搞清楚这个 file 传输器是如何工作,需要先搞清楚所有传输器都继承的类’winston-transport’是如何工作的.

winston-transport 主要实现的功能是日志按级别分发,是对可写流的继承:

记录器有日志级别,传输器也可以指定级别,未指定级别的传输器默认级别就是其记录器的级别.

同时,依据日志级别协议,低级别的日志会包含高级别日志,比如定义了输出级别为 info,则日志文件中会包含 info 以及比 info 高级别的 warn,error 等信息.

constructor(){
this.once('pipe', logger => {
//记录连接到的记录器,得到记录器的日志级别(默认是info级别) this.parent = logger; });}_write(info, enc, callback) {
//如果指定了级别就按指定的来,否则,就继承自连接的记录器的级别 const level = this.level || (this.parent && this.parent.level); //记录所有与本级别相等或高于本级别的日志(数字越小,级别越高) if (!level || this.levels[level] >= this.levels[info[LEVEL]]) {
//不需要格式化就直接打印 if (info && !this.format) {
//这里的log函数实现于本类的继承者,在本例子里就是File传输器. return this.log(info, callback); } let errState; let transformed; //格式化传入的日志 try {
transformed = this.format.transform(Object.assign({
}, info), this.format.options); } catch (err) {
errState = err; } if (errState || !transformed) {
callback(); if (errState) throw errState; return; } return this.log(transformed, callback); } return callback(null);};

Ok,了解完其基类,再来看看 file 传输器本身,这几乎也是 winston 最复杂的地方了.

file 传输器的初始化:

constructor(options = {
}) {
super(options); //基础的双工流,主要用于写入本地文件,也可以传给用户自定义的流 this._stream = new PassThrough(); /* 传输器在暂时无法写入本地文件的特殊情况(写入太快,正在轮转文件等), 可以缓冲30条日志,在此期间,如果日志多于30条.数据就丢失了 */ this._stream.setMaxListeners(30); //创建文件流并通过管道和基础双工流连接 this.open(); }

log 函数是 file 传输器的运行核心:

log(info, callback = () => {
}) {
//如果正等待硬盘处理完毕 if (this._drain) {
//通过事件进行延迟调用 this._stream.once('drain', () => {
this._drain = false; this.log(info, callback); }); return; } //如果正处在文件轮转间隙 if (this._rotate) {
//通过事件进行延迟调用 this._stream.once('rotate', () => {
this._rotate = false; this.log(info, callback); }); return; } //拼装一条完整的包含结尾符的日志 const output = `${
info[MESSAGE]}${
this.eol}`; const bytes = Buffer.byteLength(output); //每次写入后都检测是不是下一次应该轮转文件 function logged() {
this._size += bytes; this._pendingSize -= bytes; this.emit('logged', info); //如果正在第一次执行初始化(轮转后的再初始化过程不会调用此处) if (this._opening) {
return; } //检测是否需要轮转文件 if (!this._needsNewFile()) {
return; } this._rotate = true; //关闭当前流,开始轮转文件 this._endStream(() => this._rotateFile()); } this._pendingSize += bytes; //检测是否需要轮转 if (this._opening && !this.rotatedWhileOpening && this._needsNewFile(this._size + this._pendingSize)) {
this.rotatedWhileOpening = true; } //硬盘能否继续写入 const written = this._stream.write(output, logged.bind(this)); //硬盘暂时写入不了,就打开等待硬盘IO处理完毕的开关,直到可以继续消费再关闭开关. if (!written) {
this._drain = true; this._stream.once('drain', () => {
this._drain = false; callback(); }); } else {
callback(); } return written; }

file 传输器的运行逻辑如图所示:

file 传输器中大多数方法都是为轮转文件服务,同时还有实现查询等功能,因为不是核心功能,在此就不赘述了.

总结

综上,winston 的核心逻辑应该就清楚了.

其主干逻辑很清晰,就是将通过流将整个流程连接起来.

复杂的地方在于可用性保障与灵活性保障:

可用性保障

  • 用 symbol 命名属性,保证不会被修改
  • 用 Object.defineProperty 定义只读方法,同样保证不被修改.
  • 传输器增加有限缓冲,不会爆内存,也尽可能少丢失数据.

模块的健壮是灵活性的保证,不健壮的代码也一旦灵活起来处处都是漏洞.

灵活性保障

  • 可以自定义配置,也可以采用默认配置,甚至可以开箱即用.
  • 可修改模块配置,也可从已有模块派生兼容的模块

作为通用型的模块,灵活是可以让更多的人来使用的前提.

转载地址:https://blog.csdn.net/u011609113/article/details/88086940 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:How it works(2) autocannon源码阅读(A)
下一篇:Sequelize的原始查询的时区问题

发表评论

最新留言

很好
[***.229.124.182]2024年11月04日 08时24分34秒