滬江CCtalk視頻地址:https://www.cctalk.com/v/15114923883523
log 日志中間件
最困難的事情就是認(rèn)識(shí)自己霹琼。
在一個(gè)真實(shí)的項(xiàng)目中务傲,開(kāi)發(fā)只是整個(gè)投入的一小部分,版本迭代和后期維護(hù)占了極其重要的部分枣申。項(xiàng)目上線(xiàn)運(yùn)轉(zhuǎn)起來(lái)之后售葡,我們?nèi)绾沃理?xiàng)目運(yùn)轉(zhuǎn)的狀態(tài)呢?如何發(fā)現(xiàn)線(xiàn)上存在的問(wèn)題忠藤,如何及時(shí)進(jìn)行補(bǔ)救呢挟伙?記錄日志就是解決困擾的關(guān)鍵方案。正如我們每天寫(xiě)日記一樣模孩,不僅能夠記錄項(xiàng)目每天都做了什么尖阔,便于日后回顧,也可以將做錯(cuò)的事情記錄下來(lái)榨咐,進(jìn)行自我反省介却。完善的日志記錄不僅能夠還原問(wèn)題場(chǎng)景,還有助于統(tǒng)計(jì)訪(fǎng)問(wèn)數(shù)據(jù)块茁,分析用戶(hù)行為齿坷。
日志的作用
- 顯示程序運(yùn)行狀態(tài)
- 幫助開(kāi)發(fā)者排除問(wèn)題故障
- 結(jié)合專(zhuān)業(yè)的日志分析工具(如 ELK )給出預(yù)警
關(guān)于編寫(xiě) log 中間件的預(yù)備知識(shí)
log4js
本項(xiàng)目中的 log
中間件是基于 log4js 2.x
的封裝,Log4js 是 Node.js
中一個(gè)成熟的記錄日志的第三方模塊数焊,下文也會(huì)根據(jù)中間件的使用介紹一些 log4js
的使用方法永淌。
日志分類(lèi)
日志可以大體上分為訪(fǎng)問(wèn)日志和應(yīng)用日志。訪(fǎng)問(wèn)日志一般記錄客戶(hù)端對(duì)項(xiàng)目的訪(fǎng)問(wèn)佩耳,主要是 http
請(qǐng)求遂蛀。這些數(shù)據(jù)屬于運(yùn)營(yíng)數(shù)據(jù),也可以反過(guò)來(lái)幫助改進(jìn)和提升網(wǎng)站的性能和用戶(hù)體驗(yàn)蚕愤;應(yīng)用日志是項(xiàng)目中需要特殊標(biāo)記和記錄的位置打印的日志答恶,包括出現(xiàn)異常的情況饺蚊,方便開(kāi)發(fā)人員查詢(xún)項(xiàng)目的運(yùn)行狀態(tài)和定位 bug
。應(yīng)用日志包含了debug
悬嗓、info
污呼、warn
和 error
等級(jí)別的日志。
日志等級(jí)
log4js
中的日志輸出可分為如下7個(gè)等級(jí):
在應(yīng)用中按照級(jí)別記錄了日志之后包竹,可以按照指定級(jí)別輸出高于指定級(jí)別的日志燕酷。
日志切割
當(dāng)我們的項(xiàng)目在線(xiàn)上環(huán)境穩(wěn)定運(yùn)行后,訪(fǎng)問(wèn)量會(huì)越來(lái)越大周瞎,日志文件也會(huì)越來(lái)越大苗缩。日益增大的文件對(duì)查看和跟蹤問(wèn)題帶來(lái)了諸多不便,同時(shí)增大了服務(wù)器的壓力声诸。雖然可以按照類(lèi)型將日志分為兩個(gè)文件酱讶,但并不會(huì)有太大的改善。所以我們按照日期將日志文件進(jìn)行分割彼乌。比如:今天將日志輸出到 task-2017-10-16.log
文件泻肯,明天會(huì)輸出到 task-2017-10-17.log
文件。減小單個(gè)文件的大小不僅方便開(kāi)發(fā)人員按照日期排查問(wèn)題慰照,還方便對(duì)日志文件進(jìn)行遷移灶挟。
代碼實(shí)現(xiàn)
安裝 log4js
模塊
npm i log4js -S
log4js
官方簡(jiǎn)單示例
在 middleware/
目錄下創(chuàng)建 mi-log/demo.js
,并貼入官方示例代碼:
var log4js = require('log4js');
var logger = log4js.getLogger();
logger.level = 'debug';
logger.debug("Some debug messages");
然后在 /middleware/mi-log/
目錄下運(yùn)行:
cd ./middleware/mi-log/ && node demo.js
可以在終端看到如下輸出:
[2017-10-24 15:45:30.770] [DEBUG] default - Some debug messages
一段帶有日期毒租、時(shí)間稚铣、日志級(jí)別和調(diào)用 debug
方法時(shí)傳入的字符串的文本日志。實(shí)現(xiàn)了簡(jiǎn)單的終端日志輸出墅垮。
log4js
官方復(fù)雜示例
替換 mi-log/demo.js
中的代碼為如下:
const log4js = require('log4js');
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'error' } }
});
const logger = log4js.getLogger('cheese');
logger.trace('Entering cheese testing');
logger.debug('Got cheese.');
logger.info('Cheese is Gouda.');
logger.warn('Cheese is quite smelly.');
logger.error('Cheese is too ripe!');
logger.fatal('Cheese was breeding ground for listeria.');
再次在 /middleware/mi-log/
目錄下運(yùn)行:
node demo.js
運(yùn)行之后惕医,在當(dāng)前的目錄下會(huì)生成一個(gè)日志文件 cheese.log
文件,文件中有兩條日志并記錄了 error
及以上級(jí)別的信息噩斟,也就是如下內(nèi)容:
[2017-10-24 15:51:30.770] [ERROR] cheese - Cheese is too ripe!
[2017-10-24 15:51:30.774] [FATAL] cheese - Cheese was breeding ground for listeria.
注意: 日志文件產(chǎn)生的位置就是當(dāng)前啟動(dòng)環(huán)境的位置曹锨。
分析以上代碼就會(huì)發(fā)現(xiàn),configure
函數(shù)配置了日志的基本信息
{
/**
* 指定要記錄的日志分類(lèi) cheese
* 展示方式為文件類(lèi)型 file
* 日志輸出的文件名 cheese.log
*/
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
/**
* 指定日志的默認(rèn)配置項(xiàng)
* 如果 log4js.getLogger 中沒(méi)有指定剃允,默認(rèn)為 cheese 日志的配置項(xiàng)
* 指定 cheese 日志的記錄內(nèi)容為 error 及 error 以上級(jí)別的信息
*/
categories: { default: { appenders: ['cheese'], level: 'error' } }
}
改寫(xiě)為log中間件
創(chuàng)建 /mi-log/logger.js
文件沛简,并增加如下代碼:
const log4js = require('log4js');
module.exports = ( options ) => {
return async (ctx, next) => {
const start = Date.now()
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
await next()
const end = Date.now()
const responseTime = end - start;
logger.info(`響應(yīng)時(shí)間為${responseTime/1000}s`);
}
}
創(chuàng)建 /mi-log/index.js
文件,并增加如下代碼:
const logger = require("./logger")
module.exports = () => {
return logger()
}
修改 middleware/index.js
文件斥废,并增加對(duì) log
中間件的注冊(cè)椒楣, 如下代碼:
const path = require('path')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
// 引入日志中間件
const miLog = require('./mi-log')
module.exports = (app) => {
// 注冊(cè)中間件
app.use(miLog())
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}
打開(kāi)瀏覽器并訪(fǎng)問(wèn) http://localhost:3000
, 來(lái)發(fā)送一個(gè)http
請(qǐng)求牡肉。
如上捧灰,按照前幾節(jié)課程中講解的中間件的寫(xiě)法,將以上代碼改寫(xiě)為中間件。 基于 koa
的洋蔥模型毛俏,當(dāng) http
請(qǐng)求經(jīng)過(guò)此中間件時(shí)便會(huì)在 cheese.log
文件中打印一條日志級(jí)別為 info
的日志并記錄了請(qǐng)求的響應(yīng)時(shí)間炭庙。如此,便實(shí)現(xiàn)了訪(fǎng)問(wèn)日志的記錄煌寇。
實(shí)現(xiàn)應(yīng)用日志焕蹄,將其掛載到 ctx
上
若要在其他中間件或代碼中通過(guò) ctx
上的方法打印日志,首先需要在上下文中掛載 log
函數(shù)阀溶。打開(kāi) /mi-log/logger.js
文件:
const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
module.exports = () => {
const contextLogger = {}
log4js.configure({
appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
categories: { default: { appenders: ['cheese'], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
// 記錄請(qǐng)求開(kāi)始的時(shí)間
const start = Date.now()
// 循環(huán)methods將所有方法掛載到ctx 上
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
// 記錄完成的時(shí)間 作差 計(jì)算響應(yīng)時(shí)間
const responseTime = Date.now() - start;
logger.info(`響應(yīng)時(shí)間為${responseTime/1000}s`);
}
}
創(chuàng)建 contextLogger
對(duì)象腻脏,將所有的日志級(jí)別方法賦給對(duì)應(yīng)的 contextLogger
對(duì)象方法。在將循環(huán)后的包含所有方法的 contextLogger
對(duì)象賦給 ctx
上的 log
方法银锻。
打開(kāi) /mi-send/index.js
文件永品, 并調(diào)用 ctx
上的 log
方法:
module.exports = () => {
function render(json) {
this.set("Content-Type", "application/json")
this.body = JSON.stringify(json)
}
return async (ctx, next) => {
ctx.send = render.bind(ctx)
// 調(diào)用ctx上的log方法下的error方法打印日志
ctx.log.error('ikcamp');
await next()
}
}
在其他中間件中通過(guò)調(diào)用 ctx
上的 log
方法,從而實(shí)現(xiàn)打印應(yīng)用日志。
const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
module.exports = () => {
const contextLogger = {}
const config = {
appenders: {
cheese: {
type: 'dateFile', // 日志類(lèi)型
filename: `logs/task`, // 輸出的文件名
pattern: '-yyyy-MM-dd.log', // 文件名增加后綴
alwaysIncludePattern: true // 是否總是有后綴名
}
},
categories: {
default: {
appenders: ['cheese'],
level:'info'
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(`響應(yīng)時(shí)間為${responseTime/1000}s`);
}
}
修改日志類(lèi)型為日期文件,按照日期切割日志輸出辜昵,以減小單個(gè)日志文件的大小。這時(shí)候打開(kāi)瀏覽器并訪(fǎng)問(wèn) http://localhost:3000
症见,這時(shí)會(huì)自動(dòng)生成一個(gè) logs
目錄喂走,并生成一個(gè) cheese-2017-10-24.log
文件殃饿, 中間件執(zhí)行便會(huì)在其中中記錄下訪(fǎng)問(wèn)日志。
├── node_modules/
├── logs/
│ ├── cheese-2017-10-24.log
├── ……
├── app.js
抽出可配置量
const log4js = require('log4js');
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
// 提取默認(rèn)公用參數(shù)對(duì)象
const baseInfo = {
appLogLevel: 'debug', // 指定記錄的日志級(jí)別
dir: 'logs', // 指定日志存放的目錄名
env: 'dev', // 指定當(dāng)前環(huán)境芋肠,當(dāng)為開(kāi)發(fā)環(huán)境時(shí)乎芳,在控制臺(tái)也輸出,方便調(diào)試
projectName: 'koa2-tutorial', // 項(xiàng)目名帖池,記錄在日志中的項(xiàng)目信息
serverIp: '0.0.0.0' // 默認(rèn)情況下服務(wù)器 ip 地址
}
const { env, appLogLevel, dir } = baseInfo
module.exports = () => {
const contextLogger = {}
const appenders = {}
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
// 環(huán)境變量為dev local development 認(rèn)為是開(kāi)發(fā)環(huán)境
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](message)
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(`響應(yīng)時(shí)間為${responseTime/1000}s`);
}
}
代碼中奈惑,我們指定了幾個(gè)常量以方便后面提取,比如 appLogLevel
睡汹、dir
肴甸、env
等。 囚巴。并判斷當(dāng)前環(huán)境為開(kāi)發(fā)環(huán)境則將日志同時(shí)輸出到終端原在, 以便開(kāi)發(fā)人員在開(kāi)發(fā)是查看運(yùn)行狀態(tài)和查詢(xún)異常。
豐富日志信息
在 ctx
對(duì)象中彤叉,有一些客戶(hù)端信息是我們數(shù)據(jù)統(tǒng)計(jì)及排查問(wèn)題所需要的庶柿,所以完全可以利用這些信息來(lái)豐富日志內(nèi)容。在這里秽浇,我們只需要修改掛載 ctx
對(duì)象的 log
函數(shù)的傳入?yún)?shù):
logger[method](message)
參數(shù) message
是一個(gè)字符串浮庐,所以我們封裝一個(gè)函數(shù),用來(lái)把信息與上下文 ctx
中的客戶(hù)端信息相結(jié)合柬焕,并返回字符串审残。
增加日志信息的封裝文件 mi-log/access.js
:
module.exports = (ctx, message, commonInfo) => {
const {
method, // 請(qǐng)求方法 get post或其他
url, // 請(qǐng)求鏈接
host, // 發(fā)送請(qǐng)求的客戶(hù)端的host
headers // 請(qǐng)求中的headers
} = ctx.request;
const client = {
method,
url,
host,
message,
referer: headers['referer'], // 請(qǐng)求的源地址
userAgent: headers['user-agent'] // 客戶(hù)端信息 設(shè)備及瀏覽器信息
}
return JSON.stringify(Object.assign(commonInfo, client));
}
注意: 最終返回的是字符串梭域。
取出 ctx
對(duì)象中請(qǐng)求相關(guān)信息及客戶(hù)端 userAgent
等信息并轉(zhuǎn)為字符串。
在 mi-log/logger.js
文件中調(diào)用:
const log4js = require('log4js');
// 引入日志輸出信息的封裝文件
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
const baseInfo = {
appLogLevel: 'debug',
dir: 'logs',
env: 'dev',
projectName: 'koa2-tutorial',
serverIp: '0.0.0.0'
}
const { env, appLogLevel, dir, serverIp, projectName } = baseInfo
// 增加常量搅轿,用來(lái)存儲(chǔ)公用的日志信息
const commonInfo = { projectName, serverIp }
module.exports = () => {
const contextLogger = {}
const appenders = {}
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
// 將入?yún)Q為函數(shù)返回的字符串
logger[method](access(ctx, message, commonInfo))
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(access(ctx, {
responseTime: `響應(yīng)時(shí)間為${responseTime/1000}s`
}, commonInfo))
}
}
重啟服務(wù)器并訪(fǎng)問(wèn) http://localhost:3000
就會(huì)發(fā)現(xiàn)碰辅,日志文件的記錄內(nèi)容已經(jīng)變化。代碼到這里介时,已經(jīng)完成了大部分的日志功能没宾。下面我們完善下其他功能:自定義配置參數(shù)和捕捉錯(cuò)誤。
項(xiàng)目自定義內(nèi)容
安裝依賴(lài)文件 ip
:
npm i ip -S
修改 middleware/index.js
中的調(diào)用方法
const path = require('path')
const ip = require('ip')
const bodyParser = require('koa-bodyparser')
const nunjucks = require('koa-nunjucks-2')
const staticFiles = require('koa-static')
const miSend = require('./mi-send')
const miLog = require('./mi-log/logger')
module.exports = (app) => {
// 將配置中間件的參數(shù)在注冊(cè)中間件時(shí)作為參數(shù)傳入
app.use(miLog({
env: app.env, // koa 提供的環(huán)境變量
projectName: 'koa2-tutorial',
appLogLevel: 'debug',
dir: 'logs',
serverIp: ip.address()
}))
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: 'html',
path: path.join(__dirname, '../views'),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}
再次修改 mi-log/logger.js
文件:
const log4js = require('log4js');
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]
const baseInfo = {
appLogLevel: 'debug',
dir: 'logs',
env: 'dev',
projectName: 'koa2-tutorial',
serverIp: '0.0.0.0'
}
module.exports = (options) => {
const contextLogger = {}
const appenders = {}
// 繼承自 baseInfo 默認(rèn)參數(shù)
const opts = Object.assign({}, baseInfo, options || {})
// 需要的變量解構(gòu) 方便使用
const { env, appLogLevel, dir, serverIp, projectName } = opts
const commonInfo = { projectName, serverIp }
appenders.cheese = {
type: 'dateFile',
filename: `${dir}/task`,
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true
}
if (env === "dev" || env === "local" || env === "development") {
appenders.out = {
type: "console"
}
}
let config = {
appenders,
categories: {
default: {
appenders: Object.keys(appenders),
level: appLogLevel
}
}
}
const logger = log4js.getLogger('cheese');
return async (ctx, next) => {
const start = Date.now()
log4js.configure(config)
methods.forEach((method, i) => {
contextLogger[method] = (message) => {
logger[method](access(ctx, message, commonInfo))
}
})
ctx.log = contextLogger;
await next()
const responseTime = Date.now() - start;
logger.info(access(ctx, {
responseTime: `響應(yīng)時(shí)間為${responseTime/1000}s`
}, commonInfo))
}
}
將項(xiàng)目中自定義的量覆蓋默認(rèn)值沸柔,解構(gòu)使用循衰。以達(dá)到項(xiàng)目自定義的目的。
對(duì)日志中間件進(jìn)行錯(cuò)誤處理
對(duì)于日志中間件里面的錯(cuò)誤褐澎,我們也需要捕獲并處理会钝。在這里,我們提取一層進(jìn)行封裝工三。
打開(kāi) mi-log/index.js
文件迁酸,修改代碼如下:
const logger = require("./logger")
module.exports = (options) => {
const loggerMiddleware = logger(options)
return (ctx, next) => {
return loggerMiddleware(ctx, next)
.catch((e) => {
if (ctx.status < 500) {
ctx.status = 500;
}
ctx.log.error(e.stack);
ctx.state.logged = true;
ctx.throw(e);
})
}
}
如果中間件里面有拋出錯(cuò)誤,這里將通過(guò) catch
函數(shù)捕捉到并處理俭正,將狀態(tài)碼小于 500
的錯(cuò)誤統(tǒng)一按照 500
錯(cuò)誤碼處理奸鬓,以方便后面的 http-error
中間件顯示錯(cuò)誤頁(yè)面。 調(diào)用 log
中間件打印堆棧信息并將錯(cuò)誤拋出到最外層的全局錯(cuò)誤監(jiān)聽(tīng)進(jìn)行處理掸读。
到這里我們的日志中間件已經(jīng)制作完成串远。當(dāng)然,還有很多的情況我們需要根據(jù)項(xiàng)目情況來(lái)繼續(xù)擴(kuò)展儿惫,比如結(jié)合『監(jiān)控系統(tǒng)』澡罚、『日志分析預(yù)警』和『自動(dòng)排查跟蹤機(jī)制』等∩銮耄可以參考一下官方文檔留搔。
下一節(jié)中,我們將學(xué)習(xí)下如何處理請(qǐng)求錯(cuò)誤铛铁。
上一篇:iKcamp新課程推出啦~~~~~iKcamp|基于Koa2搭建Node.js實(shí)戰(zhàn)(含視頻)? 處理靜態(tài)資源