源码系列 —— koa-logger

1,286 阅读2分钟

这是我参与更文挑战的第 25 天,活动详情查看:更文挑战

koa-loggerkoa的中间件,用于进行日志打印。简单使用如下所示:

const logger = require('koa-logger')
const Koa = require('koa');

const app = new Koa();

// wrap subsequent middleware in a logger
app.use(logger());
app.use((ctx, next) => {
    const body = new Array((Math.random() * 5 * 1024) | 9).join('a');
    ctx.status = 200;
    ctx.body = body;
})

const port = process.env.PORT || 3000;
app.listen(port);
console.log('listening on port ' + port);

用浏览器访问localhost:3000,看到控制台打印日志如下所示:

image.png

它也接受自定义打印的方式,如下所示:

app.use(logger((str, args) => {
  // 自定义打印格式
}))
// 或者
app.use(logger({
  transporter: (str, args) => {
    // ...
  }
}))

源码解析

koa-logger源码不多,不到 150 行,我们来看一下它的实现逻辑。

直接从导出的主函数开始分析。

module.exports = function (options) {
  // 定义一个打印方法
  // 能够传入一个函数或者{transporter: function}进行自定义
  const print = (function () {
    let transporter;
    if (typeof options === 'function') {
      transporter = options;
    } else if (options && options.transporter) {
      transporter = options.transporter;
    }
  
    return function printFunc(...args) {
      // uril.format 将字符串格式化
      // 详见文档 http://nodejs.cn/api/util.html#util_util_format_format_args
      const string = util.format(...args);
      if (transporter) transporter(string, args);
      else console.log(...args);
    };
  })();
  // ......
}

看到 koa-logger主函数首先定义了print方法,该方法判断主函数的入参,如果是函数或者是一个具有transporter属性的对象,则使用该函数或transporter方法进行日志信息的打印,否则默认使用console.log方法打印日志。

接着往下看,返回一个async函数,就是供koa使用的中间件函数,其源码及详细解释如下所示:

module.exports = function (options) {  
  // ...
  return async function logger(ctx, next) {
    // 记录一个请求开始处理的时间,用于后续处理时间的计算
    // 这段判断是为了兼容 request-received 库,详见 https://github.com/cabinjs/request-received
    const start = ctx[Symbol.for('request-received.startTime')]
      ? ctx[Symbol.for('request-received.startTime')].getTime()
      : Date.now();
   	// 打印接收请求的日志,包括请求的方法、请求的路径
    print(
      '  ' +
        chalk.gray('<--') +
        ' ' +
        chalk.bold('%s') +
        ' ' +
        chalk.gray('%s'),
      ctx.method,
      ctx.originalUrl
    );

    try { // 经过其它的中间件流程
      await next();
    } catch (err) {
        // 打印 下流 未被捕获的异常信息
      log(print, ctx, start, null, err);
      throw err;
    }

    // 开始打印请求响应的日志
    // 在 响应头 没有配置 content-length 的情况下,计算响应流的大小
    const { // 获取 body 跟 content-length 响应头
      body,
      response: { length }
    } = ctx;
    let counter;
      // 如果 ctx.body 是一个stream类型
      // 通过 Passthrough Counter 库计算响应流的大小,详见 https://github.com/stream-utils/passthrough-counter
    if (length === null && body && body.readable)
      ctx.body = body.pipe((counter = counterFunc())).on('error', ctx.onerror);

    // 监听 res 的 finish 事件和 close 事件,
    // 不论哪个执行了,都执行 done 函数,调用 log 函数打印相关日志
    const { res } = ctx;

    const onfinish = done.bind(null, 'finish');
    const onclose = done.bind(null, 'close');

    res.once('finish', onfinish);
    res.once('close', onclose);

    function done(event) {
      res.removeListener('finish', onfinish);
      res.removeListener('close', onclose);
      log(print, ctx, start, counter ? counter.length : length, null, event);
    }
  };
};

关于响应部分,我在koa的文档中没有看到ctx.res有相关的事件跟方法,稍微看了一下源代码,其实ctx.res就是 Noderes,相关代码可以稍微看一下:

  listen(...args) {
    // ...
    const server = http.createServer(this.callback());
    // ...
  }
//...
  callback() {
    //...
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      //...
    };
    return handleRequest;
  }
//...
  createContext(req, res) {
 //...
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
//...
    return context;
  }

Noderes是一个http.ServerResponse 类实例,而这个类继承自 stream 类。所以ctx.res本质上是一个stream类型的数据,相关的事件API可以翻阅stream

我们接着看 koa-logger的源码,下面看看log函数的实现:

function log(print, ctx, start, length_, err, event) {
  // 获取响应的状态码
  const status = err
    ? err.isBoom
      ? err.output.statusCode
      : err.status || 500
    : ctx.status || 404;

  // 根据响应码设置对应的字体颜色
  const s = (status / 100) | 0; // 位运算符的妙用,异常值得到 0,非异常值得到本身
  const color = colorCodes.hasOwnProperty(s) ? colorCodes[s] : colorCodes[0];

  // 使用 bytes 库计算响应内容的大小
  const length = [204, 205, 304].includes(status)
    ? ''
    : length_ == null
    ? '-'
    : bytes(length_).toLowerCase();

  // 根据情况设置标识符,使用 chalk 库给字体设置颜色
  const upstream = err
    ? chalk.red('xxx')
    : event === 'close'
    ? chalk.yellow('-x-')
    : chalk.gray('-->');

  // 输出响应日志
  print(
    '  ' +
      upstream +
      ' ' +
      chalk.bold('%s') +
      ' ' +
      chalk.gray('%s') +
      ' ' +
      chalk[color]('%s') +
      ' ' +
      chalk.gray('%s') +
      ' ' +
      chalk.gray('%s'),
    ctx.method,
    ctx.originalUrl,
    status,
    time(start),
    length
  );
}

// 补充一下不同响应码对应的颜色
const colorCodes = {
  7: 'magenta',
  5: 'red',
  4: 'yellow',
  3: 'cyan',
  2: 'green',
  1: 'green',
  0: 'yellow'
};

至此,我们就分析完整个koa-logger的源码了。