本文详细介绍如何在 Egg.js 框架中自定义请求级别的日志格式。通过继承 egg-logger 的 FileTransport 类,绕过原生 Context Logger 对 formatter 参数的限制,实现包含时间、进程ID、TraceId 等字段的统一日志规范。
Egg.js 简介与背景
Egg.js 是什么?
Egg.js 是专为构建企业级应用而设计的框架,旨在通过其生态孕育更多上层框架,从而降低开发团队和个人的开发与维护成本。
注:Egg.js 缩写为 Egg
背景
为了统一分析各业务的请求日志,组织制定了标准化的日志打印规范,例如:
[time][processId][traceId][userid] Hello World....
统一格式后,现有业务使用的日志工具无法直接满足该规范,因此需要进行改造。
前端 Node 中间层目前采用 Egg.js 框架,下文将阐述如何在 Egg.js 中自定义请求日志格式。
Egg.js 内置 Logger 分析
开始动手
Egg.js 内置了三种 Logger,分别是:
- Context Logger
- App Logger
- Agent Logger
Context Logger 主要用于记录与请求相关的日志,每行日志开头会自动包含当前请求的若干信息,如时间、IP 地址、请求 URL 等。
App Logger 用于记录应用级别的日志,例如程序启动日志。
Agent Logger 用于记录多进程模式运行下的日志。
若需自定义请求级别的日志,重点在于研究 Context Logger。最理想的方案是 Context Logger 本身支持配置化的自定义格式,通过在 Egg.js 的 config 配置文件中传入 formatter 参数即可实现自定义。
//config.default.js
exports.customLogger = {
log: {
file: 'appname.log',
formatter: (message)=>{
return `${message.time}${message.processid}`
}
}
}
自定义 Logger 实现方案
但随后发现此路不通,设置 formatter 参数并未生效。通过查看 Context Logger 的源码 context_logger.js,我们发现了原因。
[ 'error', 'warn', 'info', 'debug' ].forEach(level => {
const LEVEL = level.toUpperCase();
ContextLogger.prototype[level] = function() {
const meta = {
formatter: contextFormatter,
paddingMessage: this.paddingMessage,
};
this._logger.log(LEVEL, arguments, meta);
};
});
module.exports = ContextLogger;
function contextFormatter(meta) {
return meta.date + ' ' + meta.level + ' ' + meta.pid + ' ' + meta.paddingMessage + ' ' + meta.message;
}
从源码中可以看出,formatter 参数已被内部的一个自定义格式化函数覆盖,配置中定义的格式不会生效。此路不通,只能尝试自行实现 Logger 来解决。自行实现时需考虑以下几点:
- 日志需写入文件,错误日志单独写入一个文件。
- 需要支持按天或按小时切割日志。
- IO 性能。
若全部自行实现则过于繁琐。幸运的是,Egg 的这些 Logger 均基于 egg-logger 和 egg-logrotator 实现,因此我们可以基于现有基础进行开发。
Context Logger 基于 egg-logger 的 FileTransport 类进行文件落地,同时 FileTransport 默认配置了 egg-logrotator 的日志拆分功能。因此,我们只需继承 FileTransport 类并实现相应接口即可,代码如下:
//CoustomTransport.js
const FileTransport = require('egg-logger').FileTransport;
const moment = require('moment');
class CoustomTransport extends FileTransport {
constructor(options, ctx) {
super(options);
this.ctx = ctx;
}
log(level, args, meta) {
const prefixStr = this.buildFormat(level);
for (let i in args) {
if (args.hasOwnProperty(i)) {
if (parseInt(i, 10) === 0) {
args[i] = `${prefixStr}${args[i]}`;
}
if (parseInt(i, 10) === args.length - 1) {
args[i] += '\n';
}
}
}
super.log(level, args, meta);
}
buildFormat(level) {
const timeStr = `[${moment().format('YYYY-MM-DD HH:mm:ss.SSS')}]`;
const threadNameStr = `[${process.pid}]`;
const urlStr = `[${this.ctx.request.url}]`
return `${timeStr}${threadNameStr}${urlStr}`;
}
setUserId(userId) {
this.userId = userId;
}
}
module.exports = CoustomTransport;
我们通过 logger.info('Hello World') 打印日志,格式显示为我们自定义的格式。
请求信息获取与调用示例
至此,自定义日志格式的问题已解决。接下来是如何获取每次请求的信息?这需要借助 Egg.js 框架对 Context 的扩展功能。Context 是请求级别的对象,我们在 Context 的原型上扩展方法,即可获取该对象携带的每次请求信息。
//CustomLogger.js
const Logger = require('egg-logger').Logger;
const CoustomTransport = require('./CoustomTransport.js');
module.exports = function(ctx){
const logger = new Logger();
logger.set('file', new CoustomTransport({
level: 'INFO',
file: 'app.log'
}, ctx));
return logger;
};
// app/extend/context.js
/*
* Context对象扩展
* */
const Logger = require('egg-logger').Logger;
const CoustomTransport = require('./CoustomTransport');
const CustomLogger = require('./CustomLogger');
module.exports = {
get swLog() {
return CustomLogger(this);
}
};
调用
// app/controller/home.js
module.exports = app => {
class HomeController extends app.Controller {
async index() {
this.ctx.swLog.info('Hello World');
}
}
return HomeController;
};
结果
[2018-11-02 19:25:09.665][22896][/] Hello World
至此,我们便能完整实现请求级别的日志自定义。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。