开发过程中,日志记录是必不可少的事情,尤其是生产系统中经常无法调试,因此日志就成了重要的调试信息来源。
1.expressWinston
访问日志一般用来记录每个客户端对应用的访问请求。在Web应用中,主要记录HTTP请求中的关键数据。如下所示记录:
{
"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
提供了请求的完整记录,记录的内容有:
- res结果
- req请求包含:url, headlers, method, httpVersion, originalUrl, query六大项
- responseTime相应时间
- level当前日志等级
- message 请求基本信息
- timestamp 时间戳
其中对于日志中是否包含哪些信息,中间件也做了比较好的支持:
expressWinston.requestWhitelist.push('body');
expressWinston.responseWhitelist.push('body');
参考:express-winston
记录日志的目的是为了分析,当前的这些数据已经足够用来帮助分析Web应用的用户分布情况、服务器端的相应时间、相应状态和客户端类型等。通过这些数据反过来帮助我们改进和提升网站。在nodejs中可以使用express-winston将日志打印在控制台,也可以将其打印在本地文件中,还可以将其写入至数据库中。写法如下:
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数据库
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',
有的开发者可能不太了解,会选择将一些日志写入到数据库中。数据库比日志好的地方在于它是结构化的数据,可以直接使用SQL语言进行查询和统计,日志文件则需要在加工之后才能分析。
但是日志文件和数据库在写入性能上是两个级别,数据库在写入过程中要经历一系列处理,比如锁表、日志等操作。写日志文件则是直接将数据写到磁盘上。相比之下,写日志是轻量型操作,将日志分析和日志记录步骤分离开来是较好的选择。
线上业务可能访问量巨大,产生的日志也可能是大量的,上述示例只是简单的将普通日志和异常日志分开存放至两个文件中,日志过多时也不便产看。为此,将产生的日志按日期分割是必要的。expressWinston
可以使用winston-daily-rotate-file,引入第三个库,目前看有点略麻烦,同时还不能将自己的输出打入到日志中,我们准备介绍下一个日志工具log4js。
2.log4js
Node.js,已经有现成的开源日志模块,就是log4js,源码地址:点击打开链接
项目引用方法:
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结构,定义日志输入,必需定义type字段
- categories:map结构,定义分类,必需定义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追加日志方法,独立输出。app使用文件作为输入,将内容输出到application.log
。
Loggers
log4js.getLogger([category])
输入哪种分类的log
Shutdown
log4js.shutdown(cb)
shutdown方法用于关闭log的接收,结束所有写日志事件。
Custom Layouts
log4js.addLayout(type, fn)
用户自定义数据记录格式。
实例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的默认格式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的默认格式coloured,替换为messagePassThrough。输入为:
Cheese is too ripe! Cheese was: gouda
实例4
不得不说的type: 'pattern'
,通过正则输出相应的格式化数据:
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文件,内容如下所示:
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"