likes
comments
collection
share

nestjs学习之日志系统

作者站长头像
站长
· 阅读数 11

日志定位什么问题

  • 什么时候发生的
  • 发生了什么
  • 错误是什么

日志分类(等级)

  • log: 通用日志,一般在控制台输出。
  • warn:警告日志,例如调用一些过时方法。
  • error: 错误日志,例如连接数据库异常等等。方便我们定位问题,给出友好提示。
  • debug: 调试日志,例如加载数据日志。方便我们开发。
  • verbose: 详细日志,所有的操作与详细信息。

日志库中都会集成上述日志登记方法供我们调用,来处理日志输出。

日志记录位置

  • 控制台日志。方便监看(开发调试使用)。
  • 文件日志。方便回溯与追踪(24小时滚动)。
  • 数据库日志。敏感操作、敏感数据记录。

了解这些后,那么将带领你熟悉一下nestjs内置Logger,之后学习第三方日志winston, pino,并且在nestjs中进行集成配置。让我们可以有更强大的日志系统。

nestjs日志系统

nestjs内置了日志系统。我们可以通过Logger去实例化对象进行一些不同等级的日志输出。在初始化时可以传入模块名称,让输出的日志更直观。

  private logger = new Logger('用户模块');
  constructor(
    private userService: UserService,
    private config: ConfigService,
  ) {
    this.logger.log('userController init.....');
  }

nestjs学习之日志系统

它是只是将日志输出到控制台进行调试使用的。具体内容可以看这里

第三方日志

pino

官方也记录了结合nestjs使用的文档,直接安装nestjs-pino库就行。

通过依赖注入的方式进行操作。


  import { Logger } from 'nestjs-pino';
  
  constructor(
    private userService: UserService,
    private config: ConfigService,
    private logger: Logger,
  ) {
    this.logger.log('userController init.....');
  }
@Module({
  // 在module中导入的依赖就会被自动实例化。
  imports: [
    TypeOrmModule.forFeature([User, Logs]),
    LoggerModule.forRoot({
      // pinoHttp: {
      //   transport: {
      //     target: 'pino-pretty',
      //     options: {
      //       colorize: true,
      //     },
      //   },
      // },
    }),
  ],
  controllers: [UserController],
  providers: [UserService],
})

他会在每次请求自动输出请求日志,非常详细。但是很不美观,没有格式化。

nestjs学习之日志系统

由于pino打印的日志很丑,我们可以安装pino-pretty来美化打印日志。

import { LoggerModule } from 'nestjs-pino';

@Module({
  // 在module中导入的依赖就会被自动实例化。
  imports: [
    TypeOrmModule.forFeature([User, Logs]),
    LoggerModule.forRoot({
      pinoHttp: {
        transport: {
          target: 'pino-pretty',
          options: {
            colorize: true,
          },
        },
      },
    }),
  ],
  controllers: [UserController],
  providers: [UserService],
})

nestjs学习之日志系统 然后我们还可以使用pino-roll去在生产环境进行日志记录,并保存在指定文件中。

import { LoggerModule } from 'nestjs-pino';
import { join } from 'path';

@Module({
  // 在module中导入的依赖就会被自动实例化。
  imports: [
    TypeOrmModule.forFeature([User, Logs]),
    LoggerModule.forRoot({
      pinoHttp: {
        transport:
          process.env.NODE_ENV === 'development'
            ? {
                target: 'pino-pretty',
                options: {
                  colorize: true,
                },
              }
            : {
                target: 'pino-roll',
                options: {
                  file: join(__dirname, '../../logs/logs.txt'),
                  // 这个表示当前文件保存日志的周期,超过当前周期,保存在下一个文件
                  frequency: 'daily',
                  // 这个表示当前文件保存的大小,如果超过则保存在下一个文件中
                  size: '10M',
                  mkdir: true,
                },
              },
      },
    }),
  ],
  controllers: [UserController],
  providers: [UserService],
})

nestjs学习之日志系统

winston

我们在nestjs中使用需要配合nest-winston库进行使用。 他就是实现了nestjs提供的LoggerService接口。然后在nestjs初始化的时候传递给logger参数即可。

// main.ts
import { createLogger } from 'winston';
import * as winston from 'winston';
import {
  utilities as nestWinstonModuleUtilities,
  WinstonModule,
} from 'nest-winston';
import 'winston-daily-rotate-file';
import { join } from 'path';

async function bootstrap() {
  const loggerInstance = createLogger({
    transports: [
      // 控制台输出格式化
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          nestWinstonModuleUtilities.format.nestLike(),
        ),
      })
    ],
  });

  const app = await NestFactory.create(AppModule, {
    logger: WinstonModule.createLogger({
      instance: loggerInstance,
    }),
  });
  await app.listen(8888);
}

并且需要在module中进行全局导出。

// app.module.ts
@Global()
@Module({
  // 导入模块
  // ConfigModule.forRoot 加载环境变量
  imports: [
    UserModule,
  ],
  // 注册控制器
  controllers: [AppController],
  // 依赖注入,在控制器中自动实例化该服务
  providers: [AppService, Logger],
  // 导出供其他模块使用
  exports: [Logger],
})

通过依赖注入方式使用即可。

  constructor(
    private readonly appService: AppService,
    private logger: Logger,
  ) {
    this.logger.error('出现错误app');
  }

nestjs学习之日志系统

我们也可以像pino一样设置滚动日志,将生产环境的日志记录保存在指定文件中。使用winston-daily-rotate-file进行操作即可。

在实例化的时候传递transport即可。具体的配置详细看这里

const loggerInstance = createLogger({
    transports: [
      // 控制台输出格式化
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          nestWinstonModuleUtilities.format.nestLike(),
        ),
      }),
      // 滚动日子,输出到文件
      new winston.transports.DailyRotateFile({
        // 只记录warn级别之后的日志。warn, error
        level: 'warn',
        dirname: join(__dirname, '../logs'),
        filename: 'application-%DATE%.log',
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true,
        maxSize: '20m',
        // 要保留的最大日志数。
        maxFiles: '14d',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.simple(),
        ),
      }),
      // 通过level来区分不同的日志输出
      new winston.transports.DailyRotateFile({
        // 记录全部类型日志
        level: 'info',
        dirname: join(__dirname, '../logs'),
        filename: 'info-%DATE%.log',
        datePattern: 'YYYY-MM-DD-HH',
        zippedArchive: true,
        maxSize: '20m',
        maxFiles: '14d',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.simple(),
        ),
      }),
    ],
  });

通过不同的日志等级(这里是根据我们调用的不同等级方法进行区分的)输出不同的文件中。 nestjs学习之日志系统 如果想要做系统化的项目,我们需要将逻辑单独抽离出一个module,不应该将代码放在main.ts中,具体操作看这里

大体步骤就是将逻辑代码提取到logs.module.ts

import { ConfigService } from '@nestjs/config';
import { Module } from '@nestjs/common';
import * as winston from 'winston';
import {
  utilities as nestWinstonModuleUtilities,
  WinstonModule,
  WinstonModuleOptions,
} from 'nest-winston';
import 'winston-daily-rotate-file';
import { join } from 'path';
const createDailyRotateTransport = (level: string, filename: string) => {
  return new winston.transports.DailyRotateFile({
    // 如果是warn时,只记录warn级别之后的日志。warn, error
    level,
    dirname: join(__dirname, '../../logs'),
    filename: `${filename}-%DATE%.log`,
    datePattern: 'YYYY-MM-DD-HH',
    zippedArchive: true,
    maxSize: '20m',
    // 要保留的最大日志数。
    maxFiles: '14d',
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.simple(),
    ),
  });
};

@Module({
  imports: [
    WinstonModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        // 控制台输出格式化
        const consoleTransport = new winston.transports.Console({
          format: winston.format.combine(
            winston.format.timestamp(),
            nestWinstonModuleUtilities.format.nestLike(),
          ),
        });
        
        return {
          transports: [
            consoleTransport,
            ...(configService.get('LOG_ON')
              ? [
                  // 使用函数创建transport,防止无论啥条件都创建logs文件
                  createDailyRotateTransport('info', 'info'),
                  createDailyRotateTransport('warn', 'warn'),
                ]
              : []),
          ],
        } as WinstonModuleOptions;
      },
    }),
  ],
})
export class LogsModule {}


main.ts中设置

  import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
  app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));

在模块中使用

constructor(
    private userService: UserService,
    private config: ConfigService,
    // 该模块需要等到logger初始化完成之后在进行初始化。
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
    private readonly logger: LoggerService,
) {
    this.logger.error('user init error....');
}

到这里我们就可以看出来pino和winston的区别了。前者无需我们打印日志,他会很好的处理我们的任何日志。但是后者的日志输出需要我们进行手动操作。

全局异常过滤器

我们可以借助nestjs提供的过滤器功能来完成全局异常的拦截处理。来集成日志打印等功能。

我们需要实现ExceptionFilter接口,实现catch方法,并在其内部获取响应请求对象,进行逻辑处理,并做出响应。我们还需要传入logger对象,将日志存入文件中,便于回溯。

import {
  ExceptionFilter,
  LoggerService,
  HttpException,
  Catch,
  ArgumentsHost,
} from '@nestjs/common';

// 全局处理http异常,统一响应
// 指定只捕获http异常
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {}

  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    // 响应对象
    const response = ctx.getResponse();
    // const request = ctx.getRequest();
    const status = exception.getStatus();
    // 通过winston处理日志写入文件中
    this.logger.error(exception.message, exception.stack);

    // 响应错误请求
    response.status(status).json({
      code: status,
      timestamp: new Date().toISOString(),
      message: exception.message || exception.name,
    });
  }
}

往期年度总结

往期文章

专栏文章