промежуточное ПО nestjs получает тело запроса / ответа - PullRequest
0 голосов
/ 09 января 2019

Я работаю с 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 идентификатор запроса: 2852f925f987 метод: GET URL: / hello-world

ServerResponse { домен: ноль, _events: {finish: [Function: bound resOnFinish]}, _eventsCount: 1, _maxListeners: не определено, выход: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, доступный для записи: правда, _last: false, модернизация: ложь, chunkedEncoding: false, shouldKeepAlive: правда, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: правда, _removedTE: правда, _contentLength: 0, _hasBody: false, _trailer: '', закончено: правда, _headerSent: верно, сокет: ноль, соединение: ноль, _header: 'HTTP / 1.1 304 Не изменено GMT \ r \ nСоединение: keep-alive \ r \ n \ r \ n ', _onPendingData: [Функция: связанная updateOutgoingData], _sent100: ложно, _expect_continue: false, REQ: IncomingMessage { _readableState: ReadableState { objectMode: false, HighWaterMark: 16384, буфер: [объект], длина: 0, трубы: ноль, pipeCount: 0, течет: правда, закончилась: правда, endEmitted: false, чтение: ложь, синхронизация: правда, needReadable: false, emittedReadable: true, readableListening: false, резюме запланировано: правда, уничтожено: ложно, defaultEncoding: 'utf8', awaitDrain: 0, чтение: правда, декодер: ноль, кодировка: ноль}, читабельно: правда, домен: ноль, _События: {}, _eventsCount: 0, _maxListeners: не определено, разъем: Разъем { подключение: ложь, _hadError: false, _handle: [Объект], _parent: null, _host: null, _readableState: [Object], читабельно: правда, домен: ноль, _events: [Объект], _eventsCount: 10, _maxListeners: не определено, _writableState: [Object], доступный для записи: правда, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', сервер: [объект], _server: [Объект], _idleTimeout: 5000, _idleNext: [Объект], _idlePrev: [Объект], _idleStart: 12562, _royroyed: ложь, парсер: [объект], вкл: [Функция: socketOnWrap], _paused: false, читать: [Функция], _consuming: правда, _httpMessage: null, [Символ (asyncId)]: 151, [Symbol (bytesRead)]: 0, [Символ (asyncId)]: 153, [Symbol (triggerAsyncId)]: 151}, подключение: Разъем { подключение: ложь, _hadError: false, _handle: [Объект], _parent: null, _host: null, _readableState: [Object], читабельно: правда, домен: ноль, _events: [Объект], _eventsCount: 10, _maxListeners: не определено, _writableState: [Object], доступный для записи: правда, allowHalfOpen: true,_bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', сервер: [объект], _server: [Объект], _idleTimeout: 5000, _idleNext: [Объект], _idlePrev: [Объект], _idleStart: 12562, _royroyed: ложь, парсер: [объект], вкл: [Функция: socketOnWrap], _paused: false, читать: [Функция], _consuming: правда, _httpMessage: null, [Символ (asyncId)]: 151, [Symbol (bytesRead)]: 0, [Символ (asyncId)]: 153, [Symbol (triggerAsyncId)]: 151}, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', завершено: правда, заголовки: {host: 'localhost: 5500', 'user-agent': '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', соединение: «keep-alive», 'upgrade-insecure-запросы': '1', 'if-none-match': 'W / "19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk"'}, rawHeaders: ['Хост', 'Локальный: 5500', 'User-Agent', 'Mozilla / 5.0 (X11; Ubuntu; Linux x86_64; rv: 64.0) Gecko / 20100101 Firefox / 64.0', «Принять», 'Текст / HTML, приложение / XHTML + XML, приложение / XML; д = 0,9, / ; д = 0,8', 'Accept-Language', «Ен-США, ан; д = 0,5», 'Accept-Encoding', 'gzip, deflate', 'Connection', «Держать-живой», «Обновление нестабильных-запросов», '1', 'If-None-Match', 'W / "19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk"'], прицепы: {}, rawTrailers: [], обновление: ложь, url: '/ hello-world', метод: «ПОЛУЧИТЬ», statusCode: null, statusMessage: null, клиент: Разъем { подключение: ложь, _hadError: false, _handle: [Объект], _parent: null, _host: null, _readableState: [Object], читабельно: правда, домен: ноль, _events: [Объект], _eventsCount: 10, _maxListeners: не определено, _writableState: [Object], доступный для записи: правда, allowHalfOpen: true, _bytesDispatched: 155, _sockname: null, _pendingData: null, _pendingEncoding: '', сервер: [объект], _server: [Объект], _idleTimeout: 5000, _idleNext: [Объект], _idlePrev: [Объект], _idleStart: 12562, _royroyed: ложь, парсер: [объект], вкл: [Функция: socketOnWrap], _paused: false, читать: [Функция], _consuming: правда, _httpMessage: null, [Символ (asyncId)]: 151, [Symbol (bytesRead)]: 0, [Символ (asyncId)]: 153, [Symbol (triggerAsyncId)]: 151}, _consuming: ложь, _dumped: правда, следующий: [Функция: следующий], baseUrl: '', originalUrl: '/ hello-world', _parsedUrl: Url { протокол: ноль, косая черта: ноль, auth: null, хост: ноль, порт: ноль, имя хоста: ноль, хэш: ноль, поиск: ноль, запрос: ноль, путь: '/ hello-world', путь: '/ hello-world', href: '/ hello-world', _raw: '/ hello-world'}, params: {}, запрос: {}, Res: [Циркуляр], тело: {}, route: Route {path: '/ hello-world', стек: [Array], методы: [Object]}}, местные жители: {}, statusCode: 304, statusMessage: «Не изменено», [Символ (outHeadersKey)]: {'x-powered-by': ['X-Powered-By', 'Express'], etag: ['ETag', 'W / "19-c6Hfa5VVP + Ghysj + 6y9cPi5QQbk"']}}

Ни в одном месте в объекте ответа не появляется сообщение {"message": "Hello World"}, я хотел бы знать, как получить тело из объектов res и req, если это возможно, пожалуйста.

Примечание: я знаю, что nestjs имеет Interceptors, но, следуя указаниям документации, промежуточное ПО должно быть решением этой проблемы.

Ответы [ 2 ]

0 голосов
/ 12 февраля 2019

Я случайно столкнулся с этим вопросом, он был указан в "связанных" с моим вопросом .

Я могу расширить Ответ Ким Керн чуть больше, об ответах.

Проблема с ответом заключается в том, что тело ответа - это не свойство объекта ответа, а stream . Чтобы получить его, вам нужно переопределить методы, которые записывают в этот поток.

Как уже говорила Ким Керн, вы можете посмотреть на эту ветку , здесь есть принятый ответ, как это сделать.

Или вы можете взять промежуточное программное обеспечение 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);
}

Обратите внимание, что это будет выполнено перед перехватчиками , потому что этот декоратор меняет поведение методов.

И я не думаю, что это хороший способ ведения журнала, просто упомянул это для разнообразия:)

0 голосов
/ 09 января 2019

Тело response не будет доступно как свойство. Смотрите это нить для решения.

Однако вы должны иметь возможность получить доступ к телу запроса с помощью req.body, так как гнездо использует bodyParser по умолчанию.

...