前言

日志的记录在后端开发中是不可或缺的,在node中有一个很有名的库winston,这个库支持多种日志传输方式,比如本地文件,控制台,远程传输等。

而nestjs有一个winston的模块库:nest-winston

既然有了模块,那说明什么?

说明我们可以通过依赖注入的形式,在其他地方直接调用模块的服务,从而实现日志记录。

在log记录之前,需要分析我们要记录什么内容,比如最重要的错误日志,当发生错误的时候,我们需要记录日志,方便后续在生产环境查看问题,其次就是请求日志记录,我们记录用户的请求,url、ip地址、路由地址、请求参数等,以及我们的响应数据,也就是发送给前端的数据,针对这三种需求我们要在不同的地方处理。

  1. 错误日志

错误日志我们需要通过错误过滤器来实现(所有的错误都是走的错误过滤器),在过滤器内部调用nest-winston的服务,记录我们需要的内容。

  1. 请求数据

用户的请求数据,我们需要自己实现一个中间件,在中间件里去获取请求的数据,考虑到中间件的生命周期,它其实天然符合我们的要求,它会在进入控制器前触发,我们通过异步的方式记录日志,也不会影响到我们后续的代码运行。

  1. 响应数据

这个就可以在我们之前的文章《Nestjs 格式化接口响应的数据结构》中的格式化拦截器中去做,格式化完毕后将需要的数据记录下来。

安装依赖

bash
复制代码
pnpm i nest-winston winston winston-daily-rotate-file
  1. nest-winston是对winston的封装;
  2. winston日志记录功能本体;
  3. winston-daily-rotate-file将日志文件根据日期、大小限制进行控制,并且可以根据传入的天数限制,删除旧的日志文件;

注册nest-winston模块

app.module.ts注册模块

typescript
复制代码
import { Module } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { WinstonModule } from "nest-winston"; import type { WinstonModuleOptions } from "nest-winston"; import { transports, format } from "winston"; import "winston-daily-rotate-file"; const NODE_ENV = process.env.NODE_ENV; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: NODE_ENV === "development" ? ".env.development" : `.env.${NODE_ENV}` }), WinstonModule.forRootAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => { // 日志输出的管道 const transportsList: WinstonModuleOptions["transports"] = [ new transports.DailyRotateFile({ level: "error", dirname: `logs`, filename: `%DATE%-error.log`, datePattern: "YYYY-MM-DD", maxSize: "20m" }), new transports.DailyRotateFile({ dirname: `logs`, filename: `%DATE%-combined.log`, datePattern: "YYYY-MM-DD", maxSize: "20m", format: format.combine( format((info) => { if (info.level === "error") { return false; // 过滤掉'error'级别的日志 } return info; })() ) }) ]; // 开发环境下,输出到控制台 if (configService.get("NODE_ENV") === "development") { transportsList.push(new transports.Console()); } return { transports: transportsList }; } }) ], controllers: [], providers: [] }) export class AppModule { }

需要注意的是Winston的日志level等级的定义:

typescript
复制代码
const levels = { error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 };

当我们配置level为wran的时候,它会记录小于或等于当前等级的日志信息,这就导致了一个问题,error是最小的,所以在combined.log日志里面,它是会有error的日志写入的,但是我们又单独创建了一个error日志文件专门存放,所以我们得想办法剔除它。

目前我能找到的办法就是通过format方法进行排除。

如果你想了解更多输出管道的options配置,可以查看官方文档:winston

如果你想了解日志文件控制 winston-daily-rotate-file的配置项,可以查看这个文档: winston-daily-rotate-file

错误日志记录

我们找到全局的错误过滤器:http-exception.filter.ts

typescript
复制代码
import { BadRequestException, Catch, ExceptionFilter, HttpException, Inject } from "@nestjs/common"; import type { ArgumentsHost } from "@nestjs/common"; import type { Response, Request } from "express"; import { getReasonPhrase } from "http-status-codes"; import { WINSTON_MODULE_PROVIDER } from "nest-winston"; import { Logger } from "winston"; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception.getStatus(); const results = exception.getResponse() as any; const code = results.statusCode; // 返回的对象 const jsonData = { code: code, message: results.message || getReasonPhrase(code), data: null }; // 参数校验错误,默认都是BadRequestException const isArrayMessage = Array.isArray(results.message); const isValidationError = isArrayMessage && typeof results.message[0] === "string" && results.message[0].includes("⓿"); if (exception instanceof BadRequestException && isValidationError) { const message: Array<{ field: string; message: Array<string> }> = []; results.message.forEach((item) => { const [key, val] = item.split("⓿") as [string, string]; const findData = message.find((item) => item.field === key); if (findData) { findData.message.push(val); } else { message.push({ field: key, message: [val] }); } }); jsonData.message = message; } // 记录日志 const { method, originalUrl, body, query, params, ip } = request; this.logger.error("HttpException", { res: { code, status, message: jsonData.message }, req: { method, url: originalUrl, body, query, params, ip } }); return response.status(status).json(jsonData); } }

在constructor构造函数中我们注入了logger服务,然后调用它的error等级的日志方法,记录数据,一般记录日志的方法,第一个参数为type类型,后面的参数则是具体的内容。

由于我们使用了注入,所以在注册该过滤器的时候,需要在app.module.ts的providers注入,至于为什么?之前的文章已经讲过了,这里不多赘述。

app.module.ts

typescript
复制代码
import { Module } from "@nestjs/common"; import { APP_FILTER } from "@nestjs/core"; import { HttpExceptionFilter } from "@/utils/filters"; @Module({ imports: [], controllers: [], providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter } ] }) export class AppModule { }

请求数据记录

先创建一个中间件

bash
复制代码
nest g mi logger utils/middleware --no-spec
  1. logger是中间件的名称
  2. utils/middleware是中间件存放的位置
typescript
复制代码
import { Inject, Injectable, NestMiddleware } from "@nestjs/common"; import { WINSTON_MODULE_PROVIDER } from "nest-winston"; import { Logger } from "winston"; import { NextFunction, Request, Response } from "express"; @Injectable() export class LoggerMiddleware implements NestMiddleware { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} use(req: Request, res: Response, next: NextFunction) { const { method, originalUrl, body, query, params, ip } = req; // 记录日志 this.logger.info("router", { req: { method, url: originalUrl, body, query, params, ip } }); next(); } }

相对来说还是很简单的,我们从req对象中,也就是请求对象中解构出需要的对象,然后日志记录即可。

然后我们去app.module.ts激活。

app.module.ts

typescript
复制代码
import { Module } from "@nestjs/common"; import type { MiddlewareConsumer } from "@nestjs/common"; import { LoggerMiddleware } from "@/utils/middleware"; @Module({ imports: [], controllers: [], providers: [] }) export class AppModule { // 全局中间件 configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes("*"); } }

中间件的注入是在AppModule类的configure方法中的,其中forRoutes表示中间件需要作用的路由有哪些,你可以是具体的/test,也可以是表示所有的*,甚至可以是一个函数,具体你可以查看官方文档:中间件

响应数据记录

找到我们之前的拦截器:result-format.interceptor.ts

typescript
复制代码
import { hasKeys } from "@/utils/tools"; import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; import type { Response, Request } from "express"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; import { getReasonPhrase } from "http-status-codes"; import { WINSTON_MODULE_PROVIDER } from "nest-winston"; import { Logger } from "winston"; @Injectable() export class ResultFormatInterceptor implements NestInterceptor { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const ctx = context.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const code = response.statusCode; const message = response.statusMessage || getReasonPhrase(code); return next.handle().pipe( map((data) => { let jsonData = { code: code, message: message, data: data ?? null }; // 判断是否已经是格式化的数据 if (data) { const hasFormat = hasKeys(data, ["code", "message", "data"]); if (hasFormat) jsonData = data; } // 记录日志 const { method, originalUrl, body, query, params, ip } = request; this.logger.info("response", { req: { method, url: originalUrl, body, query, params, ip }, res: jsonData }); return jsonData; }) ); } }

其实也没什么复杂的,也是依赖注入,然后调用info记录。

由于使用了依赖注入,我们一样需要在app.module.ts中注册。

app.module.ts

typescript
复制代码
import { Module } from "@nestjs/common"; import { APP_INTERCEPTOR } from "@nestjs/core"; import { ResultFormatInterceptor } from "@/utils/interceptor"; @Module({ imports: [], controllers: [], providers: [ { provide: APP_INTERCEPTOR, useClass: ResultFormatInterceptor }, ] }) export class AppModule { }

结尾

至此我们日志记录的代码都写完了,启动nest,它会在项目目录创建logs目录,然后里面就会有对应的文件:

bash
复制代码
# logs/ 2024-01-13-combined.log 2024-01-13-error.log

我们测试api请求,就可以在对应的日志文件中查看记录的数据。

分类: Nest.js 标签: Nestjswinston日志log

评论

暂无评论数据

暂无评论数据

目录