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

server.listen(8888);

如果我們向http://localhost:8888發(fā)送請(qǐng)求,我們會(huì)看到一個(gè)巨大的對(duì)象被記錄到stdout,它充滿了實(shí)現(xiàn)細(xì)節(jié),找到重要的部分并不容易。

讓我們看一下IncomingMessage的 Node.js 文檔,我們的請(qǐng)求是該類的一個(gè)對(duì)象。

我們可以在這里找到什么信息?

對(duì)于 GET 請(qǐng)求來(lái)說(shuō)這應(yīng)該足夠了,因?yàn)樗鼈兺ǔ](méi)有主體。讓我們更新我們的實(shí)現(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": "/"
}

我們僅使用請(qǐng)求的特定部分進(jìn)行記錄。它使用 JSON 作為格式,因此它是一種結(jié)構(gòu)化的記錄方法,并且具有時(shí)間戳,因此我們不僅知道誰(shuí)請(qǐng)求了什么,還知道請(qǐng)求何時(shí)開始。

記錄處理時(shí)間

如果我們想要添加有關(guān)請(qǐng)求處理時(shí)間的數(shù)據(jù),我們需要一種方法來(lái)檢查它何時(shí)完成。

當(dāng)我們發(fā)送完響應(yīng)時(shí),請(qǐng)求就完成了,所以我們必須檢查何時(shí)response.end()調(diào)用了。在我們的示例中,這相當(dāng)簡(jiǎn)單,但有時(shí)這些結(jié)束調(diào)用是由其他模塊完成的。

為此,我們可以查看ServerResponse類的文檔。它提到finish當(dāng)所有服務(wù)器完成發(fā)送響應(yīng)時(shí)觸發(fā)一個(gè)事件。這并不意味著客戶端收到了所有內(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);
};

我們將請(qǐng)求的處理過(guò)程傳遞給一個(gè)單獨(dú)的函數(shù),以模擬負(fù)責(zé)處理該請(qǐng)求的其他模塊setTimeout。由于 ,處理過(guò)程是異步進(jìn)行的,因此同步日志記錄無(wú)法獲得所需的結(jié)果,但事件會(huì)在調(diào)用finish觸發(fā)來(lái)處理此問(wèn)題。 response.end()

記錄身體

請(qǐng)求主體仍然未被記錄,這意味著 POST、PUT 和 PATCH 請(qǐng)求沒(méi)有 100% 覆蓋。

為了將主體也放入日志中,我們需要一種方法將其從請(qǐng)求對(duì)象中提取出來(lái)。

IncomingMessage類實(shí)現(xiàn)了ReadableStream接口。它使用該接口的事件來(lái)發(fā)出來(lái)自客戶端的主體數(shù)據(jù)到達(dá)的信號(hà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);
});

這樣,當(dāng)出現(xiàn)問(wèn)題時(shí),我們會(huì)記錄額外的錯(cuò)誤消息,并將正文內(nèi)容添加到日志中。

注意:正文可能非常大和/或二進(jìn)制,因此需要進(jìn)行驗(yàn)證檢查,否則數(shù)據(jù)量或編碼可能會(huì)弄亂我們的日志。

記錄響應(yīng)數(shù)據(jù)

現(xiàn)在我們已經(jīng)收到了請(qǐng)求,下一步就是記錄我們的回應(yīng)。

我們已經(jīng)監(jiān)聽了finish響應(yīng)事件,因此我們有一個(gè)相當(dāng)安全的方法來(lái)獲取所有數(shù)據(jù)。我們只需提取響應(yīng)對(duì)象所包含的內(nèi)容即可。

讓我們看一下ServerResponse類的文檔來(lái)了解它為我們提供了什么。

讓我們將其添加到我們的代碼中。

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);
});

處理響應(yīng)錯(cuò)誤和客戶端中止

目前,我們僅在finish觸發(fā)響應(yīng)事件時(shí)進(jìn)行記錄,如果響應(yīng)出現(xiàn)問(wèn)題或客戶端中止請(qǐng)求,則不會(huì)進(jìn)行記錄。

對(duì)于這兩種情況,我們需要?jiǎng)?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)在,我們還記錄錯(cuò)誤和中止。

響應(yīng)完成后,日志處理程序也會(huì)被刪除,并且所有日志記錄都會(huì)移至額外的函數(shù)。

記錄到外部 API

目前,該腳本僅將其日志寫入控制臺(tái),在許多情況下,這已經(jīng)足夠了,因?yàn)椴僮飨到y(tǒng)允許其他程序捕獲stdout并對(duì)其執(zhí)行操作,例如寫入文件或?qū)⑵浒l(fā)送到第三方 API(如 Moesif)。

console.log在某些環(huán)境中,這是不可能的,但由于我們將所有信息聚集到一個(gè)地方,我們可以用第三方函數(shù)替換調(diào)用。

讓我們重構(gòu)代碼,使其類似于一個(gè)庫(kù)并記錄到某些外部服務(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)過(guò)這些更改,我們現(xiàn)在可以像使用Moesif-Express一樣使用我們的日志記錄實(shí)現(xiàn)。

loggingLibrary函數(shù)以 API 密鑰作為配置,并返回實(shí)際的日志記錄函數(shù),該函數(shù)將通過(guò) HTTP 將日志數(shù)據(jù)發(fā)送到日志服務(wù)。日志記錄函數(shù)本身采用requestandresponse對(duì)象。

結(jié)論

Node.js 中的日志記錄并不像人們想象的那么簡(jiǎn)單,尤其是在 HTTP 環(huán)境中。JavaScript 異步處理許多事情,因此我們需要掛接到正確的事件,否則,我們不知道發(fā)生了什么。

幸運(yùn)的是,已經(jīng)有很多日志庫(kù)可用,所以我們不必自己編寫一個(gè)。

以下是其中幾點(diǎn):

文章來(lái)源:How we built a Node.js Middleware to Log HTTP API Requests and Responses

上一篇:

動(dòng)手做一個(gè)最小RAG——TinyRAG

下一篇:

REST API 的 CORS(跨源資源共享)權(quán)威指南
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)