您现在的位置是:首页 >技术杂谈 >JS WEB框架Express日志模块winston和express-winston以及winston-daily-rotate-file优化网站首页技术杂谈
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),
});