您现在的位置是:首页 >技术杂谈 >JS WEB框架Express日志模块winston和express-winston以及winston-daily-rotate-file优化网站首页技术杂谈

JS WEB框架Express日志模块winston和express-winston以及winston-daily-rotate-file优化

运维混子 2024-10-17 12:01:04
简介JS WEB框架Express日志模块winston和express-winston以及winston-daily-rotate-file优化

1.前言

1.Express的日志模块winston和express-winston已经提供了开箱即用的大多数功能,但是和其他语言相比,还缺失对日志记录的当前文件和行号的支持,需要自己实现,以此记录一下。
2.express-winston主要用于记录请求进入和结束时的一些信息,可以将req的请求地址,请求方法,查询参数,请求体参数等记录下来,可以将res的请求全程耗时等记录下来。
3.winston 用于业务代码中对需要记录日志的地方进行记录
4.winston-daily-rotate-file用户日志文件的自动滚存,压缩以及清理

2.版本

express-winston 4.2.0
winston 3.8.2
winston-daily-rotate-file 4.7.1
express 4.18.2

3.日志通道Transport

3.1控制台Transport

level是定义的常量日志级别
handleExceptions为true的话,会在日志中将发生异常的堆栈记录下来
format.combine中的format.label和format.timestamp会作为参数传递给myFormat({format: LOG_DATE_FORMAT}是对日期的格式化,否则是时间戳形式)

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');
const {createLogger, transports, format} = require("winston");
const {
    LOG_LEVEL,
    LOG_FILE_SIZE,
    LOG_FILE_COUNT,
    LOG_FREQUENCY,
    LOG_DATE_FORMAT,
    LOG_FILE_NAME,
    LOG_FILE_DIR
} = require("../config/setting")

const consoleTransport = new transports.Console(
    {
        level: LOG_LEVEL,
        handleExceptions: true,
        format: format.combine(
            format.label({label: "ExpressApp"}),
            format.timestamp({format: LOG_DATE_FORMAT}),
            myFormat
        )
    }
)

3.2日志文件Transport

dirname是日志文件存放的目录
filename是日志文件的文件名部分
datePattern是以’YYYY-MM-DD’格式化的日期,会拼接在文件名后面
zippedArchive为true的话,会对日志文件进行压缩保存
maxSize日志文件最大大小
maxFiles日志文件最大数量

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');
const {createLogger, transports, format} = require("winston");

const {
    LOG_LEVEL,
    LOG_FILE_SIZE,
    LOG_FILE_COUNT,
    LOG_FREQUENCY,
    LOG_DATE_FORMAT,
    LOG_FILE_NAME,
    LOG_FILE_DIR
} = require("../config/setting")

const fileTransport = new (transports.DailyRotateFile)({
    level: LOG_LEVEL,
    dirname: LOG_FILE_DIR,
    filename: LOG_FILE_NAME,
    datePattern: 'YYYY-MM-DD',
    format: format.combine(
        format.label({label: "ExpressApp"}),
        format.timestamp({format: LOG_DATE_FORMAT}),
        // format.simple()
        myFormat
    ),
    handleExceptions: true,
    handleRejections: true,
    frequency: LOG_FREQUENCY,
    zippedArchive: true,
    maxSize: LOG_FILE_SIZE,
    maxFiles: LOG_FILE_COUNT
})

4.日志记录格式myFormat

const myFormat = format.printf(({level, label, message, fileName, lineNo, timestamp}) => {
    let levelUp = level.toUpperCase()
    // let msgStr = JSON.stringify(message)
    return `${timestamp} [${label}] [${fileName}] [${lineNo} Line] [${levelUp}]: ${message}`;
});

5.业务日志记录实例LOGGER初始化以及优化

5.1初始化

exitOnError为true,winston会在遇到异常即刻停止运行,不再记录其它日志

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');
const {createLogger, transports, format} = require("winston");

const LOGGER = createLogger({
    transports: [
        consoleTransport,
        fileTransport
    ],
    exceptionHandlers: [
        consoleTransport,
        fileTransport
    ],
    rejectionHandlers: [
        consoleTransport,
        fileTransport
    ],
    exitOnError: true

});

5.2优化

1.重写LOGGER的各级别方法,将获取的文件名和行号通过meta传进去
2.我只重写了这几个级别,如需,其它可自己照样子重写

const {getStackInfo} = require("../config/utils")
LOGGER.info = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("info", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};

LOGGER.error = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("error", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};

LOGGER.debug = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("debug", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};

6.请求记录日志实例EXPRESS_LOGGER初始化以及优化

6.1优化

1.强制修改expressWinston.logger的options
2.const stackInfo = getStackInfo()获取日志记录时的文件名以及行号等
3.req.on(‘data’, (chunk))是为了拿到请求进入时的body数据,这是个回调函数
4.req.on(‘end’, ())会在请求完全进入时打印日志
5.自定义上述myFormat中message的输出格式,通过req可以拿到请求的协议protocol,请求的IP地址 hostname,请求的源URL地址 originalUrl,请求的方法method;请求体 reqBody是在req.on(‘data’, (chunk))中拿到的
6.给对象logEntry增加文件名fileName=stackInfo.fileName,行号lineNo=stackInfo.line属性
7.以info级别记录logEntry
8.在请求返回时以debug级别记录响应的时间responseTime以及响应的状态码statusCode

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');
const {getStackInfo} = require("../config/utils")

expressWinston.logger = (options = {}) => {
    const winstonInstance = options.winstonInstance || createLogger({
        transports: [consoleTransport, fileTransport]
    });
    return (req, res, next) => {
        const startTime = new Date();
        const stackInfo = getStackInfo()
        let reqBody=""
        let responseBody;
        req.on('data', (chunk) =>{
            reqBody += chunk
        })


        req.on('end', () => {

            const logEntry = {
                message: `${req.protocol.toUpperCase()} [${req.hostname} => SERVER] Request ${req.method} ${req.originalUrl} QueryParam: ${JSON.stringify(req.query)} Body: ${reqBody.replace("undefined","")}`,
                body: req.body, // 添加请求体
                query: req.query,
                headers: req.headers,
                method: req.method,
                originalUrl: req.originalUrl,
                protocol: req.protocol,
                ip: req.ip,
                user: req.user,
                serverName: req.hostname,
                serverTime: new Date().toJSON(),
            };
            logEntry.fileName = stackInfo.fileName
            logEntry.lineNo = stackInfo.line
            winstonInstance.info(logEntry);
        });
        
        res.on('finish', () => {

            let responseTime = (new Date() - startTime);
            const logResEntry = {
                message: `${req.protocol.toUpperCase()} [SERVER => ${req.hostname}] Response ${responseTime}ms ${res.statusCode} ${req.originalUrl}`,

            };
            logResEntry.fileName = stackInfo.fileName
            logResEntry.lineNo = stackInfo.line

            winstonInstance.debug(logResEntry);
        });

        next();		// 这一步不能落下
    };
};

6.2初始化

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');

const EXPRESS_LOGGER = expressWinston.logger({
    expressFormat: false,
    colorize: false,
    meta: true,

})

7.创建utils.js文件存放工具方法

1.这一段借鉴了别人的代码,stackIndex需要根据自己业务代码目录层级调整,可以在debug模式下查看堆栈
2.此方法最终会返回文件名称fileName,代码行号line,行中的位置pos等
3.projectRoot是项目的根目录,之所以去除是因为不想在日志中显示过长的路径

const projectRoot = join(__dirname, '../');
const getStackInfo = function () {
    // get call stack, and analyze it
    // get all file, method, and line numbers
    stackIndex = 3
    const stacklist = (new Error()).stack.split('
');

    // stack trace format:
    // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
    // do not remove the regex expresses to outside of this method (due to a BUG in node.js)
    const stackReg = /ats+(.*)s+((.*):(d*):(d*))/gi;
    const stackReg2 = /ats+()(.*):(d*):(d*)/gi;

    const s = stacklist[stackIndex] || stacklist[0];
    const sp = stackReg.exec(s) || stackReg2.exec(s);

    if (sp && sp.length === 5) {
        return {
         
            fileName: sp[2].replace(projectRoot, ""),
            line: sp[3],
            pos: sp[4],
            relativePath: basename(sp[2]),
            stack: stacklist.join('
')
        }
    }
}


module.exports = {getStackInfo}

8.创建settings.js存放配置

const LOG_LEVEL = "debug"
const LOG_FILE_SIZE = "20m"
const LOG_FILE_COUNT = "5"
const LOG_FREQUENCY = "24h"
const LOG_DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS"
const LOG_FILE_NAME = "nodejsServer-%DATE%.log"
const LOG_FILE_DIR = "./logs"

module.exports = {
    
    LOG_LEVEL,
    LOG_FILE_SIZE,
    LOG_FILE_COUNT,
    LOG_FREQUENCY,
    LOG_DATE_FORMAT,
    LOG_FILE_NAME,
    LOG_FILE_DIR
    
}

9.完整的log.js代码

require('winston-daily-rotate-file');
require('winston');
const expressWinston = require('express-winston');
const {
    LOG_LEVEL,
    LOG_FILE_SIZE,
    LOG_FILE_COUNT,
    LOG_FREQUENCY,
    LOG_DATE_FORMAT,
    LOG_FILE_NAME,
    LOG_FILE_DIR
} = require("../config/setting")
const {getStackInfo} = require("../config/utils")
const {createLogger, transports, format} = require("winston");


const myFormat = format.printf(({level, label, message, fileName, lineNo, timestamp}) => {
    let levelUp = level.toUpperCase()
    // let msgStr = JSON.stringify(message)
    return `${timestamp} [${label}] [${fileName}] [${lineNo} Line] [${levelUp}]: ${message}`;
});
const consoleTransport = new transports.Console(
    {
        level: LOG_LEVEL,
        handleExceptions: true,
        format: format.combine(
            format.label({label: "ExpressApp"}),
            format.timestamp({format: LOG_DATE_FORMAT}),
            // format.
            // format.colorize(),
            // format.simple()
            myFormat
        )
    }
)
const fileTransport = new (transports.DailyRotateFile)({
    level: LOG_LEVEL,
    dirname: LOG_FILE_DIR,
    filename: LOG_FILE_NAME,
    datePattern: 'YYYY-MM-DD',
    format: format.combine(
        format.label({label: "ExpressApp"}),
        format.timestamp({format: LOG_DATE_FORMAT}),
        // format.simple()
        myFormat
    ),
    handleExceptions: true,
    handleRejections: true,
    frequency: LOG_FREQUENCY,
    zippedArchive: true,
    maxSize: LOG_FILE_SIZE,
    maxFiles: LOG_FILE_COUNT
})
const LOGGER = createLogger({
    transports: [
        consoleTransport,
        fileTransport
    ],
    exceptionHandlers: [
        consoleTransport,
        fileTransport
    ],
    rejectionHandlers: [
        consoleTransport,
        fileTransport
    ],
    exitOnError: true

});

expressWinston.logger = (options = {}) => {
    const winstonInstance = options.winstonInstance || createLogger({
        transports: [consoleTransport, fileTransport]
    });
    return (req, res, next) => {
        const startTime = new Date();
        const stackInfo = getStackInfo()
        let reqBody=""
        let responseBody;
        req.on('data', (chunk) =>{
            reqBody += chunk
        })


        req.on('end', () => {

            const logEntry = {
                message: `${req.protocol.toUpperCase()} [${req.hostname} => SERVER] Request ${req.method} ${req.originalUrl} QueryParam: ${JSON.stringify(req.query)} Body: ${reqBody.replace("undefined","")}`,
                body: req.body, // 添加请求体
                query: req.query,
                headers: req.headers,
                method: req.method,
                originalUrl: req.originalUrl,
                protocol: req.protocol,
                ip: req.ip,
                user: req.user,
                serverName: req.hostname,
                serverTime: new Date().toJSON(),
            };
            logEntry.fileName = stackInfo.fileName
            logEntry.lineNo = stackInfo.line
            winstonInstance.info(logEntry);
        });
        res.on('data', (chunk) => {
            responseBody += chunk;
        });
        res.on('finish', () => {

            let responseTime = (new Date() - startTime);
            const logResEntry = {
                message: `${req.protocol.toUpperCase()} [SERVER => ${req.hostname}] Response ${responseTime}ms ${res.statusCode} ${req.originalUrl}`,

            };
            logResEntry.fileName = stackInfo.fileName
            logResEntry.lineNo = stackInfo.line

            winstonInstance.debug(logResEntry);
        });

        next();
    };
};

const EXPRESS_LOGGER = expressWinston.logger({
    expressFormat: false,
    colorize: false,
    meta: true,

})
LOGGER.info = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("info", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};

LOGGER.error = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("error", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};

LOGGER.debug = (message, meta) => {
    const stackInfo = getStackInfo()
    // 调用原始的 info() 方法进行日志输出
    LOGGER.log("debug", message, {fileName: stackInfo["fileName"], lineNo: stackInfo["line"]});
};



module.exports = {
    LOGGER, EXPRESS_LOGGER
}

10.以下是我的项目结构

日志模块全部代码在log.js里面
日志模块的一些常量配置在setting.js里面
日志模块用到一些工具函数等在utils.js里面

在这里插入图片描述

11.使用方法

11.1 LOGGER(主要在业务代码用于记录结果,参数,状态等)使用方法

const {LOGGER} = require("../log");
LOGGER.error(`test1`)
LOGGER.info(`test1`)
LOGGER.debug(`test1`)

11.2 EXPRESS_LOGGER(主要记录请求进来和返回的一些信息)使用方法

const express = require("express");
const app = new express();
const {EXPRESS_LOGGER} = require("./utils/log")
app.use(EXPRESS_LOGGER)

12.Tips

如果ORM框架用的是sequelize那么可以用LOGGER记录执行时的SQL
logging参数即用LOGGER.debug()记录执行SQL

const db = new Sequelize(DB_NAME, DB_USER, DB_PASSWD, {
  host: DB_HOST,
  dialect: DB_DIALECT,
  port:DB_PORT,
  //   pool: {
  //     max: 5,
  //     min: 0,
  //     idle: 10000,
  //   },
  dialectOptions: {
    // chartset: 'utf8mb4',--
    dateStrings: true,
    typeCast: true,
  },
  define: {
    // 字段以下划线(_)来分割(默认是驼峰命名风格)
    underscored: true,
    freezeTableName: false, //自定义表名,不设置会自动将表名转为复数形式
    timestamps: true, //自动生成更新时间、创建时间字段:updatedAt,createdAt
  },
  timezone: DB_TIMEZONE,
  logging: msg => LOGGER.debug(msg),
});
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。