自定义Egg Logger(一):egg-logger源码分析

简介

egg-logger 是 EGG 框架内置的企业级日志模块,提供了日志分级,统一错误日志、自定义日志、自动切割日志等特性,

该模块由 logger 和 transport 组成。

有哪些 Logger?

  • Agent Logger(egg-agent.log):agent 进程日志,框架和使用到 agent 进程执行任务的插件会打印一些日志到这里。
  • Core Logger(egg-web.log ): 框架内核、插件日志。
  • Error Logger( common-error.log ):实际一般不会直接使用它,任何 logger 的 .error() 调用输出的日志都会重定向到这里,重点通过查看此日志定位异常。
  • App Logger (${appInfo.name}-web.log):应用相关日志,供应用开发者使用的日志。我们在绝大数情况下都在使用它。
  • Custom Logger: 自定义的日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// egg-logger/lib/egg/loggers.js
class Loggers extends Map {
/**
* @constructor
* @param {Object} config - egg app config
* - logger
* - {String} env - egg app runtime env string, detail please see `app.config.env`
* - {String} type - current process type, `application` or `agent`
* - {String} dir - log file dir
* - {String} [encoding = utf8] - log string encoding
* - {String} [level = INFO] - file log level
* - {String} [consoleLevel = NONE] - console log level
* - {Boolean} [outputJSON = false] - send JSON log or not
* - {Boolean} [buffer = true] - use {@link FileBufferTransport} or not
* - {String} appLogName - egg app file logger name
* - {String} coreLogName - egg core file logger name
* - {String} agentLogName - egg agent file logger name
* - {String} errorLogName - err common error logger name
* - {String} eol - end of line char
* - {String} [concentrateError = duplicate] - whether write error logger to common-error.log, `duplicate` / `redirect` / `ignore`
* - customLogger
*/
constructor(config) {
super();

const loggerConfig = utils.assign({}, defaults, config.logger);
const customLoggerConfig = config.customLogger;

debug('Init loggers with options %j', loggerConfig);
assert(loggerConfig.type, 'should pass config.logger.type');
assert(loggerConfig.dir, 'should pass config.logger.dir');
assert(loggerConfig.appLogName, 'should pass config.logger.appLogName');
assert(loggerConfig.coreLogName, 'should pass config.logger.coreLogName');
assert(loggerConfig.agentLogName, 'should pass config.logger.agentLogName');
assert(loggerConfig.errorLogName, 'should pass config.logger.errorLogName');

// Error Logger
const errorLogger = new ErrorLogger(
utils.assign({}, loggerConfig, {
file: loggerConfig.errorLogName,
})
);
this.set('errorLogger', errorLogger);

// Agent Logger and Core Logger
if (loggerConfig.type === 'agent') {
const logger = new Logger(
utils.assign({}, loggerConfig, {
file: loggerConfig.agentLogName,
})
);
this.set('logger', logger);

const coreLogger = new Logger(
utils.assign({}, loggerConfig, loggerConfig.coreLogger, {
file: loggerConfig.agentLogName,
})
);
this.set('coreLogger', coreLogger);
} else {
const logger = new Logger(
utils.assign({}, loggerConfig, {
file: loggerConfig.appLogName,
})
);
this.set('logger', logger);

const coreLogger = new Logger(
utils.assign({}, loggerConfig, loggerConfig.coreLogger, {
file: loggerConfig.coreLogName,
})
);
this.set('coreLogger', coreLogger);
}

// Custom Logger

for (const name in customLoggerConfig) {
const logger = new CustomLogger(utils.assign({}, loggerConfig, customLoggerConfig[name]));
this.set(name, logger);
}
}
}

有哪些 Transport?

Transport 是一种传输通道,一个 logger 可包含多个传输通道。比如默认的 logger 就有 fileTransport 和 consoleTransport 两个通道, 分别负责打印到文件和终端。

  • Console Transport: 指定日志级别,输出日志到控制台(egg-logger/transports/console.js)
  • File Transport : 输出日志到文件(egg-logger/transports/file.js)
  • File Buffer Transport : FileBufferTransport 是 FileTransport 的子类。在内存中保存日志,并间隔一定的时间将日志刷新到文件中(egg-logger/transports/file_buffer.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// egg-logger/lib/egg/logger.js
class EggLogger extends Logger {
/**
* @constructor
* @param {Object} options
* - {String} dir - log base dir
* - {String} file - log file, support relavie path
* - {String} [encoding = utf8] - log string encoding
* - {String} [level = INFO] - file log level
* - {String} [consoleLevel = NONE] - console log level
* - {Function} [formatter] - log format function
* - {String} [jsonFile] - JSON log file
* - {Boolean} [outputJSON = false] - send JSON log or not
* - {Boolean} [buffer] - use {@link FileBufferTransport} or not
* - {String} [eol] - end of line char
* - {String} [concentrateError] - whether write error logger to common-error.log, `duplicate` / `redirect` / `ignore`
*/
constructor(options) {
super(options);

if (!path.isAbsolute(this.options.file)) this.options.file = path.join(this.options.dir, this.options.file);

if (this.options.outputJSON === true && this.options.file) {
this.options.jsonFile = this.options.file.replace(/\.log$/, '.json.log');
}

const EggFileTransport = this.options.buffer === true ? FileBufferTransport : FileTransport;

const fileTransport = new EggFileTransport({
file: this.options.file, // `${appInfo.name}-web.log`路径
level: this.options.level || 'INFO', // 日志级别,默认INFO
encoding: this.options.encoding, // 日志编码默认UTF-8
formatter: this.options.formatter, // 日志输出格式化函数,如果为空使用utils.defaultFormatter
contextFormatter: this.options.contextFormatter, // 可以获取ctx的userId、ctx.tracer、method、url等信息
flushInterval: this.options.flushInterval, // 将内存中的日志刷新到文件中的间隔时间,默认1000
eol: this.options.eol, // \r\n
});
this.set('file', fileTransport); // 添加FileTransport到logger

if (this.options.jsonFile) {
const jsonFileTransport = new EggFileTransport({
file: this.options.jsonFile,
level: this.options.level,
encoding: this.options.encoding,
flushInterval: this.options.flushInterval,
json: true,
eol: this.options.eol,
});
this.set('jsonFile', jsonFileTransport); // 添加json格式的FileTransport到logger
}

const consoleTransport = new ConsoleTransport({
level: this.options.consoleLevel,
formatter: utils.consoleFormatter,
contextFormatter: this.options.contextFormatter,
eol: this.options.eol,
});
this.set('console', consoleTransport); // 添加ConsoleTransport到logger
}
}

也可以根据实际项目需要可以定义自己的 Transport。app.getLogger('xxLogger').set('custom', new CustomTransport(...));

如何输出日志?

egg-logger 通过 logger、transport 的 log 函数将需要打印的日志信息输出到 console 或者 file 中,具体如下:

1. 根据日志级别调用 Logger 的 log 函数
1
2
3
4
5
6
7
// egg-logger/lib/logger.js
['error', 'warn', 'info', 'debug'].forEach(level => {
const LEVEL = level.toUpperCase();
Logger.prototype[level] = function () {
this.log(LEVEL, arguments);
};
});
2. 发送日志到所有的 Transport
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// egg-logger/lib/logger.js

class Logger extends Map {
...
/**
* Send log to all transports.
* It's proxy to {@link Transport}'s log method.
* @param {String} level - log level
* @param {Array} args - log arguments
* @param {Object} meta - log meta
*/
log(level, args, meta) {
let excludes;
let { logger, options } = this.duplicateLoggers.get(level) || {};
if (logger) {
excludes = options.excludes;
logger.log(level, args, meta);
} else {
logger = this.redirectLoggers.get(level);
if (logger) {
logger.log(level, args, meta);
return;
}
}

for (const [ key, transport ] of this.entries()) {
if (transport.shouldLog(level) && !(excludes && excludes.includes(key))) {
transport.log(level, args, meta); // 调用transport下的log函数
}
}
}
...
}
3. Console Transport 和 File Transport 的 log 函数

通过调用父类 Transport 的 log 函数,格式化日志并输出到 Console、File

4. 格式化日志

Transport 类中,通过调用 utils.format(level, args, meta, options) 对日志进行格式化操作并返回

ctx.logger 与 app.logger 的区别?

app.logger 是 Logger 类的对象,而 ctx.logger 是在 app.logger 的基础上做的封装,是一个 ContextLogger 的对象。app.logger 调用的是 EggApplication 中的logger(),app.logger 是通过 egg-logger 的 Loggers 类创建,日志输出格式使用 utils.defaultFormatter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// egg/lib/egg.js
/**
* application logger, log file is `$HOME/logs/{appname}/{appname}-web`
* @member {Logger}
* @since 1.0.0
*/
get logger() {
return this.getLogger('logger');
}

// egg/lib/core/logger.js
const Loggers = require('egg-logger').EggLoggers;

module.exports = function createLoggers(app) {
const loggerConfig = app.config.logger;
loggerConfig.type = app.type;

if (app.config.env === 'prod' && loggerConfig.level === 'DEBUG' && !loggerConfig.allowDebugAtProd) {
loggerConfig.level = 'INFO';
}

const loggers = new Loggers(app.config);

// won't print to console after started, except for local and unittest
app.ready(() => {
if (loggerConfig.disableConsoleAfterReady) {
loggers.disableConsole();
}
});
loggers.coreLogger.info('[egg:logger] init all loggers with options: %j', loggerConfig);

return loggers;
};


// egg-logger/lib/utils.js
defaultFormatter(meta) {
return meta.date + ' ' + meta.level + ' ' + meta.pid + ' ' + meta.message;
},

输出示例:2020-12-14 17:42:31,903 INFO 41228 …

ctx.logger 是一个 ContextLogger 对象,调用egg/app/extend/context.js中的 logger()。该 logger 是在 app logger 的基础上进行了一次封装,添加了 ctx 的 userId、ctx.tracer、method、url 等输出信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// egg/app/extend/context.js
/**
* Wrap app.loggers with context infomation,
* if a custom logger is defined by naming aLogger, then you can `ctx.getLogger('aLogger')`
*
* @param {String} name - logger name
* @return {Logger} logger
*/
getLogger(name) {
let cache = this[CONTEXT_LOGGERS];
if (!cache) {
cache = this[CONTEXT_LOGGERS] = {};
}

// read from cache
if (cache[name]) return cache[name];

// get no exist logger
const appLogger = this.app.getLogger(name);
if (!appLogger) return null;

// write to cache
cache[name] = new this.app.ContextLogger(this, appLogger);
return cache[name];
},

/**
* Logger for Application, wrapping app.coreLogger with context infomation
*
* @member {ContextLogger} Context#logger
* @since 1.0.0
* @example
* ```js
* this.logger.info('some request data: %j', this.request.body);
* this.logger.warn('WARNING!!!!');
* ```
*/
get logger() {
return this.getLogger('logger');
}


// egg-logger/lib/egg/context_logger.js
/**
* Request context Logger, itself isn't a {@link Logger}.
*/
class ContextLogger {

/**
* @constructor
* @param {Context} ctx - egg Context instance
* @param {Logger} logger - Logger instance
*/
constructor(ctx, logger) {
this.ctx = ctx;
this._logger = logger;
}

get paddingMessage() {
const ctx = this.ctx;

// Auto record necessary request context infomation, e.g.: user id, request spend time
// format: '[$userId/$ip/$traceId/$use_ms $method $url]'
const userId = ctx.userId || '-';
const traceId = ctx.tracer && ctx.tracer.traceId || '-';
const use = ctx.starttime ? Date.now() - ctx.starttime : 0;
return '[' +
userId + '/' +
ctx.ip + '/' +
traceId + '/' +
use + 'ms ' +
ctx.method + ' ' +
ctx.url +
']';
}

write(msg) {
this._logger.write(msg);
}
}

[ 'error', 'warn', 'info', 'debug' ].forEach(level => {
const LEVEL = level.toUpperCase();
ContextLogger.prototype[level] = function() {
const meta = {
formatter: contextFormatter,
paddingMessage: this.paddingMessage,
};
Object.defineProperty(meta, 'ctx', {
enumerable: false,
value: this.ctx,
});
this._logger.log(LEVEL, arguments, meta);
};
});

module.exports = ContextLogger;

function contextFormatter(meta) {
return meta.date + ' ' + meta.level + ' ' + meta.pid + ' ' + meta.paddingMessage + ' ' + meta.message;
}

输出示例:2020-12-14 17:42:32,183 INFO 41228 [-/127.0.0.1/affe8330-3df0-11eb-a788-5fdd50ee8eb3/297ms POST /app/v1/login]…