简体   繁体   English

nestjs 中间件获取请求/响应正文

[英]nestjs middleware get request/response body

I am working with nestjs for a project, and want to log as much information as possible, one such thing being the body of the response and request of every http request.我正在使用 nestjs 进行一个项目,并希望记录尽可能多的信息,其中之一是每个 http 请求的响应和请求的正文。 I made a nest middleware for that end:为此,我制作了一个嵌套中间件:

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();
    };
}
}

Then I set this middleware as a global middleware to log all request, but looking at the res and req object, neither of them have a property.然后我将这个中间件设置为全局中间件来记录所有请求,但是查看 res 和 req 对象,它们都没有属性。

In the code sample I set the response object to be printed, running a hello world endpoint on my project that returns {"message":"Hello World"} I get the following output:在代码示例中,我设置了要打印的响应对象,在我的项目上运行一个 hello world 端点,返回 {"message":"Hello World"} 我得到以下输出:

timestamp: 2019-01-09T00:37:00.912Z request id: 2852f925f987 method: GET URL: /hello-world时间戳:2019-01-09T00:37:00.912Z 请求 ID:2852f925f987 方法:GET URL:/hello-world

ServerResponse { 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 ServerResponse { domain: null, _events: { finish: [Function: bound resOnFinish] }, _eventsCount: 1, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last:false,升级:false,chunkedEncoding:false,shouldKeepAlive:true,useChunkedEncodingByDefault:true,sendDate:true,_removedConnection:false,_removedContLen:true,_removedTE:true,_contentLength:0,_hasBody:false,_trailer:'',完成: 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\\n日期:2019 年 1 月 9 日星期三 00:37:00 GMT\\r\\n连接:保持活动\\r\\n\\r\\n',_onPendingData:[函数:绑定 updateOutgoingData],_sent100:假,_expect_continue:假, req: IncomingMessage { _readableState: ReadableState { objectMode: false, highWaterMark: 16384, 缓冲区: [Object], 长度: 0, 管道: null, pipeCount: 0, 流动: true, 结束: 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(asy ,阅读:假,同步:真,needReadable:假,发射可读:真,可读监听:假,resumeScheduled:真,销毁:假,defaultEncoding:'utf8',awaitDrain:0,readingMore:真,解码器:空,编码:空},可读:真,域:空,_events:{},_eventsCount:0,_maxListeners:未定义,套接字:套接字{连接:假,_hadError:假,_handle:[对象],_parent:空,_host:空,_readableState : [Object], 可读: true, 域: 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], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asy) ncId)]: 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(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { host: 'localhost:5500', 'user-agent': 'Mozilla/5.0 (X11; ncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, connection: Socket {connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], 可读: true,域:null,_events:[Object],_eventsCount:10,_maxListeners:未定义,_writableState:[Object],可写: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], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId) )]: 151 }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { host: 'localhost:5500', 'user-agent': 'Mozilla/5.0 (X11; Ubuntu;乌班图; Linux x86_64; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9, / ;q=0.8', 'accept-language': 'en-US,en;q=0.5', 'accept-encoding': 'gzip, deflate', connection: 'keep-alive', 'upgrade-insecure-requests': '1', 'if-none-match': 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' }, rawHeaders: [ 'Host', 'localhost:5500', 'User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0', 'Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9, / ;q=0.8', 'Accept-Language', 'en-US,en;q=0.5', 'Accept-Encoding', 'gzip, deflate', 'Connection', 'keep-alive', 'Upgrade-Insecure-Requests', '1', 'If-None-Match', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' ], trailers: {}, rawTrailers: [], upgrade: false, url: '/hello-world', method: 'GET', statusCode: null, statusMessage: null, client: Socket { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, domain: null, _events: [Obje RV:64.0)的Gecko / 20100101火狐/ 64.0' ,接受: 'text / html的,是application / xhtml + xml的,应用/ XML; Q = 0.9,/ Q = 0.8', '接受语言':“EN-US ,en;q=0.5', 'accept-encoding': 'gzip, deflate', 连接: 'keep-alive', 'upgrade-insecure-requests': '1', 'if-none-match': 'W /"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' }, rawHeaders: [ 'Host', 'localhost:5500', 'User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:64.0) Gecko/ 20100101火狐/ 64.0' , '接受', 'text / html的,是application / xhtml + xml的,应用/ XML; q = 0.9,/ q = 0.8', '接受语言',“EN-US,EN; q =0.5', 'Accept-Encoding', 'gzip, deflate', 'Connection', 'keep-alive', 'Upgrade-Insecure-Requests', '1', 'If-None-Match', 'W/' 19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"'], 预告片: {}, rawTrailers: [], upgrade: false, url: '/hello-world', method: 'GET', statusCode: null, statusMessage: null, client:套接字{连接:false,_hadError:false,_handle:[Object],_parent:null,_host:null,_readableState:[Object],可读:true,域:null,_events:[对象] ct], _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 }, _consuming: false, _dumped: true, next: [Function: next], baseUrl: '', originalUrl: '/hello-world', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: null, query: null, pathname: '/hello-world', path: '/hello-world', href: '/hello-world', _raw: '/hello-world' }, params: {}, query: {}, res: [Circular], body: {}, route: Route { path: '/hello-world', stack: [Array], methods ct], _eventsCount: 10, _maxListeners: 未定义, _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 ], _sumption: true, _httpMessage: null, [Symbol(asyncId)]: 151, [Symbol(bytesRead)]: 0, [Symbol(asyncId)]: 153, [Symbol(triggerAsyncId)]: 151 }, _sumption: false , _dumped: true, next: [Function: next], baseUrl: '', originalUrl: '/hello-world', _parsedUrl: Url { protocol: null, slashes: null, auth: null, host: null, port: null ,主机名:空,哈希:空,搜索:空,查询:空,路径名:'/hello-world',路径:'/hello-world',href:'/hello-world',_raw:'/hello- world' }, params: {}, query: {}, res: [Circular], body: {}, route: Route { path: '/hello-world', stack: [Array], methods : [Object] } }, locals: {}, statusCode: 304, statusMessage: 'Not Modified', [Symbol(outHeadersKey)]: { 'x-powered-by': [ 'X-Powered-By', 'Express' ], etag: [ 'ETag', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' ] } } : [Object] } }, locals: {}, statusCode: 304, statusMessage: 'Not Modified', [Symbol(outHeadersKey)]:​​ { 'x-powered-by': [ 'X-Powered-By', 'Express '], etag: [ 'ETag', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"'] } }

In no place in the response object does the {"message":"Hello World"} message appears, I would like to know how to obtain the body from the res and req objects if it is possible please.在响应对象中没有出现 {"message":"Hello World"} 消息,如果可能的话,我想知道如何从 res 和 req 对象中获取正文。

Note: I know that nestjs has Interceptors , but following what the documentation says, middleware should be the solution for this problem.注意:我知道 nestjs 有Interceptors ,但是按照文档的说明,中间件应该是解决这个问题的方法。

I've run against this question accidentally, it was listed in "related" to my question .我不小心遇到了这个问题,它列在与我的问题“相关”中。

I can extend Kim Kern's answer a bit more, about responses.关于响应,我可以进一步扩展Kim Kern 的回答

Problem with response is that response body ain't a property of response object, but stream .响应的问题是响应主体不是响应对象的属性,而是 To be able to get it, you need to override methods, which writes to that stream.为了能够获得它,您需要覆盖写入该流的方法。

Like Kim Kern already said, you can look onto this thread , there is accepted answer how to do this.就像 Kim Kern 已经说过的那样,你可以看看这个线程,有一个公认的答案如何做到这一点。

Or you can take express-mung middleware, which will do it for you, for example:或者您可以使用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;
  }
));

And there are two other different ways, which NestJS can offer you:还有另外两种不同的方式,NestJS 可以为您提供:

  • Interceptors , like you said.拦截器,就像你说的。 There is example of LoggingInterceptor in documentation.文档中有LoggingInterceptor示例。
  • You can write decorator for controller's methods, which will intercept their responses.您可以为控制器的方法编写装饰器,这将拦截它们的响应。
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);
}

Note though, that this will executes before interceptors , because this decorator change methods behaviour.但请注意,这将在拦截之前执行,因为此装饰器更改方法行为。

And I don't think this is nice way to do logging, just mentioned it for variety :)而且我不认为这是进行日志记录的好方法,只是出于多样性提到了它:)

It's incredible how something so trivial is so hard to do.令人难以置信的是,如此琐碎的事情如此难以做到。

The easier way to log response body is to create an Interceptor ( https://docs.nestjs.com/interceptors ):记录响应正文的更简单方法是创建一个拦截器https://docs.nestjs.com/interceptors ):

AppModule :应用模块

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

HttpInterceptor : Http拦截器

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;
      }),
    );
  }
}

The response body will not be accessible as a property.响应正文将无法作为属性访问。 See this thread for a solution.请参阅此线程以获取解决方案。

You should however be able to access the request body with req.body since nest uses bodyParser by default.然而,你应该能够访问请求主体req.body因为巢用途bodyParser默认。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM