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

时间:2019-01-09 01:07:07

标签: javascript node.js json typescript nestjs

我正在为一个项目使用nestjs,并希望记录尽可能多的信息,其中之一就是每个http请求的响应和请求的正文。为此,我制作了一个嵌套中间件:

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对象,它们都不具有属性。

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

  

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

     

ServerResponse {     域:null,     _events:{完成:[功能:绑定resOnFinish]},     _eventsCount:1     _maxListeners:未定义,     输出:[],     outputEncodings:[],     outputCallbacks:[],     outputSize:0,     可写:是的,     _last:错误,     升级:错误,     chunkedEncoding:否,     shouldKeepAlive:是的,     useChunkedEncodingByDefault:true,     sendDate:是的,     _removedConnection:否,     _removedContLen:是的,     _removedTE:是的,     _contentLength:0,     _hasBody:否,     _预告片: '',     完成:是的,     _headerSent:是的,     套接字:null,     连接: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:否,     要求:      IncomingMessage {        _可读状态:         ReadableState {           objectMode:否,           highWaterMark:16384,           缓冲区:[对象],           长度:0,           管道:null,           pipeCount:0,           流动:是的,           结束:是的,           endEmitted:错误,           阅读:错误,           同步:是的,           needReadable:否,           emissionReadable:true,           可读听:false,           resumeScheduled:是的,           销毁:错误,           defaultEncoding:'utf8',           awaitDrain:0,           阅读更多:是的,           解码器:null,           编码:null},        可读:正确,        域:null,        _events:{},        _eventsCount:0,        _maxListeners:未定义,        插座:         插座{           连接:错误,           _hadError:否,           _handle:[对象],           _parent:null,           _host:null,           _可读状态:[对象],           可读:正确,           域:null,           _events:[对象],           _eventsCount:10,           _maxListeners:未定义,           _writableState:[Object],           可写:是的,           allowHalfOpen:是,           _bytesDispatched:155,           _sockname:null,           _pendingData:空,           _pendingEncoding:'',           服务器:[对象],           _server:[对象],           _idleTimeout:5000,           _idleNext:[对象],           _idlePrev:[Object],           _idleStart:12562,           _destroyed:错误,           解析器:[Object],           开启:[功能:socketOnWrap],           _paused:错误,           读取:[功能],           _using:是的,           _httpMessage:null,           [Symbol(asyncId)]:151,           [Symbol(bytesRead)]:0,           [Symbol(asyncId)]:153,           [Symbol(triggerAsyncId)]:151},        连接:         插座{           连接:错误,           _hadError:否,           _handle:[对象],           _parent:null,           _host:null,           _可读状态:[对象],           可读:正确,           域:null,           _events:[对象],           _eventsCount:10,           _maxListeners:未定义,           _writableState:[Object],           可写:是的,           allowHalfOpen:是,           _bytesDispatched:155,           _sockname:null,           _pendingData:空,           _pendingEncoding:'',           服务器:[对象],           _server:[对象],           _idleTimeout:5000,           _idleNext:[对象],           _idlePrev:[Object],           _idleStart:12562,           _destroyed:错误,           解析器:[Object],           开启:[功能:socketOnWrap],           _paused:错误,           读取:[功能],           _using:是的,           _httpMessage:null,           [Symbol(asyncId)]:151,           [Symbol(bytesRead)]:0,           [Symbol(asyncId)]:153,           [Symbol(triggerAsyncId)]:151},        httpVersionMajor:1        httpVersionMinor:1        httpVersion:“ 1.1”,        完成:正确,        标头:         {host:'localhost:5500',           '用户代理':'Mozilla / 5.0(X11; Ubuntu; Linux x86_64; rv:64.0)Gecko / 20100101 Firefox / 64.0',           接受:'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',           连接:“保持活动”,           'upgrade-insecure-requests':'1',           'if-none-match':'W /“ 19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk”'},        rawHeaders:         ['主持人',           '本地主机:5500',           '用户代理',           'Mozilla / 5.0(X11; Ubuntu; Linux x86_64; rv:64.0)Gecko / 20100101 Firefox / 64.0',           '接受',           'text / html,application / xhtml + xml,application / xml; q = 0.9, / ; q = 0.8',           “接受语言”,           'en-US,en; q = 0.5',           “接受编码”,           'gzip,放气',           '连接',           '活着',           “不安全升级请求”,           '1',           如果不匹配,           'W /“ 19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk”'],        预告片:{},        rawTrailers:[],        升级:错误,        网址:“ / hello-world”,        方法:“ GET”,        statusCode:null,        statusMessage:null,        客户:         插座{           连接:错误,           _hadError:否,           _handle:[对象],           _parent:null,           _host:null,           _可读状态:[对象],           可读:正确,           域:null,           _events:[对象],           _eventsCount:10,           _maxListeners:未定义,           _writableState:[Object],           可写:是的,           allowHalfOpen:是,           _bytesDispatched:155,           _sockname:null,           _pendingData:空,           _pendingEncoding:'',           服务器:[对象],           _server:[对象],           _idleTimeout:5000,           _idleNext:[对象],           _idlePrev:[Object],           _idleStart:12562,           _destroyed:错误,           解析器:[Object],           开启:[功能:socketOnWrap],           _paused:错误,           读取:[功能],           _using:是的,           _httpMessage:null,           [Symbol(asyncId)]:151,           [Symbol(bytesRead)]:0,           [Symbol(asyncId)]:153,           [Symbol(triggerAsyncId)]:151},        _消耗:错误,        _dumped:是的,        下一个:[功能:下一个],        baseUrl:”,        originalUrl:'/ hello-world',        _parsedUrl:         网址{           协议:null,           斜线:null,           身份验证:null,           主持人:null,           端口:null,           主机名:null,           杂凑:null,           搜索:null,           查询:null,           路径名:“ / hello-world”,           路径:“ / hello-world”,           href:'/ hello-world',           _raw:'/ hello-world'},        参数:{},        查询:{},        RES:[通告],        身体: {},        路线:路线{路径:'/ hello-world',堆栈:[Array],方法:[Object]}},     当地人:{},     statusCode:304,     statusMessage:“未修改”,     [Symbol(outHeadersKey)]:      {'x-powered by':['X-Powered-By','Express'],        etag:['ETag','W /“ 19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk”']}

在响应对象的任何地方都没有出现{“ message”:“ Hello World”}消息,我想知道如何从res和req对象获取主体。

注意:我知道nestjs具有Interceptors,但是按照文档的说明,中间件应该是此问题的解决方案。

3 个答案:

答案 0 :(得分:1)

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

关于响应,我可以再扩展Kim Kern's answer

响应问题是响应主体不是响应对象的属性,而是 stream 。为了获得它,您需要重写写入该流的方法。

就像金·克恩(Kim Kern)所说的那样,您可以查看this thread,那里给出了如何做的答案。

或者您可以使用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可以为您提供两种其他方式:

  • Interceptors,就像你说的那样。文档中有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);
}

不过请注意,这将在拦截器之前执行 ,因为此修饰器会更改方法的行为。

我不认为这是进行日志记录的好方法,只是为了多样性而提到了它:)

答案 1 :(得分:0)

响应正文将无法作为属性访问。有关解决方案,请参见此thread

但是,您应该可以使用req.body访问 request 正文,因为默认情况下nest使用bodyParser

答案 2 :(得分:0)

如此微不足道的事情很难做到如此令人难以置信。

记录响应正文的更简单方法是创建拦截器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;
      }),
    );
  }
}