日志讓我們能夠監(jiān)控應(yīng)用的運(yùn)行狀態(tài)、問題排查等。
經(jīng)過上一節(jié)的實(shí)戰(zhàn),我們已經(jīng)有了下面的目錄結(jié)構(gòu):
koa-blog
├── package.json
├── app.js
├── app
│ ├── router
│ | ├── homde.js
│ | └── index.js
│ └── view
│ ├── 404.html
│ └── index.html
└── config
└── config.js
日志對于 Web 后臺應(yīng)用來說是必要的,Koa 原生并不支持支持日志模塊,所幸 GitHub 已經(jīng)有很多優(yōu)秀的 Node.js 日志框架,這節(jié)實(shí)戰(zhàn)將使用 log4js-node 來處理 Koa 的日志,當(dāng)然也有像 koa-log4 這樣的 Koa 中間件對 log4js-node 進(jìn)行了封裝,本節(jié)實(shí)戰(zhàn)也會實(shí)現(xiàn)一個(gè)中間件來處理日志。
log4js-node的基本使用
log4js-node 是經(jīng)過對 log4js 框架進(jìn)行轉(zhuǎn)換來支持 node 的,在開始實(shí)戰(zhàn)之前,先耐心來看下 log4js 的基本使用方式:
const log4js = require("log4js"); // 引入 log4js
const logger = log4js.getLogger(); // 獲得 default category
logger.level = "debug"; // 設(shè)置 level
logger.debug("調(diào)試信息"); // 輸出日志
// 輸出: [2020-10-31T16:02:24.527] [DEBUG] default - 調(diào)試信息
默認(rèn)的情況下,default 類別(category )的日志 level 是設(shè)置為 OFF 的,上面的代碼將其設(shè)置為 "debug",這使得調(diào)試信息可以輸出到 stdout。
再來看一個(gè)示例:
const log4js = require("log4js");
// 對日志進(jìn)行配置
log4js.configure({
// 指定輸出文件類型和文件名
appenders: { cheese: { type: "file", filename: "cheese.log" } },
// appenders 指定了日志追加到 cheese
// level 設(shè)置為 error
categories: { default: { appenders: ["cheese"], level: "error" } }
});
const logger = log4js.getLogger(); // 獲取到 default 分類
logger.trace("Entering cheese testing");
logger.debug("Got cheese.");
logger.info("Cheese is Comté.");
logger.warn("Cheese is quite smelly.");
logger.error("Cheese is too ripe!"); // 從這里開始寫入日志文件
logger.fatal("Cheese was breeding ground for listeria.");
從上面的設(shè)置看到 appenders 指定了日志追加到 cheese(也就是cheese.log)里面去,level 設(shè)置為 "error",也就是說只有日志等級大于 "error" 的才會添加到 log 文件。
當(dāng)執(zhí)行了上面的代碼,可以看到項(xiàng)目目錄里面多了一個(gè) cheese.log 文件,內(nèi)如如下:
[2020-10-31T16:26:17.188] [ERROR] default - Cheese is too ripe!
[2020-10-31T16:26:17.194] [FATAL] default - Cheese was breeding ground for listeria.
有關(guān) log4js-node 的更多使用示例可以參考 example.js 以及 examples 目錄下的文件。
下面來進(jìn)入本節(jié)的實(shí)戰(zhàn)…
安裝 log4js
前面對 log4js-node 進(jìn)行了簡單介紹,現(xiàn)在來在應(yīng)用里面使用,首先安裝 log4js:
$ npm install log4js --save
設(shè)置 log4js
我們修改 config/config.js ,來對 log4js 編寫一些配置:
// config/config.js
const CONFIG = {
"API_PREFIX": "/api", // 配置了路由前綴
"LOG_CONFIG":
{
"appenders": {
"error": {
"category": "errorLogger", // logger 名稱
"type": "dateFile", // 日志類型為 dateFile
"filename": "logs/error/error", // 日志輸出位置
"alwaysIncludePattern": true, // 是否總是有后綴名
"pattern": "yyyy-MM-dd-hh.log" // 后綴,每小時(shí)創(chuàng)建一個(gè)新的日志文件
},
"response": {
"category": "resLogger",
"type": "dateFile",
"filename": "logs/response/response",
"alwaysIncludePattern": true,
"pattern": "yyyy-MM-dd-hh.log"
}
},
"categories": {
"error": {
"appenders": ["error"], // 指定日志被追加到 error 的 appenders 里面
"level": "error" // 等級大于 error 的日志才會寫入
},
"response": {
"appenders": ["response"],
"level": "info"
},
"default": {
"appenders": ["response"],
"level": "info"
}
}
}
};
module.exports = CONFIG;
寫好了配置,接下來就是使用配置,先來看一下使用的代碼示例:
const log4js = require("log4js");
const CONFIG = require('./config/config');
// 對日志進(jìn)行配置
log4js.configure(CONFIG);
// 分別獲取到 categories 里面的 error 和 response 元素
// 目的是為了輸出錯(cuò)誤日志和響應(yīng)日志
const errorLogger = log4js.getLogger('error');
const resLogger = log4js.getLogger('response');
// 輸出日志
errorLogger.error('錯(cuò)誤日志');
resLogger.info('響應(yīng)日志');
運(yùn)行完成之后,可以在 log 目錄查看到對應(yīng)的日志文件,里面的內(nèi)容分別如下:
錯(cuò)誤日志
[2020-10-31T17:12:37.263] [ERROR] error - 錯(cuò)誤日志
響應(yīng)日志
[2020-10-31T17:12:37.265] [INFO] response - 響應(yīng)日志
到這里一切正常工作,接下來就要將 Koa 應(yīng)用的每次請求響應(yīng)、報(bào)錯(cuò)等信息以一定的格式存入日志,我們自然想到日志格式和中間件,下面逐一來看怎么實(shí)現(xiàn)。
日志格式
我們關(guān)心用戶請求的信息有哪些?這里列出本節(jié)關(guān)注的日志內(nèi)容,包括訪問方法、請求原始地址、客戶端 IP、響應(yīng)狀態(tài)碼、響應(yīng)內(nèi)容、錯(cuò)誤名稱、錯(cuò)誤信息、錯(cuò)誤詳情、服務(wù)器響應(yīng)時(shí)間。
為了使請求產(chǎn)生的 log 方便查看,新增一個(gè)文件 app/util/log_format.js 來統(tǒng)一格式:
// app/util/log_format.js
const log4js = require('log4js');
const { LOG_CONFIG } = require('../../config/config'); //加載配置文件
log4js.configure(LOG_CONFIG);
let logFormat = {};
// 分別獲取到 categories 里面的 error 和 response 元素
// 目的是為了輸出錯(cuò)誤日志和響應(yīng)日志
let errorLogger = log4js.getLogger('error');
let resLogger = log4js.getLogger('response');
//封裝錯(cuò)誤日志
logFormat.error = (ctx, error, resTime) => {
if (ctx && error) {
errorLogger.error(formatError(ctx, error, resTime));
}
};
//封裝響應(yīng)日志
logFormat.response = (ctx, resTime) => {
if (ctx) {
resLogger.info(formatRes(ctx, resTime));
}
};
//格式化響應(yīng)日志
const formatRes = (ctx, resTime) => {
let responserLog = formatReqLog(ctx.request, resTime); // 添加請求日志
responserLog.push(`response status: ${ctx.status}`); // 響應(yīng)狀態(tài)碼
responserLog.push(`response body: \n${JSON.stringify(ctx.body)}`); // 響應(yīng)內(nèi)容
responserLog.push(`------------------------ end\n`); // 響應(yīng)日志結(jié)束
return responserLog.join("\n");
};
//格式化錯(cuò)誤日志
const formatError = (ctx, err, resTime) => {
let errorLog = formatReqLog(ctx.request, resTime); // 添加請求日志
errorLog.push(`err name: ${err.name}`); // 錯(cuò)誤名稱
errorLog.push(`err message: ${err.message}`); // 錯(cuò)誤信息
errorLog.push(`err stack: ${err.stack}`); // 錯(cuò)誤詳情
errorLog.push(`------------------------ end\n`); // 錯(cuò)誤信息結(jié)束
return errorLog.join("\n");
};
// 格式化請求日志
const formatReqLog = (req, resTime) => {
let method = req.method;
// 訪問方法 請求原始地址 客戶端ip
let formatLog = [`\n------------------------ ${method} ${req.originalUrl}`, `request client ip: ${req.ip}`];
if (method === 'GET') { // 請求參數(shù)
formatLog.push(`request query: ${JSON.stringify(req.query)}\n`)
} else {
formatLog.push(`request body: ${JSON.stringify(req.body)}\n`)
}
formatLog.push(`response time: ${resTime}`); // 服務(wù)器響應(yīng)時(shí)間
return formatLog;
};
module.exports = logFormat;
這段 JavaScript 最終返回了一個(gè) logFormat 對象的工具,提供了 response 和 error 記錄前面提到的必要信息,接下來就需要在中間件里面去使用。
logger 中間件
有了 log4js 的配置并且統(tǒng)一格式之后,我們需要將它們都做進(jìn)一個(gè)中間件中,這樣才能對每次請求和響應(yīng)生效,下面來創(chuàng)建一個(gè)中間件 logger :
// app/middleware/logger.js
const logFormat = require('../util/log_format');
const logger = () => {
return async (ctx, next) => {
const start = new Date(); //開始時(shí)間
let ms; //間隔時(shí)間
try {
await next(); // 下一個(gè)中間件
ms = new Date() - start;
logFormat.response(ctx, `${ms}ms`); //記錄響應(yīng)日志
} catch (error) {
ms = new Date() - start;
logFormat.error(ctx, error, `${ms}ms`); //記錄異常日志
}
}
};
module.exports = logger;
中間件 logger 已經(jīng)建立好,下面來使用這個(gè)中間件:
// ...
// 引入logger
+ const logger = require('./app/middleware/logger');
// 使用模板引擎
// ...
+ app.use(logger()); // 處理log的中間件
// ...
app.listen(3000, () => {
console.log('App started on http://localhost:3000/api')
});
都設(shè)置好了之后,執(zhí)行 npm start ,當(dāng)啟動(dòng)成功之后,我們看到項(xiàng)目多了一個(gè)目錄 logs ,里面有兩個(gè)文件,分別是報(bào)錯(cuò)日志和響應(yīng)日志。在瀏覽器中訪問 http://localhost:3000/api ,可以看到響應(yīng)日志里面添加了剛剛的訪問記錄。
完成這一節(jié)實(shí)戰(zhàn)之后,整個(gè)文件目錄如下:
koa-blog
├── package.json
├── app.js
├── app
│ ├── middleware
│ | └── logger.js
│ ├── router
│ | ├── homde.js
│ | └── index.js
│ ├── util
│ | └── log_format.js
│ └── view
│ ├── 404.html
│ └── index.html
├── logs
│ ├── error
│ └── response
└── config
└── config.js
當(dāng)然,我們不需要將 logs 目錄提交到 git 倉庫,我們可以在 .gitignore 文件中將其忽略。
下一步,我們來了解 MongoDB …