nodejs 日志框架 winston 详细配置 1

最近要把手上项目的日志打印规范一下,需要引入一个日志框架,经过多方调研,最终选择了winston。由于本人主要的开发语言是java,springboot那一套,日志打印的规范也力求按照之前使用log4j的格式靠拢,然而在真正使用对比下来,发现此框架虽然号称nodejs上功能最强大的日志框架,对比java任有一些基本的要求实现起来非常麻烦。经过多方尝试,算是基本实现了所需的功能,这里做一个记录。

这里需要完成的功能如下:

  • error日志和info日志分成两个文件打印,log.info和log.debug打印到xxx.log文件中,log.error打印到xxx-error.log文件中。
  • 每一条日志都需要打印文件名称和行号。
  • 错误日志需要包含错误堆栈。
  • 需要通过logger.info('xxxx:{},{}',aaa,obj)的方式填充日志参数。
  • 一次请求调用链上的日志需要打印同一个traceId。

这些功能在java中属于非常基础的功能,而换到nodejs则需要费一些周折。

按照日志级别打印到指定文件中

首先我们需要按照debug和info级别的日志打印到xxx.log文件中,error日志打印的xxx-error.log的文件中。这个需求我们要对winston的日志级别做一个了解:

定义一个logger的一般形式如下:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    //
    // - Write all logs with importance level of `error` or less to `error.log`
    // - Write all logs with importance level of `info` or less to `combined.log`
    //
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

这里的level代表日志级别。

上面的代码有两行注释:
  // - Write all logs with importance level of `error` or less to `error.log`
  // - Write all logs with importance level of `info` or less to `combined.log`
将重要级别为' error '或以下的日志写入error.log
将重要级别为' info'或以下的日志写入combined.log

winston中的日志记录级别符合RFC5424指定的严重性排序:

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
};

也就是说当一个transports的level定义为info时,当你打印error日志,其日志也会在info指定的日志路径中打印,所以如果按照一般的配置定义,则会造成同一条日志在不同的文件中重复打印的情况!
所以这里我们定义三个transport,分别用于打印info日志,控制台输出,和error日志。

const infoAndDebugTransport  = new DailyRotateFile({
    level: 'debug',
    filename: infologPath,
    datePattern: 'YYYY-MM-DD',
    zippedArchive: true,
    format: format.combine(
        format.timestamp({ format: "YYYY-MM-DD HH:mm:ss,SSS" }),
        format.align(),
        format.splat(),
        format.printf(
            (info) =>{
                const { timestamp, level, message,file,line,traceId } = info;
                return `[${timestamp}] [${level}] -- [${traceId}] ${file}:${line} ${message}`; // 包括文件名和行号
            }
        )
    ),
    maxSize: '1000m', // 每个日志文件最大尺寸
    maxFiles: '14d' // 保留的日志文件天数
});


const consoleTransport = new transports.Console({
    level: 'debug', // 控制台打印的日志级别
    format: format.combine(
        format.colorize(), // 添加颜色
        format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSS' }),
        format.align(),
        format.splat(),
        format.errors({ stack: false }), // 包括堆栈信息
        format.printf((info) => {
            const { timestamp, level, message,file,line,traceId } = info;
            return `[${timestamp}] [${level}] -- [${traceId}] ${file}:${line} ${message}`; // 包括文件名和行号
        })
    )
});

// 创建一个用于存放 error 级别日志的文件传输器,按照日期生成文件
const errorTransport = new DailyRotateFile({
    level: 'error',
    handleExceptions: true,
    filename: errorlogPath,
    datePattern: 'YYYY-MM-DD',
    format: format.combine(
        format.colorize(), // 添加颜色
        format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss,SSS' }),
        format.align(),
        format.splat(),
        format.printf(( error ) => {
            const { timestamp, level, message,file,line,traceId } = error;
            return `[${timestamp}] [error] -- [${traceId}] ${file}:${line} ${message}`;
        })
    ),
    zippedArchive: true,
    maxSize: '1000m', // 每个日志文件最大尺寸
    maxFiles: '14d' // 保留的日志文件天数
});

再定义两个logger,分别绑定infoAndDebugTransport  和 errorTransport 
 

export const winstonLogger = winston.createLogger({
    format: winston.format.simple(),
    transports: [
        infoAndDebugTransport,
        consoleTransport
    ]
});

export const errorWinstonLogger = winston.createLogger({
    format: winston.format.simple(),
    transports: [
        errorTransport,
        consoleTransport
    ]
});

再定义一个logger变量,再以日志级别为函数名,定义变量里的函数。

// 创建自定义logger对象
export const logger = {
    debug: (...args) => {
        winstonLogger.debug(getLogWithFileInfo(args))
    },
    info:  (...args) => {
        winstonLogger.info(getLogWithFileInfo(args))
    },
    error:  (...args) => {
        errorWinstonLogger.error(getLogWithFileInfo(args))
    },
};

由于不同的日志级别方法绑定了不同的logger,不同的logger又绑定了不同的transport,不同的transport又对应不同的日志路径,所以不会出现日志重复打印的情况!

打印文件名称和行号

显示日志打印的位置,这是一个日志框架最基本的功能,并且也是winston用户呼声很高的一个功能。然而,winston并不支持。并且似乎也不打算支持!

There is no plan to add this to winston currently. The logistics of adding this code have severe performance implications. It would require capturing a call-stack on every log call which is very expensive.

A PR would be welcome for this IFF:

  1. It is optional
  2. The performance implications can be shown to be not extreme.
  3. It comes with tests.

Until such PR is made I am locking this issue to avoid further +1s. If you wanted to leave a +1 apologies, but your +1 has already been heard. It is clear many folks want this feature, but I think most of those folks don't understand the perf side effects of the implementation details.

目前没有计划将此功能添加到 winston 中。添加这段代码涉及的后续工作会导致严重的性能问题。这将需要在每次日志调用时捕获调用栈,这是非常昂贵的。

如果符合以下条件,我们欢迎提交 PR:

1.它是可选的。

2.性能影响不是非常严重。

3.附带有测试。

在没有这样的 PR 提交之前,我将锁定此问题,以避免进一步的 +1。如果您想留下 +1,请原谅,但是您的 +1 已经被听到了。很明显,许多人希望有此功能,但我认为这些人大多数不了解实现细节的性能副作用。"

 https://github.com/winstonjs/winston/issues/200

最后一条回复是2016年,如今7年过去了,依旧没有看到符合条件的功能。所以我们只能自己实现了。

// 创建一个包装函数,用于记录带有文件名和行号的日志
function getLogLocation(args) {
    const errorTemp = new Error();
    const stack = errorTemp.stack.split('\n')[3]; // Get the third line of the stack trace
    const matches = /at\s+(.*):(\d+):(\d+)/.exec(stack); // 解析文件路径、行号和列号
    const file = path.basename(matches[1]); // 提取文件名部分
    const line = matches[2];

    // Use the captured file name, line number, and the concatenated message to log
    return {
        message:args,
        file,
        line,
    };
}

// 创建自定义logger对象
export const logger = {
    debug: (...args) => {
        // winstonLogger.debug(getLogWithFileInfo(args))
        winstonLogger.debug(getLogLocation(args))
    },
    info:  (...args) => {
        winstonLogger.info(getLogLocation(args))
    },
    error:  (...args) => {
        errorWinstonLogger.error(getLogLocation(args))
    },
};

 每次打印日志之前都会创建一个Error,解析堆栈上的信息从而获取日志打印的位置,尽管它会有一些性能损耗。

import {logger,  winstonLogger} from "../src/utils/logUtil";

describe('日志测试', () => {
     const param: routingArgType = {
            protocols: 'v2,v3,mixed',
            tokenInAddress: '0x190Eb8a183D22a4bdf278c6791b152228857c033',
            tokenInChainId: 137,
            tokenOutAddress: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
            tokenOutChainId: 137,
            amount: '1000000',
            type: 'exactIn'
    }

    it("日志测试", async ()=>{
        logger.debug("syncMeta2DB,chainId:s%,isReset:s%,param:s%", 2222, 33333, param, 'aaaaaa')
    })
})

结果如下 

[2023-11-27 22:03:31,525] [debug] -- [undefined] RouteService.test.ts:184     syncMeta2DB,chainId:s%,isReset:s%,param:s%,2222,33333,[object Object],aaaaaa

限于篇幅接下来的内容我们在后续的章节实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值