作为开发人员,在开发环境中调试和定位问题相对容易。然而,在生成环境中,通常无法方便德附加调试器来追踪BUG。因此,日志记录的作用就显得非常重要了。
因此,建立一个全面有效的日志记录系统,对于任何应用程序的生产运行都是必不可少的。它不仅帮助我们监控应用程序的状态,还能在出现问题时提供关键的信息,帮助我们快速响应并恢复服务。
内置日志器Logger#
Nest中其实默认开启了日志记录(Logger),当我们运行pnpm run start:dev命令启动Nest应用的时候,都能看到启动过程中打印的信息,这其实就是Logger帮我们打印的。
我们先创建一个项目,直接启动就能看到相关的信息
nest n nest-logger -g -p pnpm
从上图可以看到,日志信息包括时间戳、日志级别和上下文等等重要内容,根据这些信息,可以确认程序运行的状态。
默认的格式如下:
[AppName] [PID] [TimeStamp] [LogLevel] [Context] Message [+ms]其中AppName为应用程序名称,一般固定为Nest;PID为系统分配的进程编号;TimeStamp为输出的当前系统时间。
Logger分为多种级别,包括log、error、warn、debug、verbose、fatal。我们可以指定任意组合启动记录,例如:只在出现错误或者警告的时候打印日志。
async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: ['error', 'warn'], }) await app.listen(process.env.PORT ?? 3000)}bootstrap()Context上下文表示当前日志产生的阶段,比如应用程序启动阶段或者路由程序执行阶段。
在一些情况下,我们可以使用Logger手动记录自定义事件或者消息,其实,就是将我们之前的console.log的打印,换成Logger对象就行,这样打印的信息更加全面,比如在AppService中开启Logger
@Injectable()export class AppService { private logger = new Logger(AppService.name)
getHello(): string { this.logger.error('getHello error!') return 'Hello World!' }}当Controller调用getHello的时候,就会帮我打印这个error信息

当然,可以很明显的看出,在Service上直接实例化了Logger对象,这当然不是太好的方式,更好的方式我们当然应该进行依赖注入。
我们可以自己创建一个Logger模块
nest g mo logger --no-spec然后,在该模块下创建自己的logger类MyLogger,这个类需要至少继承Nest给我们提供父类ConsoleLogger或者实现更加底层的接口LoggerService
import { ConsoleLogger, Injectable } from '@nestjs/common'
@Injectable()export class MyLogger extends ConsoleLogger { error(message: any, trace?: string, context?: string) { message = message + ' - 当前环境: dev' super.error(message, trace, context) }
log(message: any, context?: string) { message = message + ' - 当前环境: dev' super.log(message, context) }}MyLogger类通过@Injectable()装饰之后,我们就可以实现注入,当然,现在由于我们是一个单独的模块,所以在logger.module.ts文件中,我们需要声明providers,而且我们肯定在外部需要用到MyLogger,所以记得需要exports
import { Module } from '@nestjs/common'import { MyLogger } from './MyLogger'
@Module({ providers: [MyLogger], exports: [MyLogger],})export class LoggerModule {}此时,在main.ts中,通过useLogger方法将应用程序的默认日志器替换为我们自己定义的日志器实例。
async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true, }) app.useLogger(app.get(MyLogger)) await app.listen(process.env.PORT ?? 3000)}bootstrap()bufferLogs表示先不打印日志,把它放到buffer缓冲区,直到用useLogger指定了Logger并且应用初始化完毕
由于之前通过命令创建的logger模块,所以自动的引入到了AppModule中,现在,我们就可以直接在AppService中进行注入了
@Injectable()export class AppService { @Inject(MyLogger) private logger: MyLogger
getHello(): string { this.logger.error('getHello error!') return 'Hello World!' }}记录日志的正确姿势#
日志记录需要注意下面几点:
1、日志行为不应该出现异常。当我们使用日志记录系统行为时,首先需要保证日志行为不能出现异常,比如最常见的空指针异常等等,比如你像下面这样去写:
this.logger.log(`id=${getUser().id}`)这句代码中,很有可能getUser()返回的结果为null,这就会导致获取id操作抛出异常,我们应该避免出现这种情况。
2、日志行为不应该产生副作用。日志应该是无状态的,日志行为不应该对业务逻辑差生任何副作用,否则系统行为可能会产生意外的结果,比如下面的伪代码:
this.logger.log(`用户保存成功:${userRepository.save(user)}`)应该避免在日志记录中执行异步操作,以及会对系统结果差生影响的操作。
3、日志信息不应该包含敏感信息。日志只用于记录程序的执行状态,例如调用了哪些函数,出现了什么错误等等,不应该打印具体的信息,特别是敏感的信息,比如用户的账户信息等等。
4、日志记录应该尽可能详细。日志是面向开发人员的,当描述错误时,应该体积具体操作及失败原因,最好提示接下来应该怎样去做,比如:
this.logger.error(`用户${id}调用getUserInfo()方法失败,进行重试中...`, error)getUserInfo(id)第三方日志器Winston#
Nest内置的日志器可以在开发过程中记录系统行为,而在生成环境中通常使用专用的日志统计模块,比如**Winston**。
它能够满足特定的日志记录要求,包括高级过滤、格式化,和几种日志记录。
我们之前的代码结构几乎不需要做变动,只是在实现上和之前不同。当然,这之前我们还需要引入相应的包依赖:
pnpm add winston winston-daily-rotate-file dayjs -Spnpm add chalk@4 -S其中,
winston当然就是需要的日志框架,
winston-daily-rotate-file可以根据日期和大小限制进行日志文件的轮转,旧日志可以根据计数和已用天数进行删除。
dayjs用于格式化日期
chalk为控制台文本着色,**注意:**使用4的版本

MyLogger.ts
import { Injectable, LoggerService } from '@nestjs/common'import { createLogger, transports, Logger as WinstonLogger } from 'winston'
@Injectable()export class MyLogger implements LoggerService { private logger: WinstonLogger
constructor() { this.logger = createLogger({ level: 'debug', transports: [new transports.Console()], }) } log(message: string, context: string) { this.logger.log('info', message, { context }) } info(message: string, context: string) { this.logger.info(message, { context }) } error(message: string, context: string) { this.logger.error(message, { context }) } warn(message: string, context: string) { this.logger.warn(message, { context }) } debug(message: string, context: string) { this.logger.debug(message, { context }) }}winston支持日志级别和npm的日志级别一致,优先级从 0 到 6(从高到低):
const levels = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6,}我们上面代码指定为debug,意味着小于数字5的日志级别都会被统计并记录
然后我们还需要在main.ts中指定使用Winston日志器,并关闭内置日志器
async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: false, }) // 使用自定义Logger app.useLogger(app.get(MyLogger)) await app.listen(process.env.PORT ?? 3000)}bootstrap()现在我们在AppService中使用:
@Injectable()export class AppService { @Inject(MyLogger) private logger: MyLogger
getHello(): string { this.logger.info('getHello info!', AppService.name) this.logger.warn('getHello warn!', AppService.name) this.logger.error('getHello error!', AppService.name) return 'Hello World!' }}在运行pnpm run start:dev命令之后,就可以在控制台看到输出以下信息:

当访问getHello路由,打印如下:

当然了,现在日志还很难看,也差了一些东西,我们可以自己进行处理:
- 加上时间戳,并格式化日志输出
- 日志记录加上颜色美化
- 根据日志级别进行分类统计,特别是区分错误日志,并按日实现滚动日志
- 能够定期清理日志文件
修改一下MyLogger.ts文件
import { Injectable, LoggerService } from '@nestjs/common'import 'winston-daily-rotate-file'import * as chalk from 'chalk'import * as dayjs from 'dayjs'import { createLogger, format, transports, Logger as WinstonLogger,} from 'winston'
@Injectable()export class MyLogger implements LoggerService { private logger: WinstonLogger
constructor() { this.logger = createLogger({ level: 'debug', transports: [ new transports.Console({ format: format.combine( // 颜色 format.colorize(), // 日志格式 format.printf(({ context, level, message, timestamp }) => { const appStr = chalk.blue('[Nest] ') const contextStr = chalk.yellow(`[${context}]`)
return `${appStr} ${timestamp} ${level} ${contextStr}: ${message}` }) ), }), // 保存到文件 new transports.DailyRotateFile({ // 日志文件夹 dirname: process.cwd() + '/src/logs', // 日志文件名 %DATE% 会自动替换为当前日期 filename: 'app-%DATE%.info.log', // 日期格式 datePattern: 'YYYY-MM-DD', // 压缩文档 zippedArchive: true, // 文件最大大小,可以是Bytes、KB、MB、GB maxSize: '20M', // 最大文件数 ‘7d’ 表示7天 maxFiles: '7d', // 日志格式 format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.json() ), // 日志等级,如果不设置,所有日志将会记录在同一文件中 level: 'info', }), new transports.DailyRotateFile({ dirname: process.cwd() + '/src/logs', filename: 'app-%DATE%.error.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20M', maxFiles: '14d', format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.json() ), level: 'error', }), ], }) } log(message: string, context: string) { const timestamp = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss') this.logger.log('info', message, { context, timestamp }) } info(message: string, context: string) { const timestamp = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss') this.logger.info(message, { context, timestamp }) } error(message: string, context: string) { const timestamp = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss') this.logger.error(message, { context, timestamp }) } warn(message: string, context: string) { const timestamp = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss') this.logger.warn(message, { context, timestamp }) } debug(message: string, context: string) { const timestamp = dayjs(Date.now()).format('YYYY-MM-DD HH:mm:ss') this.logger.debug(message, { context, timestamp }) }}现在启动程序,就能有比较好看的颜色和格式了:

同时,当我们访问路由之后:

至此,我们已经完成日志本地持久化的功能。不过在实际业务中,我们也可以将日志文件上传到远程服务器,或者通过制定transports.Http的方式将日志持久化到数据库中,以便进行后续的业务数据分析、数据过滤和数据清洗等操作。