引言

申请日志与谬误记录是后端服务中不可或缺的一环,对谬误排查和保障利用运行稳定性具备积极意义。综合 GitHub 活跃度和 Nest 官网举荐的因素,决定将 Winston 作为 Nest 利用的日志服务模块,本文将演示如何在 NestJS 中接入 Winston,实现日志记录性能。

引入与配置 Winston

相干依赖:winston、nest-winston、winston-daily-rotate-file

  • 其中 winston-daily-rotate-file 用于实现日志文件的定期归档。因为利用日志量个别都十分大,因而须要定期主动对日志文件进行轮换、归档与删除。

app.module.ts(主模块)

import {  // ...  Module,} from '@nestjs/common';import { WinstonModule } from 'nest-winston';import * as winston from 'winston';import 'winston-daily-rotate-file';// ...@Module({  controllers: [],  imports: [    // ...    WinstonModule.forRoot({      transports: [        new winston.transports.DailyRotateFile({          dirname: `logs`, // 日志保留的目录          filename: '%DATE%.log', // 日志名称,占位符 %DATE% 取值为 datePattern 值。          datePattern: 'YYYY-MM-DD', // 日志轮换的频率,此处示意每天。          zippedArchive: true, // 是否通过压缩的形式归档被轮换的日志文件。          maxSize: '20m', // 设置日志文件的最大大小,m 示意 mb 。          maxFiles: '14d', // 保留日志文件的最大天数,此处示意主动删除超过 14 天的日志文件。          // 记录时增加工夫戳信息          format: winston.format.combine(            winston.format.timestamp({                format: 'YYYY-MM-DD HH:mm:ss',            }),            winston.format.json(),          ),        }),      ],    }),  ],  // ...})export class AppModule { // ... }

在全局中间件、过滤器以及拦截器中记录日志

获取申请头信息的工具办法
utils.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,    ip,    method,    query,    body,  };};

在全局中间件中记录日志
logger.middleware.ts

import { Inject, Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response, NextFunction } from 'express';import { WINSTON_MODULE_PROVIDER } from 'nest-winston';import { Logger } from 'winston';import { getReqMainInfo } from './utils';@Injectable()export default class LoggerMiddleware implements NestMiddleware {  // 注入日志服务相干依赖  constructor(    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,  ) {}  use(req: Request, res: Response, next: NextFunction) {    // 获取申请信息    const {      query,      headers: { host },      url,      method,      body,    } = req;        // 记录日志    this.logger.info('route', {          req: getReqMainInfo(req),    });        next();  }}

在全局异样过滤器中记录日志
uinify-exception.filter.ts

import {  ArgumentsHost,  Catch,  ExceptionFilter,  HttpException,  HttpStatus,  Inject,} from '@nestjs/common';import { Response, Request } from 'express';import { WINSTON_MODULE_PROVIDER } from 'nest-winston';import { Logger } from 'winston';import { getReqMainInfo } from './utils';@Catch()export default class UnifyExceptionFilter 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 = 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: 1, msg });  }}

在响应拦截器中记录日志
unify-response.interceptor.ts

import {  CallHandler,  ExecutionContext,  Inject,  Injectable,  NestInterceptor,} 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';@Injectable()export class UnifyResponseInterceptor 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: 0,          data,          msg: '胜利',        };      }),    );  }}

利用全局中间件、过滤器以及拦截器

import {  MiddlewareConsumer,  Module,  NestModule,  RequestMethod,} from '@nestjs/common';import { APP_FILTER } from '@nestjs/core';import { WinstonModule } from 'nest-winston';import * as winston from 'winston';import 'winston-daily-rotate-file';import UnifyExceptionFilter from './common/uinify-exception.filter';import logger from './common/logger.middleware';// ...@Module({  // ...  imports: [    // ...    WinstonModule.forRoot({     // ...    }),  ],  providers: [    // ...    // 利用全局过滤器    {      provide: APP_FILTER,      useClass: UnifyExceptionFilter,    },    // 利用拦截器    {      provide: APP_INTERCEPTOR,      useClass: UnifyResponseInterceptor,    },  ],})export class AppModule implements NestModule {  // 利用全局中间件  configure(consumer: MiddlewareConsumer) {    consumer.apply(logger).forRoutes({ path: '*', method: RequestMethod.ALL });  }}

实现以上配置后,我的项目目录下就会蕴含拜访及错误信息的日志文件。日志文件将每天主动归档压缩,超过 14 天的日志也将被主动删除。

更多参考

winston
winston-daily-rotate-file
nest-winston