NestJS中间件获取请求/响应体

7

我正在使用nestjs开发一个项目,希望尽可能记录更多的信息,其中之一是每个http请求的响应和请求体。为此,我创建了一个nestjs中间件:

import {token} from 'gen-uid';
import { inspect } from 'util';
import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';
import { Stream } from 'stream';
import { createWriteStream, existsSync, mkdirSync } from 'fs';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    logfileStream: Stream;

    constructor() {
        if (!existsSync('./logs')) mkdirSync('./logs');
        this.logfileStream = createWriteStream("./logs/serviceName-"+ new Date().toISOString() + ".log", {flags:'a'});
    }

resolve(...args: any[]): MiddlewareFunction {
    return (req, res, next) => {
        let reqToken = token();
        let startTime = new Date();
        let logreq = {
            "@timestamp": startTime.toISOString(),
            "@Id": reqToken,
            query: req.query,
            params: req.params,
            url: req.url,
            fullUrl: req.originalUrl,
            method: req.method,
            headers: req.headers,
            _parsedUrl: req._parsedUrl,
        }

        console.log(
            "timestamp: " + logreq["@timestamp"] + "\t" + 
            "request id: " + logreq["@Id"] + "\t" + 
            "method:  " + req.method + "\t" +
            "URL: " + req.originalUrl);

        this.logfileStream.write(JSON.stringify(logreq));

        const cleanup = () => {
            res.removeListener('finish', logFn)
            res.removeListener('close', abortFn)
            res.removeListener('error', errorFn)
        }

        const logFn = () => {
            let endTime = new Date();
            cleanup()
            let logres = {
                "@timestamp": endTime.toISOString(),
                "@Id": reqToken,
                "queryTime": endTime.valueOf() - startTime.valueOf(),
            }
            console.log(inspect(res));
        }

        const abortFn = () => {
            cleanup()
            console.warn('Request aborted by the client')
        }

        const errorFn = err => {
            cleanup()
            console.error(`Request pipeline error: ${err}`)
        }

        res.on('finish', logFn) // successful pipeline (regardless of its response)
        res.on('close', abortFn) // aborted pipeline
        res.on('error', errorFn) // pipeline internal error

        next();
    };
}
}

然后我将此中间件设置为全局中间件以记录所有请求,但查看res和req对象,它们都没有属性。

在代码示例中,我设置响应对象要被打印,在我的项目上运行一个返回{"message":"Hello World"}的helloworld端点, 我得到以下输出:

时间戳:2019-01-09T00:37:00.912Z 请求ID:2852f925f987 方法:GET URL:/hello-world
服务器响应: { domain: null, _events: { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: false, upgrading: false, chunkedEncoding: false, shouldKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: true, _removedTE: true, _contentLength: 0, _hasBody: false, _trailer: '', finished: true, _headerSent: true, socket: null, connection: null, _header: 'HTTP/1.1 304 Not Modified\r\nX-Powered-By: Express\r\nETag: W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"\r\nDate: Wed, 09 Jan 2019 00:37:00 GMT\r\nConnection: keep-alive\r\n\r\n', _onPendingData: [Function: bound updateOutgoingData], _sent100: false, _expect_continue: false, req: IncomingMessage { _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: false, reading: false, sync: true, needReadable: false, emittedReadable: true, readableListening: false, resumeScheduled: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: true, domain: null, _events: {}, _eventsCount: 0, _maxListeners: undefined, socket: Socket { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, domain: null, _events: [Object], _eventsCount: 10, _maxListeners: undefined, _writableState: [Object], writable: true, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Object], _server: [Object], _idleTimeout: 5000, _idleNext: [Object], _idlePrev: [Object], _idleStart: 12562, _destroyed: false, parser: [Object], on: [Function: socketOnWrap], _paused: false, read: [Function], _consuming: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, connection: Socket { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, domain: null, _events: [Object], _eventsCount: 10, _maxListeners: undefined, _writableState: [Object], writable: true, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', server: [Object], _server: [Object], _idleTimeout: 5000, _idleNext: [Object], _idlePrev: [Object], _idleStart: 12562, _destroyed: false, parser: [Object], on: [Function: socketOnWrap], _paused: false, read: [Function], _consuming: true, _httpMessage: null, [Symbol(async
在响应对象中没有出现{"message":"Hello World"}消息,我想知道如何从res和req对象中获取主体(body)内容,如果可能的话。
注意:我知道nestjs有“拦截器(Interceptors)”,但按照文档所说,中间件应该是解决这个问题的方法。

仍然有问题吗? :-) - Kim Kern
3个回答

5

我偶然遇到了这个问题,它在“相关问题”中列出了我的问题

我可以在Kim Kern的答案上进行更详细的扩展,关于响应。

响应的问题在于响应正文不是响应对象的属性,而是。要能够获取它,您需要覆盖写入该流的方法。

就像Kim Kern已经说过的那样,您可以查看此线程,其中有一个被接受的答案来解决这个问题。

或者您可以使用express-mung中间件,它将为您完成此操作,例如:

var mung = require('express-mung');
app.use(mung.json(
  function transform(body, req, res) {
    console.log(body); // or whatever logger you use
    return body;
  }
));

而 NestJS 还提供了另外两种不同的方式:

  • 拦截器,就像你所说的一样。文档中有一个 LoggingInterceptor 的例子。
  • 您可以为控制器的方法编写装饰器,以拦截它们的响应。
import { isObservable, from, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

/**
 * Logging decorator for controller's methods
 */
export const LogReponse = (): MethodDecorator =>
  (target: object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>) => {

    // save original method
    const original = descriptor.value;

    // replace original method
    descriptor.value = function() { // must be ordinary function, not arrow function, to have `this` and `arguments`

      // get original result from original method
      const ret = original.apply(this, arguments);

      // if it is null or undefined -> just pass it further
      if (ret == null) {
        return ret;
      }

      // transform result to Observable
      const ret$ = convert(ret);

      // do what you need with response data
      return ret$.pipe(
        map(data => {
          console.log(data); // or whatever logger you use
          return data;
        })
      );
    };

    // return modified method descriptor
    return descriptor;
  };

function convert(value: any) {
  // is this already Observable? -> just get it
  if (isObservable(value)) {
    return value;
  }

  // is this array? -> convert from array
  if (Array.isArray(value)) {
    return from(value);
  }

  // is this Promise-like? -> convert from promise, also convert promise result
  if (typeof value.then === 'function') {
    return from(value).pipe(mergeMap(convert));
  }

  // other? -> create stream from given value
  return of(value);
}

请注意,这将在拦截器之前执行,因为此装饰器会更改方法的行为。
我认为这不是一种好的记录方式,只是为了多样性而提到它 :)

2

令人难以置信的是,一些微不足道的事情却如此难以做到。

记录响应体的更简单方法是创建一个拦截器https://docs.nestjs.com/interceptors):

AppModule

providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: HttpInterceptor,
    }
]

HttpInterceptor:

import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class HttpInterceptor implements NestInterceptor {
  private readonly logger = new Logger(HttpInterceptor.name);

  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    return next.handle().pipe(
      map(data => {
        this.logger.debug(data);
        return data;
      }),
    );
  }
}

5
通过使用这个,我只能看到成功的响应,因为我正在使用 GraphQL 数据结构。如果,比如说,一个身份验证守卫阻止了查询(响应是“未经授权”),则拦截器不会被调用。是否有已知的方法可以在所有应用程序处理完成后访问原始响应体?我想记录实际细节,以便了解可能出了什么问题。 - Alex Fortuna
@AlexFortuna 你找到解决办法了吗? - wopolow
@AlexFortuna 你找到解决办法了吗? - undefined
@wopolow 不这么想... 必须隐藏痛苦,这在Node.js中很常见。 - Alex Fortuna
@wopolow 不这么想... 必须隐藏痛苦,这在Node.js中很常见。 - undefined

1

响应主体将无法作为属性访问。查看此线程以获取解决方案。

但是,由于nest默认使用bodyParser,因此您应该能够访问请求正文,并使用req.body进行访问。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接