winston 是 Node.js 生态中最流行的日志库,通常配合 winston-daily-rotate-file 使用,以实现按天切割日志文件(防止一个日志文件无限膨胀到几个GB)。 我们将实现以下目标:
- 访问日志:记录所有 HTTP 请求(时间、IP、URL、Method、状态码、耗时)。
- 错误日志:记录所有的异常和报错堆栈。
- 日志切割:每天自动生成新文件,并自动清理旧日志(如保留30天)。
- 分环境处理:开发环境在控制台打印彩色日志,生产环境写入文件。
第一步:安装依赖
js
npm install winston winston-daily-rotate-file
第二步:封装 Logger 工具类 (src/utils/logger.js)
我们需要创建一个全局单例的 Logger 对象。
js
import winston from 'winston'
import 'winston-daily-rotate-file'
import path from 'path'
// 定义日志目录
const logDir = 'logs'
// 定义日志格式
const { combine, timestamp, printf, json, colorize } = winston.format
// 自定义控制台打印格式
const consoleFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`
if (Object.keys(metadata).length > 0) {
msg += JSON.stringify(metadata)
}
return msg
})
// 创建 Logger 实例
const logger = winston.createLogger({
level: 'info', // 默认日志级别
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
json() // 文件中存储 JSON 格式,方便后续用 ELK 等工具分析
),
transports: [
// 1. 错误日志:只记录 error 级别的日志
new winston.transports.DailyRotateFile({
dirname: path.join(logDir, 'error'),
filename: 'error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
zippedArchive: true, // 压缩旧日志
maxSize: '20m', // 单个文件最大 20MB
maxFiles: '30d' // 保留 30 天
}),
// 2. 综合日志:记录 info 及以上级别的日志 (包含访问日志)
new winston.transports.DailyRotateFile({
dirname: path.join(logDir, 'combined'),
filename: 'combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d'
})
]
})
// 如果不是生产环境,也在控制台打印,并开启颜色
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
consoleFormat
)
}))
}
export default logger
第三步:编写 HTTP 访问日志中间件 (src/middleware/httpLogger.js)
我们需要一个中间件,像保安一样,记录进出的每一个请求。
js
import logger from '../utils/logger.js'
export const httpLogger = (req, res, next) => {
// 1. 记录请求开始时间
const start = Date.now()
// 2. 监听响应完成事件 (finish)
res.on('finish', () => {
// 计算耗时
const duration = Date.now() - start
// 获取 IP (兼容 Nginx 代理)
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress
// 组装日志信息
const logInfo = {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
ip: ip,
userAgent: req.headers['user-agent'] || ''
}
// 根据状态码决定日志级别
if (res.statusCode >= 500) {
logger.error('HTTP Request Error', logInfo)
} else if (res.statusCode >= 400) {
logger.warn('HTTP Client Error', logInfo)
} else {
logger.info('HTTP Access', logInfo)
}
})
next()
}
第四步:集成到入口文件 (app.js)
我们需要把 httpLogger 放在所有路由的最前面 ,把错误记录放在所有路由的最后面。
js
import express from 'express'
import logger from './utils/logger.js' // 引入 logger
import { httpLogger } from './middleware/httpLogger.js' // 引入中间件
import HttpError from './utils/HttpError.js'
// ... 其他引入 (helmet, cors 等)
const app = express()
// ==========================================
// 1. 挂载访问日志中间件 (必须放在最前面)
// ==========================================
app.use(httpLogger)
// ... 其他中间件 (json, cors, helmet) ...
// ... 你的路由 (routes) ...
// app.use('/api/admin', adminRouter)
// app.use('/api/app', appRouter)
// ==========================================
// 2. 全局错误处理中间件 (必须放在最后)
// ==========================================
app.use((err, req, res, next) => {
// 记录错误日志到文件
logger.error(err.message, {
stack: err.stack, // 记录堆栈信息,方便排查 Bug
url: req.originalUrl,
method: req.method,
ip: req.ip
})
// 如果是我们自定义的 HttpError,返回对应的状态码
if (err instanceof HttpError) {
return res.status(err.code).json({
code: err.code,
message: err.message
})
}
// 其它未知错误,统一报 500
res.status(500).json({
code: 500,
message: '服务器内部错误,请联系管理员'
})
})
const PORT = 3000
app.listen(PORT, () => {
logger.info(`Server is running on port ${PORT}`) // 使用 logger 打印启动信息
})
第五步:效果演示
1. 启动项目
js
nodemon app.js
会发现项目根目录下多了一个 logs 文件夹,里面有 combined 和 error 两个子文件夹。
2. 发起一个正常请求 (GET /api/app/product/list)
- 控制台:显示绿色的日志 [info]: HTTP Access {"method":"GET", "status": 200 ...}
- 文件 (logs/combined/combined-2023-xx-xx.log):写入了一行 JSON 记录。
3. 发起一个错误请求 (密码错误 400 或 代码报错 500)
- 文件 (logs/error/error-2023-xx-xx.log):会自动记录下详细的错误堆栈 stack,这对于排查线上问题至关重要,你再也不用盯着黑乎乎的控制台或者猜测报错原因了。
总结
通过引入 winston:
- 自动化:日志自动按天分割,自动压缩,不用担心磁盘写满。
- 结构化:日志以 JSON 格式存储,方便以后接入 ELK (Elasticsearch, Logstash, Kibana) 做可视化监控。
- 可追溯:任何报错都有时间、堆栈和请求参数,运维和排查效率提升 10 倍。