- 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();