從零開始的Koa實(shí)戰(zhàn)(3) 日志

日志讓我們能夠監(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 對象的工具,提供了 responseerror 記錄前面提到的必要信息,接下來就需要在中間件里面去使用。

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 …

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容