使用Winston替換NestJS專案中Nest內建的logger以及結合全域性異常過濾器

小二上酒~發表於2024-07-03

  • winston是一個高度整合的日誌模組
  • 透過參照npm nest-winston文件(Replacing the Nest logger (also for bootstrapping)) 可以和nest專案高度整合,安裝依賴:

   npm install --save nest-winston winston

  • main.ts中建立並配置logger
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { createLogger } from 'winston';
import { WinstonModule } from 'nest-winston';
async function bootstrap() {
  // 1.建立winston例項
  const logger = createLogger({
    //  一些配置項
  });

  const app = await NestFactory.create(AppModule, {
    // logger: ['error', 'warn'],
    // 2.配置nest logger為winston
    logger: WinstonModule.createLogger(logger),
  });
  await app.listen(3000);
}
bootstrap();
  • 隨後按照官方事例 在app.module.ts中全域性提供全域性提供已被替換為Winston的logger
import { Logger, Module } from '@nestjs/common'

@Module({
  providers: [Logger] 
})

export class AppModule {}
  • 在user.controller.ts中注入並使用則會報錯:無法解析logger
  • 而事實上根據上述官方提供的案例 僅僅是在對應模組module中提供,以及在對應的controller中注入使用,模組和模組之間想要相互引用則需要exports出來(其他模組進行import即可,或將這個模組註冊為全域性模組);其他模組才能正常使用。
  • 將app.module註冊為全域性模組
import {} from '@nest/common'
// app.module
@Global()
@Module({
  imports: [
//
  ],
  controllers: [AppController],
  providers: [AppService, Logger],
  exports: [Logger], //仍然需要
})
export class AppModule {}
  • 全域性註冊後在其餘模組controller中使用也無需裝飾器注入
import { Logger } from '@nestjs/common'
@Controller('user')
export class UserController {
  constructor(
    private userService: UserService,
    private readonly logger: Logger,
  ) {}
  • 安裝winston-daily-rotate-file ,這是一個與Winston整合的模組,能自動每天或按需輪換日誌檔案。
npm i winston-daily-rotate-file 
  • 匯入並補全winston其餘配置
//main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as winston from 'winston';
import { WinstonModule, utilities } from 'nest-winston';
import 'winston-daily-rotate-file';
// import { format } from 'path';
async function bootstrap() {
  // 1.建立winston例項
  const logger = winston.createLogger({
    //  一些配置項

    transports: [
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          utilities.format.nestLike(),
        ),
      }),
      new winston.transports.DailyRotateFile({
        // 日誌檔案資料夾,建議使用path.join()方式來處理,或者process.cwd()來設定,此處僅作示範
        dirname: 'src/logs',
        // 日誌檔名 %DATE% 會自動設定為當前日期
        filename: 'info-%DATE%.info.log',
        // 日期格式
        datePattern: 'YYYY-MM-DD',
        // 壓縮文件,用於定義是否對存檔的日誌檔案進行 gzip 壓縮 預設值 false
        zippedArchive: true,
        // 檔案最大大小,可以是bytes、kb、mb、gb
        maxSize: '20m',
        // 最大檔案數,可以是檔案數也可以是天數,天數加單位"d",
        maxFiles: '7d',
        // 格式定義,同winston
        format: winston.format.combine(
          winston.format.timestamp({
            format: 'YYYY-MM-DD HH:mm:ss',
          }),
          winston.format.json(),
          winston.format.simple(),
        ),

        // 日誌等級,不設定所有日誌將在同一個檔案
        level: 'info',
      }),
      // 同上述方法,區分error日誌和info日誌,儲存在不同檔案,方便問題排查
      new winston.transports.DailyRotateFile({
        dirname: 'src/logs',
        filename: 'error-%DATE%.error.log',
        datePattern: 'YYYY-MM-DD',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '14d',
        format: winston.format.combine(
          winston.format.timestamp({
            format: 'YYYY-MM-DD HH:mm:ss',
          }),
          winston.format.json(),
          winston.format.simple(),
        ),
        level: 'warn',
      }),
    ],
  });

  const app = await NestFactory.create(AppModule, {
    // logger: ['error', 'warn'],
    // 2.配置nestjs logger為winston
    logger: WinstonModule.createLogger(logger),
  });
  await app.listen(3000);
}
bootstrap();
  • winston可配置功能多但是缺點則是 需要在需要的地方手動呼叫以加入日誌
//user.controller.ts
@Get()
  getUser(): any {
    this.logger.log('getUser success');
    return this.userService.findAll();
  }
  • 可以在配合全域性過濾器來使用方便記錄
//all-exceptions.filter.tss
import {
  Catch,
  ExceptionFilter,
  LoggerService,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import * as requestIp from 'request-ip';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  // ...
  constructor(
    private readonly logger: LoggerService,
    private readonly httpAdapterHost: HttpAdapterHost,
  ) {}
  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const httpStatus =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const { httpAdapter } = this.httpAdapterHost;
    const responseBody = {
      headers: request.headers,
      query: request.query,
      body: request.body,
      params: request.params,
      path: httpAdapter.getRequestUrl(request),
      timestamp: new Date().toISOString(),
      //   statusCode: httpStatus,
      ip: requestIp.getClientIp(request),
      exception: exception['name'],
      error: exception['response'] || 'Internal Server Error',
    };
    this.logger.error('[toimc]', responseBody); //加了一個錯誤的日誌
    httpAdapter.reply(response, responseBody, httpStatus);
  }
}
  • 按照官方文件的過濾器使用時會報錯,根據提示改成如下則可以成功在收到錯誤請求時透過過濾器報錯
//main.ts
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
//....
async function bootstrap() {
  //....
  const app = await NestFactory.create(AppModule, {
    // logger: ['error', 'warn'],
    // 2.配置nestjs logger為winston
    logger: logger,
  });
 // const { httpAdapter } = app.get(HttpAdapterHost);官方提供的寫法會報型別錯誤
  const httpAdapter = app.get(HttpAdapterHost);

  app.useGlobalFilters(new AllExceptionsFilter(logger, httpAdapter)); //全域性過濾器只允許提供一個
  await app.listen(3000);
}
bootstrap();
  • 傳送一個路徑錯誤的請求,可以看到目標目錄下產生錯誤日誌表示日誌模組替換並且成功使用!

  • 以上分散步驟有不少根據教程和官方文件的案例直接配置,實際大量引數寫在main.ts顯然不合適,我們將以上邏輯挪到建立好的logs模組中:
  • 值得一提的是之前參照的是nest-winston中 (Replacing the Nest logger (also for bootstrapping))是直接在main.ts配置的過程
  • 重新用自己logs模組替換nest內建logger模組則參考其中標題為(Replacing the Nest logger)的部分,現在程式碼如下:
// logs.module.ts
import { Module } from '@nestjs/common';
import { WinstonModule, WinstonModuleOptions } from 'nest-winston';
import { ConfigService } from '@nestjs/config';
import * as winston from 'winston';
import { Console } from 'winston/lib/winston/transports';
import { utilities } from 'nest-winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
import { LogEnum } from 'src/enum/config.enum';
import { LogsController } from './logs.controller';
import { LogsService } from './logs.service';

import { join } from 'path';

function createDailyRotateTrasnport(level: string, filename: string) {
  return new DailyRotateFile({
    level,
    dirname: join(process.cwd(), 'logs'),
    filename: `${filename}-%DATE%.log`,
    datePattern: 'YYYY-MM-DD-HH',
    zippedArchive: true,
    maxSize: '20m',
    maxFiles: '7d',
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.simple(),
    ),
  });
}
@Module({
  imports: [
    WinstonModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        const timestamp = configService.get(LogEnum.TIMESTAMP) === 'true';
        const conbine = [];
        if (timestamp) {
          conbine.push(winston.format.timestamp());
        }
        conbine.push(utilities.format.nestLike());
        const consoleTransports = new Console({
          level: configService.get(LogEnum.LOG_LEVEL) || 'info',
          format: winston.format.combine(...conbine),
        });

        return {
          transports: [
            consoleTransports,
            ...(configService.get(LogEnum.LOG_ON)
              ? [
                  createDailyRotateTrasnport('info', 'application'),
                  createDailyRotateTrasnport('warn', 'error'),
                ]
              : []),
          ],
        } as WinstonModuleOptions;
      },
    }),
  ],
  controllers: [LogsController],
  providers: [LogsService],
})
export class LogsModule {}
//main.ts
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
import { AppModule } from './app.module';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
// import 'winston-daily-rotate-file';
import { AllExceptionsFilter } from './filters/all-exception.filter';
// import { format } from 'path';
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {});
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

  const httpAdapter = app.get(HttpAdapterHost);

  app.useGlobalFilters(
    new AllExceptionsFilter(app.get(WINSTON_MODULE_NEST_PROVIDER), httpAdapter),
  ); //全域性過濾器只允許提供一個
  await app.listen(3000);
}
bootstrap();

相關文章