日志可以为我们提供关于系统行为的必要信息,便于定位线上代码问题,不至于抓瞎🤠,所以说日志是非常重要的,对于Node程序也是一样,日志工具也是多种多样,有log4js
、bunyan
、winston
,今天就来讲讲在GitHub
上面star
最多的winston
安装不用说吧,就是下面的代码
npm install winston
基本使用
const logger = require('winston')
logger.info('is info') // 也可以写成后面这样 logger.log('info', 'is info')
logger.warn('is warn') // logger.log('warn', 'is warn')
logger.error('is error') // logger.log('error', 'is error')
就会把日志打印到控制台中
[winston] Attempt to write logs with no transports {"message":"is info","level":"info"}
[winston] Attempt to write logs with no transports {"message":"is warn","level":"warn"}
[winston] Attempt to write logs with no transports {"message":"is error","level":"error"}
存储/输出机制(transports)
内置transports
有时候我们希望在控制台接收日志,并把日志保存到文件中
单纯使用pm2
的out.log
来记录日志的同学上述代码就可以实现,并且有level
分类
使用docker
等非pm2
启动的同学,也很简单,比如保存到指定为的的server.log
里面,只需要使用以下代码
const winston = require('winston')
const path = require('path')
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({filename: path.resolve(__dirname, '../logs/server.log')})
]
})
logger.info('print to the console and the file')
以上是使用到winston
内置存储/输出机制(transports):
- Console:控制台传输
- File:文件传输
除此之外内置的transports还有
- Http:
http
传输 - Stream:流传输
自定义transports
可以自己编写一个继承自 winston.Transport
类,并实现类的log
方法
const Transport = require('winston-transport');
const util = require('util');
module.exports = class YourCustomTransport extends Transport {
constructor(opts) {
super(opts);
}
log(info, callback) {
setImmediate(() => {
this.emit('logged', info);
});
callback();
}
};
日志文件根据时间切割(翻转)
安装日志翻转组件
npm install winston-daily-rotate-file
const dailyRotateFile = require('winston-daily-rotate-file');
const dateTransport = new dailyRotateFile({
filename: path.resolve(__dirname, './logs/server.log'),
maxSize: '50m',
createSymlink: true,
symlinkName: 'server.log'
})
const logger = winston.createLogger({
transports: [
dateTransport
]
});
上面的maxSize
就是文件旋转后的最大大小,单位为k
(kb)、m
(mb)、g
(gb)
会得到文件server.log.2021.01.28
当日志文件大小大于maxSize
的时候,能得到
server.log.2021.01.28.1
、server.log.2021.01.28.2
、server.log.2021.01.29
...
createSymlink
为真的时候,创建一个从指定名称(symlinkName
)连接当前活动日志文件的符号连接
方便ELK
扫描
日志脱敏
为了符合国家等保三级规定,必须对日志文件中存在的用户隐私信息进行加密,例如手机号,身份证等等
这时候可以用到winston
的自定义格式化功能
即在createLogger
的时候使用自定义format
const { createLogger, format } = require('winston');
const util = require('util')
// 自定义format
const formatLog = format.printf(info => {
// logger.info('is info')的info.message为is info
const msg = info.message;
// logger.info('my name is %s, my personal information is %s', 'xiaolin',{ name: 'xiaolin', phone: '15811111111' })
// info[SPLAT]就能取到'xiaolin'和{ name: 'xiaolin', phone: '15811111111' },方便字符串插值
if (typeof (msg) === 'string' && msg.includes('%s')) {// 兼容旧日志(格式)
const splat = info[SPLAT] || info.splat || [];
// 对对象进行脱敏
const splatInfo = splat.map(item => {
if (typeof item === 'object') {
// 在这里取到了phone的值,又知道这个是手机号码,就可以对其做脱敏,具体脱敏逻辑(encryptionLog)自己定义,合理利用正则表达式
return util.inspect(encryptionLog(item), false, null)
}
return item
})
info.message = util.format(msg, ...splatInfo);// 字符串插值
}
// info.timestamp 是使用了内置的格式化插件format.timestamp取到的值
const finalLog = `[${info.timestamp}] [${info.level}] ${info.message}`
return finalLog
})
exports.businessLog = createLogger({
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
formatLog
)
});
附上我自己写的脱敏
下面是我自己简单写的脱敏规则,即对日志进来的对象进行脱敏
const encryRules = require('./encryRules').encryRules
/**
* 日志脱敏深拷贝
* PS: 在深拷贝的时候顺便脱敏,避免修改到原对象
*/
const encryptionLog = (splat, keyName) => {
if (splat === null) return splat;
if (splat instanceof Date) return new Date(splat);
if (splat instanceof RegExp) return new RegExp(splat);
if (typeof splat !== "object") {
if (encryRules.rules.hasOwnProperty(keyName)) {
// 命中规则,进行正则脱敏
return encryRules.rules[keyName](splat)
}
return splat
};
let cloneSplat = new splat.constructor();
for (let key in splat) {
if (splat.hasOwnProperty(key)) {
// 递归拷贝
cloneSplat[key] = encryptionLog(splat[key], key);
}
}
return cloneSplat;
}
加密规则(encryRules.js)
const regularEncrypt = (str, ruleName) => {
if (str != null && str != undefined) {
switch (ruleName) {
case 'phone':// phone
return String(str).replace(/(\d{3})\d{4}(\d{4})/g, '$1****$2')
case 'email':// 邮箱
return str.replace(/([^@]*)/, word => {
word.slice(0, 3) + word.slice(3).replace(/.{1}/g, '*')
})
}
}
return str
}
const encryptPhone = (str) => regularEncrypt(str, 'phone')// 手机号脱敏
const encryptEmail = (str) => regularEncrypt(str, 'email')// 邮箱脱敏
// 规则对象
const rules = {
phone: encryptPhone,
email: encryptEmail
}
exports.encryRules = {
rules
}
最后
上述的功能对于一般场景来说就已经完全足够了🤪,对于winston
的其他功能,这里就不一一叙述了,具体可以到Github
上面查看相关文档,如果有什么更好的方法,欢迎评论区交流哈哈😜。
最后最后,希望觉得文章还行的靓仔靓女们可以给我点个赞,mua ~