nest.js 日志记录

21 阅读2分钟

✅ 你的日志需求拆解(我逐条对)

你要的是:

  1. ✅ 记录 用户操作

    • 哪个接口
    • 哪些参数
    • 用户信息(可选)
  2. ✅ 区分日志级别

    • info(操作日志)
    • warn(警告)
    • error(异常、报错)
  3. 按日期生成文件

  4. 文件名带:年月日 + 时分秒

  5. 单个文件不能太大,自动切分

  6. 日志保留 N 天,自动删除

  7. NestJS 可全局使用

👉 这套需求,winston + winston-daily-rotate-file 正好 100% 覆盖


✅ 技术选型(结论)

能力方案
日志框架winston
文件切割winston-daily-rotate-file
Nest 集成自定义 Logger + Interceptor + ExceptionFilter
日志格式JSON(方便后期 ELK / Loki)

① 安装依赖

npm i winston winston-daily-rotate-file

② 日志目录结构(推荐)

logs/
├── info/
├── warn/
├── error/

③ 创建 Winston 日志实例(核心)

src/logger/winston.logger.ts

import * as winston from 'winston';
import 'winston-daily-rotate-file';

const baseFormat = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.json(),
);

function createRotateTransport(level: string) {
  return new winston.transports.DailyRotateFile({
    level,
    dirname: `logs/${level}`,
    filename: `%DATE%-${level}.log`,
    datePattern: 'YYYY-MM-DD-HH-mm-ss',
    zippedArchive: true,
    maxSize: '20m',        // 单文件最大 20MB
    maxFiles: '14d',       // 保留 14 天
  });
}

export const logger = winston.createLogger({
  format: baseFormat,
  transports: [
    createRotateTransport('info'),
    createRotateTransport('warn'),
    createRotateTransport('error'),
  ],
});

📌 重点你关心的点:

需求对应配置
文件名时间datePattern
文件大小切割maxSize
自动删除maxFiles: '14d'
分级目录dirname

④ NestJS 全局 Logger 封装

src/logger/app.logger.ts

import { LoggerService } from '@nestjs/common';
import { logger } from './winston.logger';

export class AppLogger implements LoggerService {
  log(message: any, context?: string) {
    logger.info({ message, context });
  }

  warn(message: any, context?: string) {
    logger.warn({ message, context });
  }

  error(message: any, trace?: string, context?: string) {
    logger.error({ message, trace, context });
  }
}

⑤ 记录【用户操作日志】——Interceptor(核心)

src/logger/operation.interceptor.ts

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { logger } from './winston.logger';

@Injectable()
export class OperationLogInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const { method, url, body, query, params } = req;
    const user = req.user || null;
    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        logger.info({
          type: 'operation',
          method,
          url,
          params,
          query,
          body,
          user,
          duration: Date.now() - start,
        });
      }),
    );
  }
}

📌 这一步 直接满足你:

  • 哪个接口
  • 哪些参数
  • 用户信息
  • 耗时

⑥ 全局异常日志(error)

src/logger/http-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { logger } from './winston.logger';

@Catch()
export class AllExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req = ctx.getRequest();

    logger.error({
      type: 'exception',
      method: req.method,
      url: req.url,
      body: req.body,
      error: exception.message,
      stack: exception.stack,
    });

    throw exception;
  }
}

⑦ main.ts 全局启用

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });

  app.useLogger(new AppLogger());
  app.useGlobalInterceptors(new OperationLogInterceptor());
  app.useGlobalFilters(new AllExceptionFilter());

  await app.listen(3000);
}

📂 最终日志示例

logs/info/2026-01-05-18-20-01-info.log

{
  "timestamp": "2026-01-05 18:20:01",
  "type": "operation",
  "method": "POST",
  "url": "/user/login",
  "body": { "username": "test" },
  "duration": 32
}

logs/error/2026-01-05-18-22-11-error.log

{
  "timestamp": "2026-01-05 18:22:11",
  "type": "exception",
  "url": "/user/login",
  "error": "password error"
}