
Node.js 后端开发指南:搭建、优化与部署
有许多不同的运行时和生态系统用于构建 API,在 Moesif,我们尝试使与它们的集成尽可能简单。我们构建了许多有助于这种集成的库,其中之一就是Moesif Express 中间件库,简称 Moesif-Express。
尽管名字如此,Moesif-Express 仍可与使用内置http
模块的 Node.js 应用程序一起使用。
Node.js 异步处理请求,这有时会导致问题,尤其是当我们想要调试系统或记录它们正在做什么时。
在本文中,我们将介绍构建 Moesif-Express 库的步骤、相关日志数据的位置以及如何挂接到 Node.jshttp
模块以在管道中的正确时间处理数据收集。
Node.js 带有开箱即用的 HTTP 服务器实现,虽然它在大多数应用程序中并不直接使用,但它对于了解请求和响应的基础知识是一个很好的开始。
日志记录的理念是,我们将某种数据写入某个持久性数据存储中,以便稍后查看。为了简单起见,我们首先使用 进行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 文档,我们的请求是该类的一个对象。
我们可以在这里找到什么信息?
headers
和rawHeaders
(对于无效/重复的标题)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
}
})
);
};
现在,我们还记录错误和中止。
响应完成后,日志处理程序也会被删除,并且所有日志记录都会移至额外的函数。
目前,该脚本仅将其日志写入控制台,在许多情况下,这已经足够了,因为操作系统允许其他程序捕获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 将日志数据发送到日志服务。日志记录函数本身采用request
andresponse
对象。
Node.js 中的日志记录并不像人们想象的那么简单,尤其是在 HTTP 环境中。JavaScript 异步处理许多事情,因此我们需要挂接到正确的事件,否则,我们不知道发生了什么。
幸运的是,已经有很多日志库可用,所以我们不必自己编写一个。
以下是其中几点:
文章来源:How we built a Node.js Middleware to Log HTTP API Requests and Responses