2021年了,该会日志脱敏了吧(Node篇)

4,840 阅读3分钟

日志可以为我们提供关于系统行为的必要信息,便于定位线上代码问题,不至于抓瞎🤠,所以说日志是非常重要的,对于Node程序也是一样,日志工具也是多种多样,有log4jsbunyanwinston,今天就来讲讲在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

有时候我们希望在控制台接收日志,并把日志保存到文件中

单纯使用pm2out.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.1server.log.2021.01.28.2server.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 ~

img