Egg 日志

2020-02-06 14:11 更新

日志對(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。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)