Skip to content

日志中使用 winston

安装

bash
pnpm install --save nest-winston winston

pnpm install winston-daily-rotate-file

快速入门

app.module.ts

ts
// 日志模块
import { WinstonModule, utilities } from "nest-winston";
import * as winston from "winston";
import "winston-daily-rotate-file";

@Module({
  imports: [
   // 其他 模块 LoginModule,
    WinstonModule.forRoot({
      transports: [
        new winston.transports.Console({
          level: 'info',
          format: winston.format.combine(
            winston.format.timestamp(),
            utilities.format.nestLike(),
          ),
        }),
        new winston.transports.DailyRotateFile({
          level: 'error',
          dirname: 'logs',
          filename: 'error-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '10m',
          maxFiles: '14d',
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.json(),
          ),
        }),
        new winston.transports.DailyRotateFile({
          level: 'info',
          dirname: 'logs',
          filename: 'info-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true, // 是否通过压缩的方式归档被轮换的日志文件。
          maxSize: '10m', // 设置日志文件的最大大小,m 表示 mb 。
          maxFiles: '14d', // 保留日志文件的最大天数,此处表示自动删除超过 14 天的日志文件。
          // 记录时添加时间戳信息
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.json(),
          ),
        }),
      ],
    }),
  ],
  //...
})

这样运行起来 就会在根目录生成一个 log 文件夹

升级版

利用 拦截器,中间件,过滤器 写的

(1) 创建工具

  • 新建一个 utils 文件夹 下面 新建一个 utils.ts
ts
import { Request } from "express";

export const getReqMainInfo: (req: Request) => {
  [prop: string]: any;
} = (req) => {
  const { query, headers, url, method, body, connection } = req;

  // 获取 IP
  const xRealIp = headers["X-Real-IP"];
  const xForwardedFor = headers["X-Forwarded-For"];
  const { ip: cIp } = req;
  const { remoteAddress } = connection || {};
  const ip = xRealIp || xForwardedFor || cIp || remoteAddress;

  return {
    url,
    host: headers.host,
    headers,
    ip,
    method,
    query,
    body,
  };
};

(2) 创建拦截器,中间件,过滤器

在 src 目录下面新建一个 common 文件夹

  • 过滤器

在 common 文件夹下面创建 filter 文件夹

然后再 filter 文件夹下面新建 global.filter.ts

ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Inject,
} from "@nestjs/common";
import { Request, Response } from "express";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { Logger } from "winston";
import { getReqMainInfo } from "../../utils/utils";

@Catch(HttpException)
export class GlobalExceptionFilter implements ExceptionFilter {
  // 注入日志服务相关依赖
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp(); // 获取当前执行上下文
    const res = ctx.getResponse<Response>(); // 获取响应对象
    const req = ctx.getRequest<Request>(); // 获取请求对象
    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;
    const response: any = exception.getResponse(); // 获取异常响应信息

    let msg =
      exception.message || (status >= 500 ? "Service Error" : "Client Error");
    if (
      Object.prototype.toString.call(response) === "[object Object]" &&
      response.message
    ) {
      msg = response.message;
    }
    const { query, headers, url, method, body } = req;

    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(msg, {
      status,
      req: getReqMainInfo(req),
      // stack: exception.stack,
    });

    res.status(status >= 500 ? status : 200).json({ code: status, msg });
  }
}
  • 拦截器

在 common 文件夹下面新建 interceptor 文件夹

在 interceptor 文件夹下面新建 global.interceptor.ts

ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
  Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { Request } from "express";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { Logger } from "winston";
import { getReqMainInfo } from "../../utils/utils";

@Injectable()
export class GlobalInterceptor implements NestInterceptor {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = context.switchToHttp();
    const req = ctx.getRequest<Request>();

    return next.handle().pipe(
      map((data) => {
        // 记录请求日志
        this.logger.info("response", {
          responseData: data,
          req: getReqMainInfo(req),
        });
        return {
          code: 200,
          data,
          msg: "success",
        };
      })
    );
  }
}
  • 中间件

在 common 文件夹下面新建 middleware 文件夹

在 middleware 文件夹下面新建 global.middleware.ts

ts
import { Injectable, NestMiddleware, Inject } from "@nestjs/common";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { Logger } from "winston";
import { getReqMainInfo } from "../../utils/utils";
@Injectable()
export class GlobalMiddleware implements NestMiddleware {
  // 注入日志服务相关依赖
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
  ) {}

  use(req: any, res: any, next: () => void) {
    // 获取请求信息
    const {
      query,
      headers: { host },
      url,
      method,
      body,
    } = req;

    // 记录日志
    this.logger.info("route", {
      req: getReqMainInfo(req),
    });
    next();
  }
}

(3)挂载到全局

在 app.module.ts 文件中挂载

ts
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { LoginModule } from "./Login/login.module";
import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
// 日志模块
import { WinstonModule, utilities } from "nest-winston";
import * as winston from "winston";
import "winston-daily-rotate-file";

// 加载中间件

import { GlobalMiddleware } from "./common/middleware/global.middleware";

// 过滤器

import { GlobalExceptionFilter } from "./common/filter/global.filter";

// 拦截器

import { GlobalInterceptor } from "./common/interceptor/global.interceptor";

@Module({
  imports: [
    LoginModule,
    WinstonModule.forRoot({
      transports: [
        new winston.transports.Console({
          level: "info",
          format: winston.format.combine(
            winston.format.timestamp(),
            utilities.format.nestLike()
          ),
        }),
        new winston.transports.DailyRotateFile({
          level: "error",
          dirname: "logs",
          filename: "error-%DATE%.log",
          datePattern: "YYYY-MM-DD",
          zippedArchive: true,
          maxSize: "10m",
          maxFiles: "14d",
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.json()
          ),
        }),
        new winston.transports.DailyRotateFile({
          level: "info",
          dirname: "logs",
          filename: "info-%DATE%.log",
          datePattern: "YYYY-MM-DD",
          zippedArchive: true, // 是否通过压缩的方式归档被轮换的日志文件。
          maxSize: "10m", // 设置日志文件的最大大小,m 表示 mb 。
          maxFiles: "14d", // 保留日志文件的最大天数,此处表示自动删除超过 14 天的日志文件。
          // 记录时添加时间戳信息
          format: winston.format.combine(
            winston.format.timestamp(),
            winston.format.json()
          ),
        }),
      ],
    }),
  ],
  controllers: [],
  providers: [
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalInterceptor,
    },
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(GlobalMiddleware).forRoutes("*");
  }
}