最近要把手上项目的日志打印规范一下,需要引入一个日志框架,经过多方调研,最终选择了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:
- It is optional
- The performance implications can be shown to be not extreme.
- 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
限于篇幅接下来的内容我们在后续的章节实现
文章评论