開發(fā)過程中,日志記錄是必不可少的事情,尤其是生產(chǎn)系統(tǒng)中經(jīng)常無法調(diào)試,因此日志就成了重要的調(diào)試信息來源。
1.expressWinston
訪問日志一般用來記錄每個客戶端對應(yīng)用的訪問請求。在Web應(yīng)用中,主要記錄HTTP請求中的關(guān)鍵數(shù)據(jù)。如下所示記錄:
{
"res":{
"statusCode":200
},
"req":{
"url":"/user/login",
"headers":{
"host":"localhost:8001",
"connection":"keep-alive",
"content-length":"348",
"postman-token":"0ef999d7-1b51-4bc5-c4d8-f067f165b2be",
"cache-control":"no-cache",
"origin":"chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop",
"user-agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"content-type":"multipart/form-data; boundary=----WebKitFormBoundaryef7X45UMFFjdZwsB",
"accept":"*/*",
"dnt":"1",
"accept-encoding":"gzip, deflate, br",
"accept-language":"zh-CN,zh;q=0.8,en;q=0.6",
"cookie":"SID=s%3AzPVGIcZLGk9osGdrAOpF9BULNZJnGwkj.INJURLoEcBKNFZlvCNonwpqFJq56lXlpQFIu15c6N1Y"
},
"method":"POST",
"httpVersion":"1.1",
"originalUrl":"/user/login",
"query":{
}
},
"responseTime":39,
"level":"info",
"message":"HTTP POST /user/login",
"timestamp":"2017-09-12T08:25:44.568Z"
}
中間件winston express-winston提供了請求的完整記錄,記錄的內(nèi)容有:
- res結(jié)果
- req請求包含:url, headlers, method, httpVersion, originalUrl, query六大項
- responseTime相應(yīng)時間
- level當(dāng)前日志等級
- message 請求基本信息
- timestamp 時間戳
其中對于日志中是否包含哪些信息,中間件也做了比較好的支持:
expressWinston.requestWhitelist.push('body');
expressWinston.responseWhitelist.push('body');
記錄日志的目的是為了分析,當(dāng)前的這些數(shù)據(jù)已經(jīng)足夠用來幫助分析Web應(yīng)用的用戶分布情況、服務(wù)器端的相應(yīng)時間、相應(yīng)狀態(tài)和客戶端類型等。通過這些數(shù)據(jù)反過來幫助我們改進(jìn)和提升網(wǎng)站。在nodejs中可以使用express-winston將日志打印在控制臺,也可以將其打印在本地文件中,還可以將其寫入至數(shù)據(jù)庫中。寫法如下:
import winston from 'winston';
import expressWinston from 'express-winston';
import winstonMongodb from 'winston-mongodb';
const MongoDB = winstonMongodb.MongoDB;
expressWinston.requestWhitelist.push('body');
app.use(expressWinston.logger({
transports: [
// 控制臺
new winston.transports.Console({
json: true,
colorize: true
}),
// 文件
new winston.transports.File({
filename: './logs/success.log'
}),
// mongodb數(shù)據(jù)庫
new winston.transports.MongoDB({
db: config.logsUrl
})
],
meta: true,
msg: "HTTP {{req.method}} {{req.url}}",
expressFormat: true,
colorize: true,
ignoreRoute: function (req, res) { return false; }
}));
其中db的寫法參考如下,常見問題:
db : 'mongodb://localhost:27017/Book-catalog',
有的開發(fā)者可能不太了解,會選擇將一些日志寫入到數(shù)據(jù)庫中。數(shù)據(jù)庫比日志好的地方在于它是結(jié)構(gòu)化的數(shù)據(jù),可以直接使用SQL語言進(jìn)行查詢和統(tǒng)計,日志文件則需要在加工之后才能分析。
但是日志文件和數(shù)據(jù)庫在寫入性能上是兩個級別,數(shù)據(jù)庫在寫入過程中要經(jīng)歷一系列處理,比如鎖表、日志等操作。寫日志文件則是直接將數(shù)據(jù)寫到磁盤上。相比之下,寫日志是輕量型操作,將日志分析和日志記錄步驟分離開來是較好的選擇。
線上業(yè)務(wù)可能訪問量巨大,產(chǎn)生的日志也可能是大量的,上述示例只是簡單的將普通日志和異常日志分開存放至兩個文件中,日志過多時也不便產(chǎn)看。為此,將產(chǎn)生的日志按日期分割是必要的。expressWinston可以使用winston-daily-rotate-file,引入第三個庫,目前看有點(diǎn)略麻煩,同時還不能將自己的輸出打入到日志中,我們準(zhǔn)備介紹下一個日志工具log4js。
2.log4js
Node.js,已經(jīng)有現(xiàn)成的開源日志模塊,就是log4js,源碼地址:點(diǎn)擊打開鏈接
項目引用方法:
npm install log4js
用法也是非常見簡單:
var log4js = require('log4js');
var logger = log4js.getLogger();
logger.level = 'debug'; // default level is OFF - which means no logs at all.
logger.debug("Some debug messages");
配置
log4js.configure(object || string)
配置屬性包含有:
- levels:用來定義日志等級
- appenders:map結(jié)構(gòu),定義日志輸入,必需定義type字段
- categories:map結(jié)構(gòu),定義分類,必需定義default分類,分離里面包含appenders和level屬性
- pm2:pm2日志
實例1
const log4js = require('log4js');
log4js.configure({
appenders: {
out: { type: 'stdout' },
app: { type: 'file', filename: 'application.log' }
},
categories: {
default: { appenders: [ 'out', 'app' ], level: 'debug' }
}
});
代碼中定義了兩個appenders,分別為out和app。out使用stdout追加日志方法,獨(dú)立輸出。app使用文件作為輸入,將內(nèi)容輸出到application.log。
Loggers
log4js.getLogger([category])
輸入哪種分類的log
Shutdown
log4js.shutdown(cb)
shutdown方法用于關(guān)閉log的接收,結(jié)束所有寫日志事件。
Custom Layouts
log4js.addLayout(type, fn)
用戶自定義數(shù)據(jù)記錄格式。
實例2
log4js.configure({
appenders: { 'out': { type: 'stdout', layout: { type: 'basic' } } },
categories: { default: { appenders: ['out'], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
logger.error('Cheese is too ripe!');
該配置替換stdout的默認(rèn)格式coloured,替換為basic。輸入為:
[2017-03-30 07:57:00.113] [ERROR] cheese - Cheese is too ripe!
實例3
log4js.configure({
appenders: { out: { type: 'stdout', layout: { type: 'messagePassThrough' } } },
categories: { default: { appenders: [ 'out' ], level: 'info' } }
});
const logger = log4js.getLogger('cheese');
const cheeseName = 'gouda';
logger.error('Cheese is too ripe! Cheese was: ', cheeseName);
該配置替換stdout的默認(rèn)格式coloured,替換為messagePassThrough。輸入為:
Cheese is too ripe! Cheese was: gouda
實例4
不得不說的type: 'pattern',通過正則輸出相應(yīng)的格式化數(shù)據(jù):
log4js.configure({
appenders: {
out: { type: 'stdout', layout: {
type: 'pattern',
pattern: '%d %p %c %X{user} %m%n'
}}
},
categories: { default: { appenders: ['out'], level: 'info' } }
});
const logger = log4js.getLogger();
logger.addContext('user', 'charlie');
logger.info('doing something.');
通過正則匹配也可以定義傳遞變量,如上代碼的user。輸出為:
2017-06-01 08:32:56.283 INFO default charlie doing something.
實例5
定義自己的輸出格式,type: 'json',如下所示:
const log4js = require('log4js');
log4js.addLayout('json', function(config) {
return function(logEvent) { return JSON.stringify(logEvent) + config.separator; }
});
log4js.configure({
appenders: {
out: { type: 'stdout', layout: { type: 'json', separator: ',' } }
},
categories: {
default: { appenders: ['out'], level: 'info' }
}
});
const logger = log4js.getLogger('json-test');
logger.info('this is just a test');
logger.error('of a custom appender');
logger.warn('that outputs json');
log4js.shutdown(() => {});
輸出為:
{"startTime":"2017-06-05T22:23:08.479Z","categoryName":"json-test","data":["this is just a test"],"level":{"level":20000,"levelStr":"INFO"},"context":{}},
{"startTime":"2017-06-05T22:23:08.483Z","categoryName":"json-test","data":["of a custom appender"],"level":{"level":40000,"levelStr":"ERROR"},"context":{}},
{"startTime":"2017-06-05T22:23:08.483Z","categoryName":"json-test","data":["that outputs json"],"level":{"level":30000,"levelStr":"WARN"},"context":{}},
具體使用
新建log.js文件,內(nèi)容如下所示:
var log4js = require('log4js');
log4js.configure({
appenders: {
out: { type: 'console' }, //控制臺輸出
app: {
type: "dateFile",
filename: 'logs/log.log',
pattern: "_yyyy-MM-dd",
alwaysIncludePattern: false,
}//日期文件格式
},
categories: {
default: { appenders: ['out'], level: 'info' },
logFile: { appenders: ['out', 'app'], level: 'ALL' },
},
replaceConsole: true, //替換console.log
});
var fileLog = log4js.getLogger('logFile');
exports.logger = fileLog;
exports.use = function (app) {
//頁面請求日志
app.use(log4js.connectLogger(fileLog));
}
輸入的log如下所示:
[2017-09-18 20:37:02.366] [INFO] logFile - ::1 - - "POST /user/login HTTP/1.1" 200 393 "" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"