جاوااسکریپت - میدلور نِستجیاس دریافت بدنه درخواست/پاسخ


من برای یک پروژه با nestjs کار میکنم و میخواهم هر چه اطلاعات ممکن است را ثبت کنم، یکی از این موارد بدنهٔ پاسخ و درخواست هر درخواست HTTP است. من یک middleware در nest برای این منظور ساختهام:

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، هیچکدام از آنها دارای ویژگی نبودند.

در نمونه کد، شیء پاسخ را برای چاپ تنظیم کردم، و یک endpoint سلام دنیا در پروژهام اجرا کردم که {"message":"Hello World"} را برمیگرداند، خروجی زیر را دریافت میکنم:

timestamp: 2019-01-09T00:37:00.912Z   request id: 2852f925f987    method:  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, 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(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; 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: [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 }, _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: [Object] } }, locals: {}, statusCode: 304, statusMessage: 'Not Modified', [Symbol(outHeadersKey)]: { 'x-powered-by': [ 'X-Powered-By', 'Express' ], etag: [ 'ETag', 'W/"19-c6Hfa5VVP+Ghysj+6y9cPi5QQbk"' ] } }

در هیچ بخشی از شیء پاسخ پیام {"message":"Hello World"} ظاهر نمیشود، من میخواهم بدانم چگونه میتوان بدنه را از اشیاء res و req دریافت کرد اگر ممکن است لطفاً.

توجه: میدانم که nestjs دارای Interceptors است، اما طبق آنچه مستندات میگوید، میانافزار باید راهحل این مشکل باشد.

پاسخ شماره ۱

من به طور اتفاقی با این سؤال مواجه شدم، این سؤال در بخش «مرتبط» با سؤال من فهرست شده بود.

میتوانم پاسخ کیم کرن را کمی بیشتر در مورد واکنشها گسترش دهم.

مشکل با پاسخ این است که بدنهٔ پاسخ یک ویژگی از شیء پاسخ نیست، بلکه جریان است. برای اینکه بتوان آن را دریافت کرد، شما نیاز دارید روشهایی را بازنویسی کنید که به آن جریان مینویسند.

همانطور که کیم کرن قبلاً گفته بود، میتوانید به این رشته نگاه کنید، یک پاسخ پذیرفتهشده وجود دارد که چگونه این کار را انجام دهید.

یا میتوانید از میدلور 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);
}

با این حال توجه کنید، این قبل از میانجیها اجرا خواهد شد، زیرا این دکوریتور رفتار متدها را تغییر میدهد.

و فکر نمیکنم این روش خوبی برای ثبت لاگ باشد، فقط به خاطر تنوع اشاره کردم :)

پاسخ شماره ۲

شگفتآور است که چیزی اینقدر پیش پا افتاده، اینقدر سخت است که انجام شود.

روش سادهتر برای لاگ گرفتن از بدنه پاسخ این است که یک اینترسپتور ایجاد کنید (https://docs.nestjs.com/interceptors):

ماژولاپ:

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