苏格团队
作者: MaxPan
交流 QQ 群: 855833773
背景
组织为了更好的对各个业务的请求日志进行统一的分析, 制定了统一的日志打印规范, 比如:
[time][processId][traceId][userid] Hello World....
统一格式之后, 业务现有业务的日志工具打印出来的格式是无法满足该规范的, 所以我们需要对此进行改造.
我们前端目前 Node 中间层使用的框架是 Egg.JS, 所以下文讲述下如何在 Egg.JS 上自定义请求日志格式.
开始动手
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}`
- }
- }
- }
但不久我们发现这条路走不通, 设置了这个 formatter 并不起作用. 从 Context Logger 的源码中, 我们发现的端倪
- [ '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;
实现 CoustomTransport 类后, 我们就可以初始化 logger
- //CustomLogger.JS
- const Logger = require('egg-logger').Logger;
- const CoustomTransport = require('./CoustomTransport.js');
- const logger = new Logger();
- logger.set('file', new CoustomTransport({
- level: 'INFO',
- file: 'app.log'
- }));
- module.exports = logger;
我们通过 http://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
到此, 我们就能完整的自定义请求级别的日志了.
来源: https://juejin.im/post/5bdcfd1f518825171b2d820d