likes
comments
collection
share

NestJS博客实战04-导入Log日志模块

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

接着上一章,这章主要说明怎么写自定义Log日志模块。官网Logger说明

Nest有自己的内置Logger,可以用来显示程序中捕获的异常。这个Logger方法是有由@nestjs/common这个包提供的.您可以完全控制包含以下内容的日志系统的行为:

  • 完全禁用日志记录
  • 指定日志的详细级别(例如,显示errors、warnings、debug信息等)
  • 覆盖默认Logger中的时间戳(例如,使用ISO8601标准作为日期格式)
  • 完全覆盖默认Logger
  • 通过扩展来自定义默认Logger
  • 利用依赖项注入来简化应用程序的组成和测试

您能够利用内置的Logger创建自定义的实现,来为程序打印程序级别的时间和消息。

对于更高级的日志记录功能,您可以使用任何Node.js日志记录包,如Winston,来实现一个完全自定义的生产级日志记录系统。

基本的自定义

为了禁用日志,首先要设置loggerfalse,并且在NestFactory.create()的时候传入这个可选参数。

const app = await NestFactory.create(AppModule, {
  logger: false,
});
await app.listen(3000);

想选择特定的日志级别,可以为logger设置字符串数组

const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});
await app.listen(3000);

可以选择的级别有:'log''error''warn''debug', 和 'verbose'.

要禁用默认记录器消息中的颜色,请将NO_COLOR环境变量设置为一些非空字符串。

自定义实现

通过将logger属性的值设置为满足LoggerService接口的对象,您可以提供Nest用于系统Logger的自定义记录器实现。例如,您可以告诉Nest使用内置的全局JavaScript控制台对象(实现LoggerService接口),如下所示:

const app = await NestFactory.create(AppModule, {
  logger: console,
});
await app.listen(3000);

想实现自己的Logger,只要实现LoggerService这个接口就行了

import { LoggerService } from '@nestjs/common';

export class MyLogger implements LoggerService {
  /**
   * 写log'等级日志.
   */
  log(message: any, ...optionalParams: any[]) {}

  /**
   * 写'error'等级日志.
   */
  error(message: any, ...optionalParams: any[]) {}

  /**
   * 写'warn'等级日志.
   */
  warn(message: any, ...optionalParams: any[]) {}

  /**
   * 写'debug'等级日志.
   */
  debug?(message: any, ...optionalParams: any[]) {}

  /**
   * 写'verbose'等级日志.
   */
  verbose?(message: any, ...optionalParams: any[]) {}
}

然后您可以传入自己定义的MyLogger

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});
await app.listen(3000);

这种技术虽然简单,但没有为MyLogger类使用依赖项注入。这可能会带来一些挑战,尤其是对测试而言,并限制MyLogger的可重用性。有关更好的解决方案,请参阅下面的依赖注入部分。

继承内置的Logger

您可以通过扩展内置的ConsoleLogger类并覆盖默认实现的选定行为来满足您的需求,而不是从头开始编写Logger。

import { ConsoleLogger } from '@nestjs/common';

export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    // 在这里写逻辑
    super.error(...arguments);
  }
}

参照下面Logger为程序记录日志,您可以为自己的模块添加继承内置的Logger

您可以告诉Nest使用扩展Logger进行系统日志记录,方法是通过应用程序选项对象的Logger属性传递它的实例(如上面的自定义实现部分所示),或者使用下面的依赖注入部分所示的技术。如果这样做,您应该注意调用super,如上面的示例代码所示,将特定的日志方法调用委托给父类(内置),这样Nest就可以依赖于它所期望的内置功能。

依赖注入

对于更高级的日志记录功能,您需要利用依赖项注入。例如,您可能想注入上一章讲的ConfigService。为了能够注入自定义Logger,需要创建一个类并实现LoggerService然后再使用的模块里面注册provider

举例:

  1. 定义MyLogger类,它可以是继承内置的ConsoleLogger或者完全的重写它。
  2. 创建LoggerModule,提供MyLogger
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}

有了这个构造,您现在可以提供自定义Logger供任何其他模块使用。因为MyLogger类是模块的一部分,所以它可以使用依赖项注入(例如,注入ConfigService)。还有一种技术需要提供这个自定义记录器,供Nest用于系统日志记录(例如,用于引导和错误处理)。

因为应用程序实例化(NestFactory.create())发生在任何模块的上下文之外,所以它不参与初始化的正常依赖注入阶段。因此,我们必须确保至少有一个应用程序模块导入LoggerModule,以触发Nest实例化MyLogger类的单个实例。

然后,我们可以指示Nest使用以下构造的MyLogger的相同单例实例:

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(app.get(MyLogger));
await app.listen(3000);

在这里,我们在NestApplication实例上使用get()方法来检索MyLogger对象的singleton实例。这种技术本质上是一种“注入”记录器实例供Nest使用的方法。app.get()调用检索MyLogger的singleton实例,并取决于该实例首先被注入到另一个模块中,如上所述。

用Logger为程序记录日志

我们可以将上面的几种技术结合起来,在Nest系统日志和我们自己的应用程序事件/消息日志中提供一致的行为和格式。

一个好的做法是在我们的每个服务中从@nestjs/common实例化Logger类。我们可以在Logger构造函数中提供服务名称作为context上下文参数,如下所示:

import { Logger, Injectable } from '@nestjs/common';

@Injectable()
class MyService {
  private readonly logger = new Logger(MyService.name);

  doSomething() {
    this.logger.log('Doing something...');
  }
}

在默认的Logger实现中,上下文打印在方括号中,如下面示例中的NestFactory:

[Nest] 19096   - 12/08/2019, 7:12:59 AM   [NestFactory] Starting Nest application...

如果我们通过app.useLogger()提供自定义Logger,Nest实际上会在内部使用它。这意味着我们的代码仍然与实现无关,而我们可以通过调用app.useLogger()来轻松地用默认Logger代替自定义Logger。

这样,如果我们按照上一节中的步骤调用app.useLogger(app.get(MyLogger)),那么下面从MyService调用this.logger.log()将导致从MyLogger实例调用方法log

这应该适用于大多数情况。

注入自定义 logger

首先,使用如下代码扩展内置Logger。我们提供scope选项作为ConsoleLogger类的配置元数据,指定一个临时作用域,以确保在每个功能模块中都有一个唯一的MyLogger实例。在本例中,我们不扩展单独的ConsoleLogger方法(如log()warn()等),尽管您可以选择这样做。

import { Injectable, Scope, ConsoleLogger } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class MyLogger extends ConsoleLogger {
  customLog() {
    this.log('Please feed the cat!');
  }
}

接下来,创建LoggerModule

import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}

接下来,将LoggerModule导入到您的功能模块中。由于我们扩展了默认Logger,因此可以方便地使用setContext方法。因此,我们可以开始使用上下文感知的自定义记录器,如下所示:

import { Injectable } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  constructor(private myLogger: MyLogger) {
    //由于临时作用域,CatsService有自己唯一的MyLogger实例,因此在此处设置上下文不会影响其他服务中的其他实例
    this.myLogger.setContext('CatsService');
  }

  findAll(): Cat[] {
    // 您可以调用所有默认方法
    this.myLogger.warn('About to return cats!');
    // 以及您的自定义方法
    this.myLogger.customLog();
    return this.cats;
  }
}

最后,指示Nest在main.ts文件中使用自定义Logger的实例,如下所示。当然,在这个例子中,我们实际上还没有自定义Logger行为(通过扩展logger方法,如log()warn()等),所以实际上不需要这个步骤。但是,如果您将自定义逻辑添加到这些方法中,并希望Nest使用相同的实现,那么就需要这样做。

const app = await NestFactory.create(AppModule, {
  bufferLogs: true,
});
app.useLogger(new MyLogger());
await app.listen(3000);

使用扩展Logger

内置的Logger只能在命令行显示。这在开发环境中是非常好用,但是在生产环境中,我们可能希望有更强大功能。比如把日志信息写入文件。winston可以满足这些需求。

1. 导入winston所需要的依赖包

pnpm i winston nest-winston winston-daily-rotate-file --save

2. 新建一个Logger模块

nest g resource common/logs-config --no-spec
# 选择 REST API
#  CRUD 选择不要 n
# 最后把生成出来的logs-config.controller.ts文件删除

3. 写module模块

import { Module } from '@nestjs/common';
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
import { ConfigService } from '@nestjs/config';
import * as winston from 'winston';
import { Console } from 'winston/lib/winston/transports';
import * as DailyRotateFile from 'winston-daily-rotate-file';
import { LogsConfigService } from './logs-config.service';
import { ConfigEnum } from '../enum/config.enum';

function createDailyRotateTrasnport(level: string, filename: string) {
  return new DailyRotateFile({
    level,
    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 config = configService.get(ConfigEnum.LOG_CONFIG);
        const timestamp = config.TIMESTAMP;
        const conbine = [];
        if (timestamp) {
          conbine.push(winston.format.timestamp());
        }
        conbine.push(utilities.format.nestLike());
        const consoleTransports = new Console({
          level: config.LOG_LEVEL || 'info',
          format: winston.format.combine(...conbine),
        });

        return {
          transports: [
            consoleTransports,
            ...(config.LOG_ON
              ? [
                  createDailyRotateTrasnport('info', 'application'),
                  createDailyRotateTrasnport('warn', 'error'),
                ]
              : []),
          ],
        } as WinstonModuleOptions;
      },
    }),
  ],
  providers: [LogsConfigService],
})
export class LogsConfigModule {}

4. 运行确认

配置文件.dev.yaml如下

LOG_CONFIG:
  TIMESTAMP: true
  LOG_LEVEL: 'info'
  LOG_ON: true

启动程序:

npm run start:dev

访问接口,确认是否生成logs文件

NestJS博客实战04-导入Log日志模块

本章代码

代码