從 Node.js 錯誤中獲得的經驗
有多少次你發現自己在終端或監控系統內查看堆棧軌跡,但并不能看出個所以然來?如果你的回答是“很多次”,那么這篇帖子你應該看看。如果你不經常碰上這種情況也沒關系,你也可以看看這篇文章解悶。
當處理 Node.js 服務器的復雜數據時,要會從可返回給請求方的錯誤中總結經驗,具備此能力至關重要。在處理一個請求時,一個錯誤出現會引起鏈接里另一個錯誤的出現,于是問題就來了。當此腳本出現時,一旦你生成了新錯誤,并將它返回到了鏈接,那你就丟失了與原始錯誤的所有連接。
在 Codefresh ,我們花費了大量的時間試圖找到最好的模式來處理這些情境。我們真正想要的是能夠讓一個錯誤能鏈接到前一個錯誤,有能力獲取整個鏈的聚合信息。我們想要這樣的接口來做界面將非常簡單,也更利于擴展與改進。
我們探索已經存在的模塊以支持我們的需要。我們發現了唯一滿足要求的模塊就是 WError 。
‘WError’ 提供給你包裝已存錯誤和新錯誤的能力。接口是非常酷和簡單的,因此我們決定試一試。經過一段時間的密集使用,我們得出了一些做得還不太好的地方:
-
堆棧跟蹤的誤差不會超過整個鏈,而只會生成堆棧跟蹤高的錯誤。
-
它沒有能力去簡單地創建你自己的錯誤類型。
-
擴展錯誤的附加行為,需要擴展他們的代碼。
介紹 CFError
用一個真實的 Express 示例來看看如何使用 CFError。創建一個 Express 應用,它通過一個特定的路由處理某個單獨的請求。這個請求需要從 Mongo 數據庫中查詢一個用戶的信息。現在定義一個路由,以及一個負責從 DB 中獲取用戶信息的函數。
var CFError = require('cf-errors');
var Errors = CFError.Errors;
var Q = require('q');
var express = require('express');
var UserNotFoundError = {
name: "UserNotFoundError"
};
var app = express();
app.get('/user/:id', function (request, response, next) {
var userId = request.params.id;
if (userId !== "coolId") {
return next(new CFError(Errors.Http.BadRequest, {
message: "Id must be coolId.",
internalCode: 04001,
recognized: true
}));
}
findUserById(userId)
.done((user) => {
response.send(user);
}, (err) => {
if (err.name === UserNotFoundError.name) {
next(new CFError(Errors.Http.NotFound, {
internalCode: 04041,
cause: err,
message: `User ${userId} could not be found`,
recognized: true
}));
}
else {
next(new CFError(Errors.Http.InternalServer, {
internalCode: 05001,
cause: err
}));
}
});
});
var findUserById = function (userId) {
return User.findOne({_id: userId})
.exec((user) => {
if (user) {
return user;
}
else {
return Q.reject(new CFError(UserNotFoundError, `Failed to retrieve user: ${userId}`));
}
})
};
有幾件事情需要注意:
-
創建錯誤信息的時候,可以從預定義的 HTTP 錯誤擴展。
-
創建錯誤信息的時候可以加入一個 ‘cause’ 屬性,用來連接到前一個錯誤,形成錯誤鏈。這樣在打印錯誤相關聯的調用棧時,就能得到完整錯誤鏈的調用棧信息,使之在閱讀時容易識別。
-
你可以在錯誤信息對象中添加各種字段。稍后我會解釋 ‘internalCode’ 和 ‘recognized’ 的用法。
-
可以在你的代碼之外定義錯誤對象,然后在創建錯誤信息的時候引用它。
下面,給 Express 應用添加一個錯誤處理中間件。
app.use(function (err, request, response, next) {
var error;
if (!(err instanceof CFError)){
error = new CFError(Errors.Http.InternalServer, {
cause: err
});
}
else {
if (!err.statusCode){
error = new CFError(Errors.Http.InternalServer, {
cause: err
});
}
else {
error = err;
}
}
console.error(error.stack);
return response.status(error.statusCode).send(error.message);
});
注意:
-
確保將最后出現的的錯誤輸出到日志,以及總是將 ‘CFError’ 對象返回給用戶。這樣你才能向錯誤處理中間件中添加其它邏輯。
-
所有預定義的 HTTP 錯誤都有 ‘statusCode’ 屬性和 ‘message’ 屬性,它們都是和的屬性。
-
擴展錯誤信息使你可以在一個地方添加處理錯誤的邏輯。創建錯誤信息的時候不用考慮去打印每一個錯誤對象,之后可以一次性打印調用棧的跟蹤信息,它包含了完全的執行過程和上下文數據。
現在換個方法向客戶端返回錯誤,返回一個對象來代替頂層的錯誤消息。
return response.status(error.statusCode).send({
message: error.message,
statusCode: error.statusCode,
internalCode: error.internalCode
});
非常好!現在我們有一個的向客戶端返回錯誤的過程了。
向監控器(監控進程)通報錯誤信息
在Codefresh中,我們使用NewRelic作為APM監控器。要注意我們生成并觸發到NewRelic的錯誤信息分為兩類:第一類包含了在我們服務器上因不當操作而產生和拋出的各種錯誤信息。另一類則是我們的服務器正確分析處理產生的異常部分的各種錯誤信息(業務異常)。
向NewRelic報告第二類錯誤時會造成Apdex積分不可預測地下降,這又導致各種來自我們告警系統的虛假告警消息。
所以我們給出了一種新的約定,當我們可將一個生成的錯誤歸納為系統正確行為的結果時,我們構造一個錯誤對象并為之附加一個recognized字段。我們想要具備一種能力可在錯誤鏈條上的某一錯誤打上recognized標記,但仍能獲取它的值,即使更高層級的錯誤沒有包含這個標記。我們在CFError對象上暴露了一個getFirstValue函數,用來取得它在整個錯誤鏈條上碰到的第一個值。我們用下面代碼看看在Codefresh中是如何使用的。
app.use(function (err, request, response, next) {
var error;
if (!(err instanceof CFError)){
error = new CFError(Errors.Http.InternalServer, {
cause: err
});
}
else {
if (!err.statusCode){
error = new CFError(Errors.Http.InternalServer, {
cause: err
});
}
else {
error = err;
}
}
if (!error.getFirstValue('recognized')){
nr.noticeError(error); //report to monitoring systems (newrelic in our case)
}
console.error(error.stack);
return response.status(error.statusCode).send({
message: error.message,
statusCode: error.statusCode,
internalCode: error.internalCode
});
});
注意:
-
因為我們知道只需要處理 CFError 對象,所以只需要添加兩行代碼就行。
-
既然已經確定了實際要發送的錯誤,使用 New Relic 的時候就要手工關閉自動發送錯誤的選項。為了達到這個目的,需要手工將所有 HTTP 錯誤狀態碼加入 ‘newrelic.js’ 中的 ‘ignore_status_codes’ 字段。我們已經向 New Relic 支持團隊提出需要一個更簡單的辦法來做這個事情。
exports.config = {
error_collector: {
ignore_status_codes: [400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511]
}
};
小結
想很好地處理錯誤不僅需要一個好的錯誤處理模塊,還需要定義好處理過程:在什么時候、什么位置用什么方法來處理錯誤。這需要你遵循自己的設計模式,否則就會搞得一團糟。
僅向監控系統報告實際的錯誤是至關重要的,這樣你的公司才能專注于檢查和處理發生的問題。
來自:https://www.oschina.net/translate/getting-the-most-out-of-your-nodejs-errors