使用Winston替换NestJS项目中Nest内置的logger以及结合全局异常过滤器
- 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();