日志對(duì)于 Web 開發(fā)的重要性毋庸置疑,它對(duì)于監(jiān)控應(yīng)用的運(yùn)行狀態(tài)、問題排查等都有非常重要的意義。
框架內(nèi)置了強(qiáng)大的企業(yè)級(jí)日志支持,由 egg-logger 模塊提供。
主要特性:
- 日志分級(jí)
- 統(tǒng)一錯(cuò)誤日志,所有 logger 中使用 .error() 打印的 ERROR 級(jí)別日志都會(huì)打印到統(tǒng)一的錯(cuò)誤日志文件中,便于追蹤
- 啟動(dòng)日志和運(yùn)行日志分離
- 自定義日志
- 多進(jìn)程日志
- 自動(dòng)切割日志
- 高性能
日志路徑
- 所有日志文件默認(rèn)都放在 ${appInfo.root}/logs/${appInfo.name} 路徑下,例如 /home/admin/logs/example-app。
- 在本地開發(fā)環(huán)境 (env: local) 和單元測(cè)試環(huán)境 (env: unittest),為了避免沖突以及集中管理,日志會(huì)打印在項(xiàng)目目錄下的 logs 目錄,例如 /path/to/example-app/logs/example-app。
如果想自定義日志路徑:
// config/config.${env}.js exports.logger = { dir: '/path/to/your/custom/log/dir', };
|
日志分類
框架內(nèi)置了幾種日志,分別在不同的場(chǎng)景下使用:
- appLogger ${appInfo.name}-web.log,例如 example-app-web.log,應(yīng)用相關(guān)日志,供應(yīng)用開發(fā)者使用的日志。我們?cè)诮^大數(shù)情況下都在使用它。
- coreLogger egg-web.log 框架內(nèi)核、插件日志。
- errorLogger common-error.log 實(shí)際一般不會(huì)直接使用它,任何 logger 的 .error() 調(diào)用輸出的日志都會(huì)重定向到這里,重點(diǎn)通過查看此日志定位異常。
- agentLogger egg-agent.log agent 進(jìn)程日志,框架和使用到 agent 進(jìn)程執(zhí)行任務(wù)的插件會(huì)打印一些日志到這里。
如果想自定義以上日志文件名稱,可以在 config 文件中覆蓋默認(rèn)值:
// config/config.${env}.js module.exports = appInfo => { return { logger: { appLogName: `${appInfo.name}-web.log`, coreLogName: 'egg-web.log', agentLogName: 'egg-agent.log', errorLogName: 'common-error.log', }, }; };
|
如何打印日志
Context Logger
如果我們?cè)谔幚碚?qǐng)求時(shí)需要打印日志,這時(shí)候使用 Context Logger,用于記錄 Web 行為相關(guān)的日志。
每行日志會(huì)自動(dòng)記錄上當(dāng)前請(qǐng)求的一些基本信息, 如 [$userId/$ip/$traceId/${cost}ms $method $url]。
ctx.logger.debug('debug info'); ctx.logger.info('some request data: %j', ctx.request.body); ctx.logger.warn('WARNNING!!!!');
// 錯(cuò)誤日志記錄,直接會(huì)將錯(cuò)誤日志完整堆棧信息記錄下來,并且輸出到 errorLog 中 // 為了保證異??勺粉櫍仨毐WC所有拋出的異常都是 Error 類型,因?yàn)橹挥?Error 類型才會(huì)帶上堆棧信息,定位到問題。 ctx.logger.error(new Error('whoops'));
|
對(duì)于框架開發(fā)者和插件開發(fā)者會(huì)使用到的 Context Logger 還有 ctx.coreLogger。
例如
ctx.coreLogger.info('info');
|
App Logger
如果我們想做一些應(yīng)用級(jí)別的日志記錄,如記錄啟動(dòng)階段的一些數(shù)據(jù)信息,可以通過 App Logger 來完成。
// app.js module.exports = app => { app.logger.debug('debug info'); app.logger.info('啟動(dòng)耗時(shí) %d ms', Date.now() - start); app.logger.warn('warning!');
app.logger.error(someErrorObj); };
|
對(duì)于框架和插件開發(fā)者會(huì)使用到的 App Logger 還有 app.coreLogger。
// app.js module.exports = app => { app.coreLogger.info('啟動(dòng)耗時(shí) %d ms', Date.now() - start); };
|
Agent Logger
在開發(fā)框架和插件時(shí)有時(shí)會(huì)需要在 Agent 進(jìn)程運(yùn)行代碼,這時(shí)使用 agent.coreLogger。
// agent.js module.exports = agent => { agent.logger.debug('debug info'); agent.logger.info('啟動(dòng)耗時(shí) %d ms', Date.now() - start); agent.logger.warn('warning!');
agent.logger.error(someErrorObj); };
|
如需詳細(xì)了解 Agent 進(jìn)程,請(qǐng)參考多進(jìn)程模型。
日志文件編碼
默認(rèn)編碼為 utf-8,可通過如下方式覆蓋:
// config/config.${env}.js exports.logger = { encoding: 'gbk', };
|
日志文件格式
設(shè)置輸出格式為JSON,方便日志監(jiān)控系統(tǒng)分析
// config/config.${env}.js exports.logger = { outputJSON: true, };
|
日志級(jí)別
日志分為 NONE,DEBUG,INFO,WARN 和 ERROR 5 個(gè)級(jí)別。
日志打印到文件中的同時(shí),為了方便開發(fā),也會(huì)同時(shí)打印到終端中。
文件日志級(jí)別
默認(rèn)只會(huì)輸出 INFO 及以上(WARN 和 ERROR)的日志到文件中。
可通過如下方式配置輸出到文件日志的級(jí)別:
打印所有級(jí)別日志到文件中:
// config/config.${env}.js exports.logger = { level: 'DEBUG', };
|
關(guān)閉所有打印到文件的日志:
// config/config.${env}.js exports.logger = { level: 'NONE', };
|
生產(chǎn)環(huán)境打印 debug 日志
為了避免一些插件的調(diào)試日志在生產(chǎn)環(huán)境打印導(dǎo)致性能問題,生產(chǎn)環(huán)境默認(rèn)禁止打印 DEBUG 級(jí)別的日志,如果確實(shí)有需求在生產(chǎn)環(huán)境打印 DEBUG 日志進(jìn)行調(diào)試,需要打開 allowDebugAtProd 配置項(xiàng)。
// config/config.prod.js exports.logger = { level: 'DEBUG', allowDebugAtProd: true, };
|
終端日志級(jí)別
默認(rèn)只會(huì)輸出 INFO 及以上(WARN 和 ERROR)的日志到終端中。(注意:這些日志默認(rèn)只在 local 和 unittest 環(huán)境下會(huì)打印到終端)
- logger.consoleLevel: 輸出到終端日志的級(jí)別,默認(rèn)為 INFO
可通過如下方式配置輸出到終端日志的級(jí)別:
打印所有級(jí)別日志到終端:
// config/config.${env}.js exports.logger = { consoleLevel: 'DEBUG', };
|
關(guān)閉所有打印到終端的日志:
// config/config.${env}.js exports.logger = { consoleLevel: 'NONE', };
|
- 基于性能的考慮,在正式環(huán)境下,默認(rèn)會(huì)關(guān)閉終端日志輸出。如有需要,你可以通過下面的配置開啟。(不推薦)
// config/config.${env}.js exports.logger = { disableConsoleAfterReady: false, };
|
自定義日志
增加自定義日志
一般應(yīng)用無需配置自定義日志,因?yàn)槿罩敬蛱嗷蛱稚⒍紩?huì)導(dǎo)致關(guān)注度分散,反而難以管理和難以排查發(fā)現(xiàn)問題。
如果實(shí)在有需求可以如下配置:
// config/config.${env}.js const path = require('path');
module.exports = appInfo => { return { customLogger: { xxLogger: { file: path.join(appInfo.root, 'logs/xx.log'), }, }, }; };
|
可通過 app.getLogger('xxLogger') / ctx.getLogger('xxLogger') 獲取,最終的打印結(jié)果和 coreLogger 類似。
自定義日志格式
// config/config.${env}.js const path = require('path');
module.exports = appInfo => { return { customLogger: { xxLogger: { file: path.join(appInfo.root, 'logs/xx.log'), formatter(meta) { return `[${meta.date}] ${meta.message}`; }, // ctx logger contextFormatter(meta) { return `[${meta.date}] [${meta.ctx.method} ${meta.ctx.url}] ${meta.message}`; }, }, }, }; };
|
高級(jí)自定義日志
日志默認(rèn)是打印到日志文件中,當(dāng)本地開發(fā)時(shí)同時(shí)會(huì)打印到終端。 但是,有時(shí)候我們會(huì)有需求把日志打印到其他媒介上,這時(shí)候我們就需要自定義日志的 transport。
Transport 是一種傳輸通道,一個(gè) logger 可包含多個(gè)傳輸通道。比如默認(rèn)的 logger 就有 fileTransport 和 consoleTransport 兩個(gè)通道, 分別負(fù)責(zé)打印到文件和終端。
舉個(gè)例子,我們不僅需要把錯(cuò)誤日志打印到 common-error.log,還需要上報(bào)給第三方服務(wù)。
首先我們定義一個(gè)日志的 transport,代表第三方日志服務(wù)。
const util = require('util'); const Transport = require('egg-logger').Transport;
class RemoteErrorTransport extends Transport {
// 定義 log 方法,在此方法中把日志上報(bào)給遠(yuǎn)端服務(wù) log(level, args) { let log; if (args[0] instanceof Error) { const err = args[0]; log = util.format('%s: %s\n%s\npid: %s\n', err.name, err.message, err.stack, process.pid); } else { log = util.format(...args); }
this.options.app.curl('http://url/to/remote/error/log/service/logs', { data: log, method: 'POST', }).catch(console.error); } }
// app.js 中給 errorLogger 添加 transport,這樣每條日志就會(huì)同時(shí)打印到這個(gè) transport 了 app.getLogger('errorLogger').set('remote', new RemoteErrorTransport({ level: 'ERROR', app }));
|
上面的例子比較簡(jiǎn)單,實(shí)際情況中我們需要考慮性能,很可能采取先打印到內(nèi)存,再定時(shí)上傳的策略,以提高性能。
日志切割
企業(yè)級(jí)日志一個(gè)最常見的需求之一是對(duì)日志進(jìn)行自動(dòng)切割,以方便管理??蚣軐?duì)日志切割的支持由 egg-logrotator 插件提供。
按天切割
這是框架的默認(rèn)日志切割方式,在每日 00:00 按照 .log.YYYY-MM-DD 文件名進(jìn)行切割。
以 appLog 為例,當(dāng)前寫入的日志為 example-app-web.log,當(dāng)凌晨 00:00 時(shí),會(huì)對(duì)日志進(jìn)行切割,把過去一天的日志按 example-app-web.log.YYYY-MM-DD 的形式切割為單獨(dú)的文件。
按照文件大小切割
我們也可以按照文件大小進(jìn)行切割。例如,當(dāng)文件超過 2G 時(shí)進(jìn)行切割。
例如,我們需要把 egg-web.log 按照大小進(jìn)行切割:
// config/config.${env}.js const path = require('path');
module.exports = appInfo => { return { logrotator: { filesRotateBySize: [ path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'), ], maxFileSize: 2 * 1024 * 1024 * 1024, }, }; };
|
添加到 filesRotateBySize 的日志文件不再按天進(jìn)行切割。
按照小時(shí)切割
我們也可以選擇按照小時(shí)進(jìn)行切割,這和默認(rèn)的按天切割非常類似,只是時(shí)間縮短到每小時(shí)。
例如,我們需要把 common-error.log 按照小時(shí)進(jìn)行切割:
// config/config.${env}.js const path = require('path');
module.exports = appInfo => { return { logrotator: { filesRotateByHour: [ path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'), ], }, }; };
|
添加到 filesRotateByHour 的日志文件不再被按天進(jìn)行切割。
性能
通常 Web 訪問是高頻訪問,每次打印日志都寫磁盤會(huì)造成頻繁磁盤 IO,為了提高性能,我們采用的文件日志寫入策略是:
日志同步寫入內(nèi)存,異步每隔一段時(shí)間(默認(rèn) 1 秒)刷盤
更多詳細(xì)請(qǐng)參考 egg-logger 和 egg-logrotator。
更多建議: