所有文章 > 日积月累 > 如何构建 Node.js 中间件来记录 HTTP API 请求和响应
如何构建 Node.js 中间件来记录 HTTP API 请求和响应

如何构建 Node.js 中间件来记录 HTTP API 请求和响应

有许多不同的运行时和生态系统用于构建 API,在 Moesif,我们尝试使与它们的集成尽可能简单。我们构建了许多有助于这种集成的库,其中之一就是Moesif Express 中间件库,简称 Moesif-Express。

尽管名字如此,Moesif-Express 仍可与使用内置http模块的 Node.js 应用程序一起使用。

Node.js 异步处理请求,这有时会导致问题,尤其是当我们想要调试系统或记录它们正在做什么时。

在本文中,我们将介绍构建 Moesif-Express 库的步骤、相关日志数据的位置以及如何挂接到 Node.jshttp模块以在管道中的正确时间处理数据收集。

Node.js 的 HTTP 模块

Node.js 带有开箱即用的 HTTP 服务器实现,虽然它在大多数应用程序中并不直接使用,但它对于了解请求和响应的基础知识是一个很好的开始。

GET 请求的基本日志记录

日志记录的理念是,我们将某种数据写入某个持久性数据存储中,以便稍后查看。为了简单起见,我们首先使用 进行stdout写入console.log()

有时,记录到stdout不是一种选择,我们必须将记录数据发送到其他地方。例如在无服务器环境中运行时,函数没有持久存储。

让我们尝试一个简单的服务器,它除了对每个请求发送空响应之外什么都不做,以说明如何获取可记录的请求数据。

const http = require("http");

const server = http.createServer((request, response) => {
console.log(request);
response.end();
});

server.listen(8888);

如果我们向http://localhost:8888发送请求,我们会看到一个巨大的对象被记录到stdout,它充满了实现细节,找到重要的部分并不容易。

让我们看一下IncomingMessage的 Node.js 文档,我们的请求是该类的一个对象。

我们可以在这里找到什么信息?

  • headersrawHeaders(对于无效/重复的标题)
  • httpVersion
  • method
  • url
  • socket.remoteAddress(针对客户端 IP)

对于 GET 请求来说这应该足够了,因为它们通常没有主体。让我们更新我们的实现。

const server = http.createServer((request, response) => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

console.log(
JSON.stringify({
timestamp: Date.now(),
rawHeaders,
httpVersion,
method,
remoteAddress,
remoteFamily,
url
})
);

response.end();
});

输出应如下所示:

{
"timestamp": 1562331336922,
"rawHeaders": [
"cache-control",
"no-cache",
"Postman-Token",
"dcd81e98-4f98-42a3-9e13-10c8401892b3",
"User-Agent",
"PostmanRuntime/7.6.0",
"Accept",
"*/*",
"Host",
"localhost:8888",
"accept-encoding",
"gzip, deflate",
"Connection",
"keep alive"
],
"httpVersion": "1.1",
"method": "GET",
"remoteAddress": "::1",
"remoteFamily": "IPv6",
"url": "/"
}

我们仅使用请求的特定部分进行记录。它使用 JSON 作为格式,因此它是一种结构化的记录方法,并且具有时间戳,因此我们不仅知道谁请求了什么,还知道请求何时开始。

记录处理时间

如果我们想要添加有关请求处理时间的数据,我们需要一种方法来检查它何时完成。

当我们发送完响应时,请求就完成了,所以我们必须检查何时response.end()调用了。在我们的示例中,这相当简单,但有时这些结束调用是由其他模块完成的。

为此,我们可以查看ServerResponse类的文档。它提到finish当所有服务器完成发送响应时触发一个事件。这并不意味着客户端收到了所有内容,但它表明我们的工作已完成。

让我们更新我们的代码!

const server = http.createServer((request, response) => {
const requestStart = Date.now();

response.on("finish", () => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

console.log(
JSON.stringify({
timestamp: Date.now(),
processingTime: Date.now() - requestStart,
rawHeaders,
httpVersion,
method,
remoteAddress,
remoteFamily,
url
})
);
});

process(request, response);
});

const process = (request, response) => {
setTimeout(() => {
response.end();
}, 100);
};

我们将请求的处理过程传递给一个单独的函数,以模拟负责处理该请求的其他模块setTimeout。由于 ,处理过程是异步进行的,因此同步日志记录无法获得所需的结果,但事件会在调用finish触发来处理此问题。 response.end()

记录身体

请求主体仍然未被记录,这意味着 POST、PUT 和 PATCH 请求没有 100% 覆盖。

为了将主体也放入日志中,我们需要一种方法将其从请求对象中提取出来。

IncomingMessage类实现了ReadableStream接口。它使用该接口的事件来发出来自客户端的主体数据到达的信号。

  • data当服务器从客户端收到新的数据块时触发此事件
  • end当所有数据都已发送时,将调用此事件
  • error出现错误时调用此事件

让我们更新代码:

const server = http.createServer((request, response) => {
const requestStart = Date.now();

let errorMessage = null;
let body = [];
request.on("data", chunk => {
body.push(chunk);
});
request.on("end", () => {
body = Buffer.concat(body);
body = body.toString();
});
request.on("error", error => {
errorMessage = error.message;
});

response.on("finish", () => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

console.log(
JSON.stringify({
timestamp: Date.now(),
processingTime: Date.now() - requestStart,
rawHeaders,
body,
errorMessage,
httpVersion,
method,
remoteAddress,
remoteFamily,
url
})
);
});

process(request, response);
});

这样,当出现问题时,我们会记录额外的错误消息,并将正文内容添加到日志中。

注意:正文可能非常大和/或二进制,因此需要进行验证检查,否则数据量或编码可能会弄乱我们的日志。

记录响应数据

现在我们已经收到了请求,下一步就是记录我们的回应。

我们已经监听了finish响应事件,因此我们有一个相当安全的方法来获取所有数据。我们只需提取响应对象所包含的内容即可。

让我们看一下ServerResponse类的文档来了解它为我们提供了什么。

  • statusCode
  • statusMessage
  • getHeaders()

让我们将其添加到我们的代码中。

const server = http.createServer((request, response) => {
const requestStart = Date.now();

let errorMessage = null;
let body = [];
request.on("data", chunk => {
body.push(chunk);
});
request.on("end", () => {
body = Buffer.concat(body).toString();
});
request.on("error", error => {
errorMessage = error.message;
});

response.on("finish", () => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

const { statusCode, statusMessage } = response;
const headers = response.getHeaders();

console.log(
JSON.stringify({
timestamp: Date.now(),
processingTime: Date.now() - requestStart,
rawHeaders,
body,
errorMessage,
httpVersion,
method,
remoteAddress,
remoteFamily,
url,
response: {
statusCode,
statusMessage,
headers
}
})
);
});

process(request, response);
});

处理响应错误和客户端中止

目前,我们仅在finish触发响应事件时进行记录,如果响应出现问题或客户端中止请求,则不会进行记录。

对于这两种情况,我们需要创建额外的处理程序。

const server = http.createServer((request, response) => {
const requestStart = Date.now();

let body = [];
let requestErrorMessage = null;

const getChunk = chunk => body.push(chunk);
const assembleBody = () => {
body = Buffer.concat(body).toString();
};
const getError = error => {
requestErrorMessage = error.message;
};
request.on("data", getChunk);
request.on("end", assembleBody);
request.on("error", getError);

const logClose = () => {
removeHandlers();
log(request, response, "Client aborted.");
};
const logError = error => {
removeHandlers();
log(request, response, error.message);
};
const logFinish = () => {
removeHandlers();
log(request, response, requestErrorMessage);
};
response.on("close", logClose);
response.on("error", logError);
response.on("finish", logFinish);

const removeHandlers = () => {
request.off("data", getChunk);
request.off("end", assembleBody);
request.off("error", getError);
response.off("close", logClose);
response.off("error", logError);
response.off("finish", logFinish);
};

process(request, response);
});

const log = (request, response, errorMessage) => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

const { statusCode, statusMessage } = response;
const headers = response.getHeaders();

console.log(
JSON.stringify({
timestamp: Date.now(),
processingTime: Date.now() - requestStart,
rawHeaders,
body,
errorMessage,
httpVersion,
method,
remoteAddress,
remoteFamily,
url,
response: {
statusCode,
statusMessage,
headers
}
})
);
};

现在,我们还记录错误和中止。

响应完成后,日志处理程序也会被删除,并且所有日志记录都会移至额外的函数。

记录到外部 API

目前,该脚本仅将其日志写入控制台,在许多情况下,这已经足够了,因为操作系统允许其他程序捕获stdout并对其执行操作,例如写入文件或将其发送到第三方 API(如 Moesif)。

console.log在某些环境中,这是不可能的,但由于我们将所有信息聚集到一个地方,我们可以用第三方函数替换调用。

让我们重构代码,使其类似于一个库并记录到某些外部服务。

const log = loggingLibrary({ apiKey: "XYZ" });
const server = http.createServer((request, response) => {
log(request, response);
process(request, response);
});

const loggingLibray = config => {
const loggingApiHeaders = {
Authorization: "Bearer " + config.apiKey
};

const log = (request, response, errorMessage, requestStart) => {
const { rawHeaders, httpVersion, method, socket, url } = request;
const { remoteAddress, remoteFamily } = socket;

const { statusCode, statusMessage } = response;
const responseHeaders = response.getHeaders();

http.request("https://example.org/logging-endpoint", {
headers: loggingApiHeaders,
body: JSON.stringify({
timestamp: requestStart,
processingTime: Date.now() - requestStart,
rawHeaders,
body,
errorMessage,
httpVersion,
method,
remoteAddress,
remoteFamily,
url,
response: {
statusCode,
statusMessage,
headers: responseHeaders
}
})
});
};

return (request, response) => {
const requestStart = Date.now();

// ========== REQUEST HANLDING ==========
let body = [];
let requestErrorMessage = null;
const getChunk = chunk => body.push(chunk);
const assembleBody = () => {
body = Buffer.concat(body).toString();
};
const getError = error => {
requestErrorMessage = error.message;
};
request.on("data", getChunk);
request.on("end", assembleBody);
request.on("error", getError);

// ========== RESPONSE HANLDING ==========
const logClose = () => {
removeHandlers();
log(request, response, "Client aborted.", requestStart);
};
const logError = error => {
removeHandlers();
log(request, response, error.message, requestStart);
};
const logFinish = () => {
removeHandlers();
log(request, response, requestErrorMessage, requestStart);
};
response.on("close", logClose);
response.on("error", logError);
response.on("finish", logFinish);

// ========== CLEANUP ==========
const removeHandlers = () => {
request.off("data", getChunk);
request.off("end", assembleBody);
request.off("error", getError);

response.off("close", logClose);
response.off("error", logError);
response.off("finish", logFinish);
};
};
};

经过这些更改,我们现在可以像使用Moesif-Express一样使用我们的日志记录实现。

loggingLibrary函数以 API 密钥作为配置,并返回实际的日志记录函数,该函数将通过 HTTP 将日志数据发送到日志服务。日志记录函数本身采用requestandresponse对象。

结论

Node.js 中的日志记录并不像人们想象的那么简单,尤其是在 HTTP 环境中。JavaScript 异步处理许多事情,因此我们需要挂接到正确的事件,否则,我们不知道发生了什么。

幸运的是,已经有很多日志库可用,所以我们不必自己编写一个。

以下是其中几点:

  • Bunyan – https://github.com/trentm/node-bunyan
  • Debug – https://github.com/visionmedia/debug
  • Log4js – https://github.com/log4js-node/log4js-node
  • Morgan – https://github.com/expressjs/morgan
  • Npmlog – https://github.com/npm/npmlog
  • Winston – https://github.com/winstonjs/winston

文章来源:How we built a Node.js Middleware to Log HTTP API Requests and Responses

#你可能也喜欢这些API文章!