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

server.listen(8888);

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

讓我們看一下IncomingMessage的 Node.js 文檔,我們的請求是該類的一個對象。

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

對于 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ù)到達的信號。

讓我們更新代碼:

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ù)量或編碼可能會弄亂我們的日志。

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

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

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

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

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

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)錯誤和客戶端中止

目前,我們僅在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ù)。

記錄到外部 API

目前,該腳本僅將其日志寫入控制臺,在許多情況下,這已經(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ù)本身采用requestandresponse對象。

結(jié)論

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

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

以下是其中幾點:

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

上一篇:

動手做一個最小RAG——TinyRAG

下一篇:

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

我們有何不同?

API服務(wù)商零注冊

多API并行試用

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

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

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

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費