面向切面的日志处理#
在某些特殊业务场景中,开发人员可能会选择手动记录日志,但是在处理请求、响应和捕获服务异常等等常见场景时,手动记录日志可能效率较低。为了减少冗余的日志代码并且统一日志格式,通常会采用全局日志记录的策略,比如通过AOP的方式来实现。
中间件日志统计#
在Nest中,中间件可以在路由处理程序之前或者之后执行函数。它们可以操作请求和响应对象,或者执行其他运行时确定的任务。一般情况下,中间件可以用于收集请求参数、请求体、请求方法、IP地址等等信息,这些信息对于后续的问题排查也是比较重要的。
我们可以实现一个日志记录的中间件,为了统一,创建一个common的文件夹,我们不同的拦截操作文件都放在这里面,在这个文件夹下创建中间件,直接使用命令:
nest g mi logger --flat --no-specimport { Inject, Injectable, NestMiddleware } from '@nestjs/common'import { NextFunction, Request, Response } from 'express'import { MyLogger } from 'src/logger/MyLogger'
@Injectable()export class LoggerMiddleware implements NestMiddleware { @Inject(MyLogger) private logger: MyLogger
use(req: Request, res: Response, next: NextFunction) { const statusCode = res.statusCode const logFormat = ` ############################################################ Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip} Status code: ${statusCode} Params: ${JSON.stringify(req.params)} Query: ${JSON.stringify(req.query)} Body: ${JSON.stringify(req.body)} ############################################################ ` next()
if (statusCode >= 500) { this.logger.error(logFormat, 'Request LoggerMiddleware') } else if (statusCode >= 400) { this.logger.warn(logFormat, 'Request LoggerMiddleware') } else { this.logger.log(logFormat, 'Request LoggerMiddleware') } }}为了中间件应用于所有路由上,可以在AppModule上进行处理:
@Module({ imports: [LoggerModule], controllers: [AppController], providers: [AppService],})export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes('*') }}访问127.0.0.1:3000/打印如下结果:

拦截器日志统计#
同样,我们可以使用拦截器实现HTTP响应成功的日志功能。
同样在common文件夹下,使用命令直接创建拦截器
nest g itc response --flat --no-specimport { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor,} from '@nestjs/common'import { map, Observable } from 'rxjs'import { MyLogger } from 'src/logger/MyLogger'
@Injectable()export class ResponseInterceptor implements NestInterceptor { @Inject(MyLogger) private logger: MyLogger
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest() return next.handle().pipe( map((data) => { const logFormat = ` ############################################################ Request original url: ${req.originalUrl} Method: ${req.method} IP: ${req.ip} Response data: ${JSON.stringify(data)} ############################################################ ` this.logger.log(logFormat, 'Response LoggerInterceptor') return data }) ) }}在AppModule中注册拦截器
import { ResponseInterceptor } from './common/response.interceptor';
@Module({ imports: [LoggerModule], controllers: [AppController], providers: [ AppService, { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor, }, ],})export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes('*'); }}
过滤器日志统计#
过滤器也常常用来进行统计日志,比如Http的异常信息收集,同样,我们在common文件夹下创建filter过滤器
nest g f http-exception --flat --no-specimport { ArgumentsHost, Catch, ExceptionFilter, Inject } from '@nestjs/common'import { MyLogger } from 'src/logger/MyLogger'
@Catch()export class HttpExceptionFilter implements ExceptionFilter { @Inject(MyLogger) private logger: MyLogger catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp() const response = ctx.getResponse() const request = ctx.getRequest() const status = exception.getStatus() const exceptionResponse = exception.getResponse() const logFormat = ` ############################################################ Request original url: ${request.originalUrl} Method: ${request.method} IP: ${request.ip} Status code: ${status} Response: ${ exception.toString() + `(${exceptionResponse?.message || exceptionResponse})` } ############################################################ ` this.logger.error(logFormat, 'HttpExceptionFilter') response.status(status).json({ code: status, timestamp: new Date().toLocaleString(), error: exceptionResponse?.message || exception.message, msg: `${status >= 500 ? 'Service Error' : 'Client Error'}`, }) }}同样,在AppModule上应用
@Module({ imports: [LoggerModule], controllers: [AppController], providers: [ AppService, // 应用拦截器 { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor, }, // 应用过滤器 { provide: APP_FILTER, useClass: HttpExceptionFilter, }, ],})export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(LoggerMiddleware).forRoutes('*') }}稍微修改一下Controller
@Get('hello')getHello2(@Query() name: string): string { console.log('name:', name); throw new HttpException( 'getHello2()请求异常', HttpStatus.EXPECTATION_FAILED, ); return 'hello world2';}
后台打印:
