我正在为一个项目使用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
,但是按照文档的说明,中间件应该是此问题的解决方案。
答案 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可以为您提供两种其他方式:
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;
}),
);
}
}