这是我参与更文挑战的第 25 天,活动详情查看:更文挑战
koa-logger
是koa
的中间件,用于进行日志打印。简单使用如下所示:
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
,看到控制台打印日志如下所示:
它也接受自定义打印的方式,如下所示:
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
就是 Node
的 res
,相关代码可以稍微看一下:
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;
}
Node
的 res
是一个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
的源码了。