You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS LoggingMiddleware日志重复记录问题排查与解决

NestJS日志中间件重复输出问题分析与解决

场景与代码实现

尝试基于NestMiddleware实现日志中间件,核心代码如下:

LoggingMiddleware 实现

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { AppLogger } from '../app-logger';
import { CommonLoggingHttpFunctions } from '../http/common-logging-http-functions';
import { HeadersInfoTreatment } from '../http/headers-info-treatment';

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
    constructor(private logger: AppLogger) {}

    public use(request: Request, response: Response, next: NextFunction): void {
        this.logHttpRequest(request);
        response.on('close', () => {
            const httpStatusCode = response.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR;

            if (httpStatusCode < StatusCodes.BAD_REQUEST) {
                this.logHttpResponse(response);
            } else {
                this.logHttpErrorResponse(response);
            }
        });

        next();
    }

    private logHttpRequest(request: Request): void {
        const httpVerb = request.method.toUpperCase();
        this.logger.log(`${httpVerb} ${request.url}`, {
            'http-msg': {
                headers: HeadersInfoTreatment.maskSensitiveInfo(request.headers),
                method: httpVerb,
                body: request.body
            },
        });
    }

    private logHttpResponse(response: Response): void {
        const httpVerb = response.req.method?.toUpperCase();
        const statusCode = response.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR;
        const reasonPhrase = CommonLoggingHttpFunctions.isHttpStatusCode(response.statusCode)
            ? getReasonPhrase(response.statusCode)
            : '';
        this.logger.log(`${response.statusCode} ${reasonPhrase} ${httpVerb} ${response.req.url}`, {
            'http-msg': {
                headers: HeadersInfoTreatment.maskSensitiveInfo(response.getHeaders()),
                method: httpVerb,
                body: response,
                statusCode
            },
        });
    }

    private logHttpErrorResponse(response: Response): void {
        const httpVerb = response.req.method?.toUpperCase();
        const statusCode = response.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR;
        const reasonPhrase = CommonLoggingHttpFunctions.isHttpStatusCode(statusCode)
            ? getReasonPhrase(statusCode)
            : '';

        this.logger.error(`${statusCode} ${reasonPhrase} ${httpVerb} ${response.req.url}`, {
            'http-msg': {
                headers: HeadersInfoTreatment.maskSensitiveInfo(response.getHeaders()),
                method: httpVerb,
                body: response,
                statusCode
            },
        });
    }
}

LoggingModule 实现

import { DynamicModule, Global, MiddlewareConsumer, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { CommonLoggingHttpFunctions, configureLogger, LoggingInterceptor } from '..';
import { AppLogger } from './app-logger';
import { CONFIG_OPTIONS, LoggingOptions } from './model';
import { LoggingMiddleware } from './middleware/logging.middleware';

@Global()
@Module({})
export class LoggingModule {
    public static register(options: LoggingOptions): DynamicModule {
        configureLogger(options);
        return {
            module: LoggingModule,
            providers: [
                {
                    provide: CONFIG_OPTIONS,
                    useValue: { logPayloads: true, ...options },
                },
                AppLogger,
                {
                    provide: APP_INTERCEPTOR,
                    useClass: LoggingInterceptor,
                },
            ],
            exports: [AppLogger],
        };
    }

    public configure(consumer: MiddlewareConsumer) {
        consumer.apply(LoggingMiddleware).exclude(`(.*)${CommonLoggingHttpFunctions.excludeEndpoint}(.*)`).forRoutes('*');
    }
}

问题现象

  • 保留LoggingModule中的configure方法时,请求与响应日志均重复输出
  • 移除configure方法后,应用无任何日志输出(已确认AppModule无重复导入)

问题原因

核心冲突在于**LoggingInterceptor和LoggingMiddleware同时在处理日志记录**:

  1. LoggingModule的register方法中注册了全局的APP_INTERCEPTOR(即LoggingInterceptor),它会拦截所有请求和响应并生成日志
  2. 同时configure方法又全局应用了LoggingMiddleware,同样会处理请求和响应日志
  3. 两者功能重叠,导致每一次请求/响应都会被记录两次
  4. 移除configure方法后,LoggingMiddleware未被注册,若LoggingInterceptor未正确实现日志逻辑(或日志配置未生效),就会出现无日志输出的情况

解决办法

方案1:仅使用LoggingMiddleware(移除Interceptor)

修改LoggingModule的register方法,去掉全局Interceptor的注册,保留configure方法让Middleware生效:

public static register(options: LoggingOptions): DynamicModule {
    configureLogger(options);
    return {
        module: LoggingModule,
        providers: [
            {
                provide: CONFIG_OPTIONS,
                useValue: { logPayloads: true, ...options },
            },
            AppLogger,
            // 移除全局Interceptor注册
            // {
            //     provide: APP_INTERCEPTOR,
            //     useClass: LoggingInterceptor,
            // },
        ],
        exports: [AppLogger],
    };
}

这样只有LoggingMiddleware负责日志记录,不会出现重复输出。

方案2:仅使用LoggingInterceptor(移除Middleware)

如果倾向于用Interceptor处理日志,移除LoggingModule中的configure方法,同时确保LoggingInterceptor完整实现了请求和响应的日志逻辑:

  • 在LoggingInterceptor的intercept方法中,记录请求信息
  • 通过RxJS的tap操作符监听响应流,记录响应/错误日志
  • 检查configureLogger方法是否正确初始化日志配置,确保AppLogger能正常输出内容

额外检查点

  • 确认LoggingInterceptor的逻辑是否覆盖了请求和响应的日志输出,没有遗漏
  • 验证AppLogger的实例是否正确注入,日志级别等配置是否生效

内容的提问来源于stack exchange,提问作者Nikasha Von carstein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 21:07:35