
Node.js 后端開發(fā)指南:搭建、優(yōu)化與部署
如果我們向http://localhost:8888發(fā)送請求,我們會看到一個巨大的對象被記錄到stdout
,它充滿了實現(xiàn)細節(jié),找到重要的部分并不容易。
讓我們看一下IncomingMessage的 Node.js 文檔,我們的請求是該類的一個對象。
我們可以在這里找到什么信息?
headers
和rawHeaders
(對于無效/重復(fù)的標題)httpVersion
method
url
socket.remoteAddress
(針對客戶端 IP)對于 GET 請求來說這應(yīng)該足夠了,因為它們通常沒有主體。讓我們更新我們的實現(xiàn)。
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();
});
輸出應(yīng)如下所示:
{
"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 作為格式,因此它是一種結(jié)構(gòu)化的記錄方法,并且具有時間戳,因此我們不僅知道誰請求了什么,還知道請求何時開始。
如果我們想要添加有關(guān)請求處理時間的數(shù)據(jù),我們需要一種方法來檢查它何時完成。
當我們發(fā)送完響應(yīng)時,請求就完成了,所以我們必須檢查何時response.end()
調(diào)用了。在我們的示例中,這相當簡單,但有時這些結(jié)束調(diào)用是由其他模塊完成的。
為此,我們可以查看ServerResponse類的文檔。它提到finish
當所有服務(wù)器完成發(fā)送響應(yīng)時觸發(fā)一個事件。這并不意味著客戶端收到了所有內(nèi)容,但它表明我們的工作已完成。
讓我們更新我們的代碼!
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);
};
我們將請求的處理過程傳遞給一個單獨的函數(shù),以模擬負責處理該請求的其他模塊setTimeout
。由于 ,處理過程是異步進行的,因此同步日志記錄無法獲得所需的結(jié)果,但事件會在調(diào)用后finish
觸發(fā)來處理此問題。 response.end()
請求主體仍然未被記錄,這意味著 POST、PUT 和 PATCH 請求沒有 100% 覆蓋。
為了將主體也放入日志中,我們需要一種方法將其從請求對象中提取出來。
IncomingMessage類實現(xiàn)了ReadableStream接口。它使用該接口的事件來發(fā)出來自客戶端的主體數(shù)據(jù)到達的信號。
data
當服務(wù)器從客戶端收到新的數(shù)據(jù)塊時觸發(fā)此事件end
當所有數(shù)據(jù)都已發(fā)送時,將調(diào)用此事件error
出現(xiàn)錯誤時調(diào)用此事件讓我們更新代碼:
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);
});
這樣,當出現(xiàn)問題時,我們會記錄額外的錯誤消息,并將正文內(nèi)容添加到日志中。
注意:正文可能非常大和/或二進制,因此需要進行驗證檢查,否則數(shù)據(jù)量或編碼可能會弄亂我們的日志。
現(xiàn)在我們已經(jīng)收到了請求,下一步就是記錄我們的回應(yīng)。
我們已經(jīng)監(jiān)聽了finish
響應(yīng)事件,因此我們有一個相當安全的方法來獲取所有數(shù)據(jù)。我們只需提取響應(yīng)對象所包含的內(nèi)容即可。
讓我們看一下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
觸發(fā)響應(yīng)事件時進行記錄,如果響應(yīng)出現(xiàn)問題或客戶端中止請求,則不會進行記錄。
對于這兩種情況,我們需要創(chuàng)建額外的處理程序。
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
}
})
);
};
現(xiàn)在,我們還記錄錯誤和中止。
響應(yīng)完成后,日志處理程序也會被刪除,并且所有日志記錄都會移至額外的函數(shù)。
目前,該腳本僅將其日志寫入控制臺,在許多情況下,這已經(jīng)足夠了,因為操作系統(tǒng)允許其他程序捕獲stdout
并對其執(zhí)行操作,例如寫入文件或?qū)⑵浒l(fā)送到第三方 API(如 Moesif)。
console.log
在某些環(huán)境中,這是不可能的,但由于我們將所有信息聚集到一個地方,我們可以用第三方函數(shù)替換調(diào)用。
讓我們重構(gòu)代碼,使其類似于一個庫并記錄到某些外部服務(wù)。
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);
};
};
};
經(jīng)過這些更改,我們現(xiàn)在可以像使用Moesif-Express一樣使用我們的日志記錄實現(xiàn)。
該loggingLibrary
函數(shù)以 API 密鑰作為配置,并返回實際的日志記錄函數(shù),該函數(shù)將通過 HTTP 將日志數(shù)據(jù)發(fā)送到日志服務(wù)。日志記錄函數(shù)本身采用request
andresponse
對象。
Node.js 中的日志記錄并不像人們想象的那么簡單,尤其是在 HTTP 環(huán)境中。JavaScript 異步處理許多事情,因此我們需要掛接到正確的事件,否則,我們不知道發(fā)生了什么。
幸運的是,已經(jīng)有很多日志庫可用,所以我們不必自己編寫一個。
以下是其中幾點:
文章來源:How we built a Node.js Middleware to Log HTTP API Requests and Responses