Egg 日志

2020-02-06 14:11 更新

日志對于 Web 開發(fā)的重要性毋庸置疑,它對于監(jiān)控應(yīng)用的運行狀態(tài)、問題排查等都有非常重要的意義。

框架內(nèi)置了強(qiáng)大的企業(yè)級日志支持,由 egg-logger 模塊提供。

主要特性:

  • 日志分級
  • 統(tǒng)一錯誤日志,所有 logger 中使用 .error() 打印的 ERROR 級別日志都會打印到統(tǒng)一的錯誤日志文件中,便于追蹤
  • 啟動日志和運行日志分離
  • 自定義日志
  • 多進(jìn)程日志
  • 自動切割日志
  • 高性能

日志路徑

  • 所有日志文件默認(rèn)都放在 ${appInfo.root}/logs/${appInfo.name} 路徑下,例如 /home/admin/logs/example-app。
  • 在本地開發(fā)環(huán)境 (env: local) 和單元測試環(huán)境 (env: unittest),為了避免沖突以及集中管理,日志會打印在項目目錄下的 logs 目錄,例如 /path/to/example-app/logs/example-app。

如果想自定義日志路徑:

// config/config.${env}.js
exports.logger = {
dir: '/path/to/your/custom/log/dir',
};

日志分類

框架內(nèi)置了幾種日志,分別在不同的場景下使用:

  • appLogger ${appInfo.name}-web.log,例如 example-app-web.log,應(yīng)用相關(guān)日志,供應(yīng)用開發(fā)者使用的日志。我們在絕大數(shù)情況下都在使用它。
  • coreLogger egg-web.log 框架內(nèi)核、插件日志。
  • errorLogger common-error.log 實際一般不會直接使用它,任何 logger 的 .error() 調(diào)用輸出的日志都會重定向到這里,重點通過查看此日志定位異常。
  • agentLogger egg-agent.log agent 進(jìn)程日志,框架和使用到 agent 進(jìn)程執(zhí)行任務(wù)的插件會打印一些日志到這里。

如果想自定義以上日志文件名稱,可以在 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

如果我們在處理請求時需要打印日志,這時候使用 Context Logger,用于記錄 Web 行為相關(guān)的日志。

每行日志會自動記錄上當(dā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!!!!');

// 錯誤日志記錄,直接會將錯誤日志完整堆棧信息記錄下來,并且輸出到 errorLog 中
// 為了保證異??勺粉?,必須保證所有拋出的異常都是 Error 類型,因為只有 Error 類型才會帶上堆棧信息,定位到問題。
ctx.logger.error(new Error('whoops'));

對于框架開發(fā)者和插件開發(fā)者會使用到的 Context Logger 還有 ctx.coreLogger。

例如

ctx.coreLogger.info('info');

App Logger

如果我們想做一些應(yīng)用級別的日志記錄,如記錄啟動階段的一些數(shù)據(jù)信息,可以通過 App Logger 來完成。

// app.js
module.exports = app => {
app.logger.debug('debug info');
app.logger.info('啟動耗時 %d ms', Date.now() - start);
app.logger.warn('warning!');

app.logger.error(someErrorObj);
};

對于框架和插件開發(fā)者會使用到的 App Logger 還有 app.coreLogger。

// app.js
module.exports = app => {
app.coreLogger.info('啟動耗時 %d ms', Date.now() - start);
};

Agent Logger

在開發(fā)框架和插件時有時會需要在 Agent 進(jìn)程運行代碼,這時使用 agent.coreLogger。

// agent.js
module.exports = agent => {
agent.logger.debug('debug info');
agent.logger.info('啟動耗時 %d ms', Date.now() - start);
agent.logger.warn('warning!');

agent.logger.error(someErrorObj);
};

如需詳細(xì)了解 Agent 進(jìn)程,請參考多進(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,
};

日志級別

日志分為 NONE,DEBUG,INFO,WARN 和 ERROR 5 個級別。

日志打印到文件中的同時,為了方便開發(fā),也會同時打印到終端中。

文件日志級別

默認(rèn)只會輸出 INFO 及以上(WARN 和 ERROR)的日志到文件中。

可通過如下方式配置輸出到文件日志的級別:

打印所有級別日志到文件中:

// 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 級別的日志,如果確實有需求在生產(chǎn)環(huán)境打印 DEBUG 日志進(jìn)行調(diào)試,需要打開 allowDebugAtProd 配置項。

// config/config.prod.js
exports.logger = {
level: 'DEBUG',
allowDebugAtProd: true,
};

終端日志級別

默認(rèn)只會輸出 INFO 及以上(WARN 和 ERROR)的日志到終端中。(注意:這些日志默認(rèn)只在 local 和 unittest 環(huán)境下會打印到終端)

  • logger.consoleLevel: 輸出到終端日志的級別,默認(rèn)為 INFO

可通過如下方式配置輸出到終端日志的級別:

打印所有級別日志到終端:

// config/config.${env}.js
exports.logger = {
consoleLevel: 'DEBUG',
};

關(guān)閉所有打印到終端的日志:

// config/config.${env}.js
exports.logger = {
consoleLevel: 'NONE',
};
  • 基于性能的考慮,在正式環(huán)境下,默認(rèn)會關(guān)閉終端日志輸出。如有需要,你可以通過下面的配置開啟。(不推薦)
// config/config.${env}.js
exports.logger = {
disableConsoleAfterReady: false,
};

自定義日志

增加自定義日志

一般應(yīng)用無需配置自定義日志,因為日志打太多或太分散都會導(dǎo)致關(guān)注度分散,反而難以管理和難以排查發(fā)現(xiàn)問題。

如果實在有需求可以如下配置:

// 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}`;
},
},
},
};
};

高級自定義日志

日志默認(rèn)是打印到日志文件中,當(dāng)本地開發(fā)時同時會打印到終端。 但是,有時候我們會有需求把日志打印到其他媒介上,這時候我們就需要自定義日志的 transport。

Transport 是一種傳輸通道,一個 logger 可包含多個傳輸通道。比如默認(rèn)的 logger 就有 fileTransport 和 consoleTransport 兩個通道, 分別負(fù)責(zé)打印到文件和終端。

舉個例子,我們不僅需要把錯誤日志打印到 common-error.log,還需要上報給第三方服務(wù)。

首先我們定義一個日志的 transport,代表第三方日志服務(wù)。

const util = require('util');
const Transport = require('egg-logger').Transport;

class RemoteErrorTransport extends Transport {

// 定義 log 方法,在此方法中把日志上報給遠(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,這樣每條日志就會同時打印到這個 transport 了
app.getLogger('errorLogger').set('remote', new RemoteErrorTransport({ level: 'ERROR', app }));

上面的例子比較簡單,實際情況中我們需要考慮性能,很可能采取先打印到內(nèi)存,再定時上傳的策略,以提高性能。

日志切割

企業(yè)級日志一個最常見的需求之一是對日志進(jìn)行自動切割,以方便管理。框架對日志切割的支持由 egg-logrotator 插件提供。

按天切割

這是框架的默認(rèn)日志切割方式,在每日 00:00 按照 .log.YYYY-MM-DD 文件名進(jìn)行切割。

以 appLog 為例,當(dāng)前寫入的日志為 example-app-web.log,當(dāng)凌晨 00:00 時,會對日志進(jìn)行切割,把過去一天的日志按 example-app-web.log.YYYY-MM-DD 的形式切割為單獨的文件。

按照文件大小切割

我們也可以按照文件大小進(jìn)行切割。例如,當(dāng)文件超過 2G 時進(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)行切割。

按照小時切割

我們也可以選擇按照小時進(jìn)行切割,這和默認(rèn)的按天切割非常類似,只是時間縮短到每小時。

例如,我們需要把 common-error.log 按照小時進(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 訪問是高頻訪問,每次打印日志都寫磁盤會造成頻繁磁盤 IO,為了提高性能,我們采用的文件日志寫入策略是:

日志同步寫入內(nèi)存,異步每隔一段時間(默認(rèn) 1 秒)刷盤

更多詳細(xì)請參考 egg-logger 和 egg-logrotator。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號