企业级全栈项目(14) winston记录所有日志

winston 是 Node.js 生态中最流行的日志库,通常配合 winston-daily-rotate-file 使用,以实现按天切割日志文件(防止一个日志文件无限膨胀到几个GB)。 我们将实现以下目标:

  1. 访问日志:记录所有 HTTP 请求(时间、IP、URL、Method、状态码、耗时)。
  2. 错误日志:记录所有的异常和报错堆栈。
  3. 日志切割:每天自动生成新文件,并自动清理旧日志(如保留30天)。
  4. 分环境处理:开发环境在控制台打印彩色日志,生产环境写入文件。

第一步:安装依赖

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:

  1. 自动化:日志自动按天分割,自动压缩,不用担心磁盘写满。
  2. 结构化:日志以 JSON 格式存储,方便以后接入 ELK (Elasticsearch, Logstash, Kibana) 做可视化监控。
  3. 可追溯:任何报错都有时间、堆栈和请求参数,运维和排查效率提升 10 倍。
相关推荐
空&白16 分钟前
vue暗黑模式
javascript·vue.js
梦帮科技34 分钟前
Node.js配置生成器CLI工具开发实战
前端·人工智能·windows·前端框架·node.js·json
css趣多多1 小时前
一个UI内置组件el-scrollbar
前端·javascript·vue.js
-凌凌漆-1 小时前
【vue】pinia中的值使用 v-model绑定出现[object Object]
javascript·vue.js·ecmascript
C澒2 小时前
前端整洁架构(Clean Architecture)实战解析:从理论到 Todo 项目落地
前端·架构·系统架构·前端框架
C澒2 小时前
Remesh 框架详解:基于 CQRS 的前端领域驱动设计方案
前端·架构·前端框架·状态模式
C澒2 小时前
前端分层架构实战:DDD 与 Clean Architecture 在大型业务系统中的落地路径与项目实践
前端·架构·系统架构·前端框架
Misnice3 小时前
Webpack、Vite、Rsbuild区别
前端·webpack·node.js
大橙子额4 小时前
【解决报错】Cannot assign to read only property ‘exports‘ of object ‘#<Object>‘
前端·javascript·vue.js
晚霞的不甘5 小时前
守护智能边界:CANN 的 AI 安全机制深度解析
人工智能·安全·语言模型·自然语言处理·前端框架