十一、自定义日志实现文件滚动日志和数据库日志(nestjs+next.js从零开始一步一步创建通用后台管理系统)

112 阅读2分钟

1. 使用 Winston 替换 NestJS 默认日志系统

步骤:

  1. 安装依赖

    pnpm install winston @types/winston
    
  2. 创建自定义 Logger 类src/logger/winston.logger.ts):

    import { LoggerService, Injectable } from '@nestjs/common';
    import { createLogger, format, transports, Logger } from 'winston';
    
    @Injectable()
    export class WinstonLogger implements LoggerService {
      private readonly logger: Logger;
    
      constructor() {
        this.logger = createLogger({
          level: 'info',
          format: format.combine(
            format.timestamp(),
            format.json(),
          ),
          transports: [new transports.Console()],
        });
      }
    
      log(message: string) {
        this.logger.info(message);
      }
    
      error(message: string, trace: string) {
        this.logger.error(message, { trace });
      }
    
      warn(message: string) {
        this.logger.warn(message);
      }
    
      debug(message: string) {
        this.logger.debug(message);
      }
    
      verbose(message: string) {
        this.logger.verbose(message);
      }
    }
    
    
  3. 全局启用 Loggermain.ts) 目的是替换掉框架默认的日志系统,统一使用winstin日志

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { WinstonLogger } from './logger/winston.logger';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule, {
        logger: new WinstonLogger(), // 覆盖默认 Logger
      });
      await app.listen(3000);
    }
    bootstrap();
    

4.**创建全局 Logger 模块(logger.module.ts): 全局模块是方便在各个模块中直接使用日志,不用再在每个模块中导入日志模块

import { Global, Module } from '@nestjs/common';
import { WinstonLogger } from './winston.logger';

@Global() // 标记为全局模块
@Module({
  providers: [WinstonLogger],
  exports: [WinstonLogger], // 导出供其他模块使用
})
export class LoggerModule {}
  1. 在根模块中导入app.module.ts):

    import { LoggerModule } from './logger/logger.module';
    
    @Module({
      imports: [LoggerModule], // 导入全局模块
      // ...其他配置
    })
    export class AppModule {}
    
  2. Service 中直接使用(无需修改 app.module.tsproviders):

    @Injectable()
    export class UserService {
      constructor(private readonly logger: WinstonLogger) {} // 自动注入
    }
    

2. 配置 Winston 使用文件滚动日志

步骤:

  1. 安装依赖

    pnpm install winston-daily-rotate-file
    
  2. 修改 Winston 配置winston.logger.ts):

    import * as DailyRotateFile from 'winston-daily-rotate-file';
    
    // 在构造函数中添加文件滚动配置
    constructor() {
      this.logger = createLogger({
        // ... 其他配置
        transports: [
          new transports.Console(),
          new DailyRotateFile({
            dirname: 'logs',
            filename: 'application-%DATE%.log',
            datePattern: 'YYYY-MM-DD',
            zippedArchive: true,
            maxSize: '20m',
            maxFiles: '14d',
          }),
        ],
      });
    }
    

3. 数据库日志(以 TypeORM 为例)

步骤:

  1. 创建 TypeORM 自定义 Loggersrc/logger/typeorm.logger.ts):

    import { Logger } from 'typeorm';
    import { WinstonLogger } from './winston.logger';
    
    export class TypeOrmCustomLogger implements Logger {
      constructor(private readonly winstonLogger: WinstonLogger) {}
    
      logQuery(query: string, parameters?: any[]) {
        this.winstonLogger.verbose(`Query: ${query} ${parameters ? JSON.stringify(parameters) : ''}`);
      }
    
      logError(error: string, query: string, parameters?: any[]) {
        this.winstonLogger.error(`Error: ${error}`, { query, parameters });
      }
    
      // 其他方法(logSchema, logMigration, logQuerySlow)类似
    }
    
  2. 在 TypeORM 配置中使用自定义 Loggerapp.module.ts):

    import { TypeOrmModule } from '@nestjs/typeorm';
    import { TypeOrmCustomLogger } from './logger/typeorm.logger';
    import { WinstonLogger } from './logger/winston.logger';
    
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'mysql',
          // ... 其他配置
          logger: new TypeOrmCustomLogger(new WinstonLogger()),
          logging: true, // 开启所有类型日志
        }),
      ],
    })
    export class AppModule {}
    

4. Redis 日志(以 ioredis 为例)

步骤:

  1. 在 Redis 客户端配置中绑定事件redis.provider.ts):
    import { Logger } from '@nestjs/common';
    import Redis from 'ioredis';
    import { WinstonLogger } from './winston.logger';
    
    export const redisProvider = {
      provide: 'REDIS_CLIENT',
      useFactory: (logger: WinstonLogger) => {
        const client = new Redis({
          host: 'localhost',
          port: 6379,
        });
    
        client.on('connect', () => logger.log('Redis connected'));
        client.on('error', (err) => logger.error('Redis error', err));
        return client;
      },
      inject: [WinstonLogger],
    };
    

5. 日志记录到 MySQL 数据库

步骤:

  1. 安装依赖

    npm install winston-mysql
    
  2. 创建 MySQL Transport 配置(修改 winston.logger.ts):

    import * as MySQLTransport from 'winston-mysql';
    
    // 在构造函数中添加 MySQL Transport
    constructor() {
      this.logger = createLogger({
        // ... 其他配置
        transports: [
          // ... 其他 transports
          new MySQLTransport({
            host: 'localhost',
            user: 'root',
            password: 'password',
            database: 'logs_db',
            table: 'system_logs',
            fields: {
              level: 'level',
              message: 'message',
              timestamp: 'timestamp',
              meta: 'meta',
            },
          }),
        ],
      });
    }
    
  3. 创建 MySQL 表

    CREATE TABLE system_logs (
      id INT AUTO_INCREMENT PRIMARY KEY,
      level VARCHAR(20),
      message TEXT,
      timestamp DATETIME,
      meta JSON
    );
    

5.HTTP 拦截器日志


1. 创建 HTTP 日志拦截器

新建文件 src/interceptors/http-logging.interceptor.ts,实现请求/响应日志记录:

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

@Injectable()
export class HttpLoggingInterceptor implements NestInterceptor {
  constructor(private readonly logger: WinstonLogger) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const httpContext = context.switchToHttp();
    const request = httpContext.getRequest();
    const response = httpContext.getResponse();

    const { method, originalUrl, body, headers } = request;
    const userAgent = headers['user-agent'] || '';
    const ip = request.ip || headers['x-forwarded-for'] || 'unknown';

    // 记录请求开始
    this.logger.log({
      message: 'HTTP Request Start',
      type: 'HTTP_REQUEST',
      method,
      url: originalUrl,
      userAgent,
      ip,
      // 可选:根据需求决定是否记录请求体(敏感信息需过滤)
      // body: this.sanitizeBody(body),
    });

    const startTime = Date.now();

    return next.handle().pipe(
      tap({
        next: (data) => {
          // 记录成功响应
          const duration = Date.now() - startTime;
          this.logger.log({
            message: 'HTTP Response Success',
            type: 'HTTP_RESPONSE',
            method,
            url: originalUrl,
            statusCode: response.statusCode,
            duration: `${duration}ms`,
            // 可选:记录响应体(数据量大时可关闭)
            // responseBody: data,
          });
        },
        error: (err) => {
          // 记录错误响应
          const duration = Date.now() - startTime;
          this.logger.error({
            message: 'HTTP Response Error',
            type: 'HTTP_ERROR',
            method,
            url: originalUrl,
            statusCode: err.getStatus?.() || 500,
            duration: `${duration}ms`,
            error: err.message,
            stack: err.stack, // 仅在开发环境记录堆栈
          });
        },
      }),
    );
  }

  // 可选:敏感信息脱敏
  private sanitizeBody(body: Record<string, any>): Record<string, any> {
    const sanitized = { ...body };
    if (sanitized.password) sanitized.password = '******';
    if (sanitized.token) sanitized.token = '******';
    return sanitized;
  }
}

2. 注册全局拦截器

app.module.ts 中全局启用拦截器:

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { WinstonLogger } from './logger/winston.logger';
import { HttpLoggingInterceptor } from './interceptors/http-logging.interceptor';

@Module({
  providers: [
    // 其他 providers...
    {
      provide: APP_INTERCEPTOR,
      useClass: HttpLoggingInterceptor,
    },
    WinstonLogger, // 确保 Logger 可注入
  ],
})
export class AppModule {}

3. 日志输出示例

winston.logger.ts 的日志格式中,format.json() 会将日志转为 JSON 结构。发送一个 GET /api/users 请求后,日志文件会输出:

请求日志

{
  "level": "info",
  "message": "HTTP Request Start",
  "type": "HTTP_REQUEST",
  "method": "GET",
  "url": "/api/users",
  "userAgent": "PostmanRuntime/7.32.3",
  "ip": "127.0.0.1",
  "timestamp": "2023-10-05T08:20:00.000Z"
}

响应成功日志

{
  "level": "info",
  "message": "HTTP Response Success",
  "type": "HTTP_RESPONSE",
  "method": "GET",
  "url": "/api/users",
  "statusCode": 200,
  "duration": "45ms",
  "timestamp": "2023-10-05T08:20:00.045Z"
}

响应错误日志

{
  "level": "error",
  "message": "HTTP Response Error",
  "type": "HTTP_ERROR",
  "method": "POST",
  "url": "/api/login",
  "statusCode": 401,
  "duration": "12ms",
  "error": "Invalid credentials",
  "stack": "Error: Invalid credentials\n    at AuthService.login (...)",
  "timestamp": "2023-10-05T08:21:30.000Z"
}