Egg 日志系统完善
Egg
已经内置了比较全面的日志了, 比如common-error
和egg-web
等, 会自动把错误日志聚集到一起等, 但是还缺少后端服务里比较关键的一类: access_log
, 以及node
用于转发请求较多的情况下, 记录转发的其他请求日志:request_log
. 而且现在服务基本都是部署在Docker
容器中, Egg
日志都是打印在本地磁盘上, 如果需要统一收集也会比较麻烦, 需要一个简单轻量的解决方案
自定义 logger
- 根据
Egg
的文档, 添加一个自定义logger
非常简单, 只需要在config
里添加相关配置, 使用时候获取就行了
- 同时记得打开前置代理模式, 我们的服务一般都会部署在负载均衡后面, 如果不打开获取的用户
IP
会是错误的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = (appInfo) => { config.customLogger = { accessLogger: { file: path.join(appInfo.root, 'logs', appInfo.name, 'access.log'), consoleLevel: 'DEBUG', }, requestLogger: { file: path.join(appInfo.root, 'logs', appInfo.name, 'request.log'), consoleLevel: 'DEBUG', }, }; config.proxy = true; };
|
access_log
首先来看看接口访问日志如何实现, 在上一篇内容中已经介绍了中间件和开发了错误处理中间件, 那么访问日志就很简单了, 在此中间件中加上相关逻辑就行. 同时这里也会有个问题, 在此中间件之前的报错是无法收集到日志的, 比如csrf
中间件, 实际使用中需要注意这个问题.
这里最开始是自动生成request_id
, 后来发现我们的LBC有接入tracing
, 于是在接入tracing
后, 日志里也加入了相关内容, 做到日志全链路打通
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
| module.exports = ({ level = 'DEBUG' }, app) => { const logger = app.getLogger('accessLogger');
return async function wrapper(ctx, next) { const now = new Date(); let access_log = { time: now.toISOString(), id: ctx.getTraceId(), remote_ip: ctx.ip || '', host: ctx.request.host, method: ctx.request.method, uri: ctx.href, user_agent: ctx.header['user-agent'] || '', http_referer: ctx.header.referer || '', status: 200, error: '', latency: 0, latency_human: '0', bytes_in: ctx.request.length || 0, bytes_out: 0, snapshot: LOG_UTILS.getValidateLogMap(ctx.args), response_body: {}, };
try { let result = await next(); if (!ctx.body && !_.isUndefined(result)) { if (_.isArray(result)) { let [_result, code = 200, msg = ''] = result; ctx.body = ctx.wrap({ data: _result, code, msg }); } else { ctx.body = ctx.wrap({ data: result }); } } return result; } catch (e) { if (!_.isError(e)) { e = new Error(e); } e.trace_id = ctx.getTraceId(); ctx.logger.error(e); if (!ctx.res.finished) { result = ctx.wrap({ success: false, code: 500, msg: _.toString(e.message) }); if (level == 'DEBUG') { result.stack = e.stack || undefined; } ctx.body = result; ctx.status = 500; } access_log.error = _.toString(e.message); } finally { access_log.status = ctx.status; const spendTime = Date.now() - now.getTime(); access_log.latency_human = `${spendTime}ms`; access_log.latency = spendTime * 1000000; const logStr = JSON.stringify(access_log); logger.log('INFO', [logStr], { raw: true }); } }; };
|
这里面稍微有点坑的地方在于logger.log
, 如果不加第三个参数{ raw: true }
会发现日志里会自动加上一些时间/进程/级别的头部信息, 导致此条日志不是一个合法的json
格式, 解析起来会有问题. 最开始还担心改动起来比较麻烦, 查完源码后发现使用这个参数就能完美的解决这个问题了
request_log
- 当
app
启动的时候, 在app.httpclient
相关事件中注入日志埋点数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class AppBootHook { constructor(app) { const requestLogger = app.getLogger('requestLogger'); app.httpclient.on('request', (req) => { req.__start_time = new Date(); }); app.httpclient.on('response', ({ req, res, ctx }) => { const http_log = { time: req.__start_time.toISOString(), id: ctx ? ctx.getTraceId() : '', method: req.options.method, uri: req.url, status: res.status, latency: res.rt * 1000000, latency_human: `${res.rt}ms`, bytes_in: req.size, request_time: res.rt, snapshot: this.getValidateLogMap(req.args.data), result: this.getValidateLogMap(res.data), }; logger.log('INFO', [JSON.stringify(http_log)], { raw: true }); }); } }
|
rsyslog
上面的两个日志内容开发完成后, 在运行时就能在本地/日志文件夹中看到对应的日志记录了, 现在面临的问题是, 如何把本地Docker
容器的日志无缝发送到远端日志收集器中. 了解之后发现使用rsyslog
是一个比较好的方案.
Transport
查看Egg
文档后, 发现它有提供将日志打印到其他媒介上的很简单的方案: 实现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
| class RsyslogTransport extends Transport { constructor(options) { super(options); } log(level, args, meta) { const result = super.log(level, args, meta); } close() { } }
class AppBootHook { constructor(app) { app.getLogger(loggerKey).set( 'rsyslog', new RsyslogTransport({ ...app.config.logger, level: logger.level, app, rsyslogConfig: { ...app.config.rsyslog.config }, }), ); } }
|
node-rsyslog
上面的内容开发完后, 发现自定义的日志传输通道就已经生效了, 剩下的就是将日志内容发送到远端的rsyslog-server
中
但是试了下npm
提供的rsyslog
包之后, 发现它不支持TCP
发送, 只支持UDP
发送, 而且有些细节实现貌似和标准也不一致, 于是只能参考它自己实现一个node-rsyslog
.
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
| const _ = require('lodash'); const Transport = require('egg-logger').Transport; const { RSyslog, SEVERITY } = require('./tools/node-rsyslog');
class RsyslogTransport extends Transport { constructor(options) { super(options); this.rsyslogConfig = options.rsyslogConfig; this.initRsysLog(); } initRsysLog() { this._rsyslog = new RSyslog(this.rsyslogConfig); this._rsyslog.on('error', (err) => { this.options.app.logger.error(err); this._rsyslog = null; if (!this._rsyslog_timer) { this._rsyslog_timer = setTimeout(() => { this.initRsysLog(); this.options.app.logger.info(`Reconnect rsyslog server`); this._rsyslog_timer = null; }, 1000); } }); } log(level, args, meta) { const result = super.log(level, args, meta); this._rsyslog && this._rsyslog.send(SEVERITY.NOTICE, _.trim(result).replace(/\r?\n/gi, ' ')); } close() { this._rsyslog && this._rsyslog.disconnect(); } }
module.exports = RsyslogTransport;
|
整合
最后就剩下将rsyslog
的配置抽离到config
文件中了, 并且实现不同的日志打印到不同的facility
中
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
| module.exports = (appInfo) => { config.rsyslog = { enable: true, config: { host: '', port: 0, method: 'TCP', appname: `${appInfo.name}-${config.appEnvTag}`, hostname: os.hostname(), procid: process.pid, }, logger: { logger: { appname: `${appInfo.name}-egg`, facility: FACILITY.local0 }, coreLogger: { appname: `${appInfo.name}-${config.appEnvTag}-core`, facility: FACILITY.local0 }, errorLogger: { appname: `${appInfo.name}-${config.appEnvTag}-error`, facility: FACILITY.local0 }, scheduleLogger: { appname: `${appInfo.name}-${config.appEnvTag}-schedule`, facility: FACILITY.local0 }, accessLogger: { appname: `${appInfo.name}-${config.appEnvTag}-access`, facility: FACILITY.local2 }, requestLogger: { appname: `${appInfo.name}-${config.appEnvTag}-request`, facility: FACILITY.local4 }, }, }; };
class AppBootHook { constructor(app) { if (app.config.rsyslog.enable) { _.map(app.config.rsyslog.logger, (loggerConfig, loggerKey) => { const logger = app.getLogger(loggerKey); logger.set( 'rsyslog', new RsyslogTransport({ ...app.config.logger, level: logger.level, app, rsyslogConfig: { ...app.config.rsyslog.config, ...loggerConfig }, }), ); }); } } }
|
日志收集
在Docker
中直接启动rsyslog
, 然后在config
文件中配置好地址后, 就可以在有日志的时候, 同步看见rsyslog
服务器中的日志打印, 日志收集器也只需要在此机器上采集日志存储到ELK