在一個後端服務設計中,異常捕獲是必不可少須要考慮的因素javascript
而當異常發生時,可以第一時間捕捉到而且可以得到足夠的信息定位到問題相當重要 這也是本篇文章的內容前端
剛開始,先拋出兩個問題java
本文連接:node
異常通常發生在如下幾個位置docker
API/GraphQL 層,在 API 層的最外層使用一箇中間件對錯誤集中進行處理,並進行上報。在具體邏輯層每每不須要主動捕捉異常,除非針對異常有特殊處理,如數據庫事務失敗後的回退typescript
// 在中間件中集中處理異常
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.body = formatError(err)
}
})
// graphql 中也能夠
new ApolloServer({
typeDefs,
resolvers,
formatError
})
複製代碼
當在邏輯層捕捉到異常再手動拋出時,不要丟失上下文信息,此時可使用添加字段 originalError 來保持上下文信息數據庫
script 等非 API 層,如拉取配置,數據庫遷移腳本以及計劃任務等後端
另外除了主動捕捉到的異常,還有一些可能漏掉的異常,可能致使程序退出api
process.on('uncaughtException', (err) => {
console.error('uncaught', err)
})
process.on('unhandledRejection', (reason, p) => {
console.error('unhandle', reason, p)
})
複製代碼
當 API 層發生異常時,傳輸數據至客戶端時須要對異常進行結構化,方便下一步的異常上報與前端對結構化信息的解析以及對應的展現app
如下使用 FormatError
表示當發生異常時 API 應該返回的結構化的信息
而當異常發生時,異常能夠在最頂層中間件做爲錯誤處理中間件統一捕獲,捕獲到時可使用一個函數 formatError
在中間件中統一結構化異常信息
type FormatError {
code: string;
message: string;
info: any;
data: Record<string, any> | null;
originalError?: Error;
}
function formatError (error: Error): FormatError;
複製代碼
如下是 FormatError
各字段的示意
表示錯誤標識碼,用以對錯誤進行歸類,如用戶輸入數據不合法 (ValidationError),數據庫異常 (DatabaseError),外部服務請求失敗 (RequestError)
根據經驗我把 code 分爲如下幾類
對於數據校驗,數據庫異常與請求失敗,咱們一般會使用第三方庫。 此時能夠根據第三方庫的 Error 來定製 code
表示 human-readable 的錯誤信息,但不必定表明它能夠展現在前端。這裏的 human 表明的是開發人員,而非用戶,如如下兩個 message 就不適宜展現在前端
你能夠根據 code,來決定前端是否能夠展現後端期待它展現的信息,而在前端也能夠根據 code 來進行全局集中處理
表示一些針對 code 的更爲詳細的信息
originalError 表示由該異常引起的錯誤 API,它每每會包含更加詳細的上下文信息
originalError.stack
表示當前錯誤的堆棧,當異常發生時能夠快速定位問題發生的位置 (雖然 node 有時候拋出的堆棧信息都是本身從未見過的文件)
當在開發和測試環境時,把 originalError 附到 API 中能夠快速定位問題, 當在生產環境時,不要把你的 originalError 也放到 API 裏,你能夠在監控系統中找到完整的錯誤信息
你可使用如下兩個 API 來優化你的 stacktrace
Error.captureStackTrace(error, constructorOpt)
Error.prepareStackTrace(error, structuredStackTrace)
複製代碼
具體使用方法能夠參考 v8 stack trace api
表明該接口返回的數據。當 API 報錯時 data 是否是應該返回爲 null
?
不該該,當 API 報錯時,可能只有部分字段有問題,剩餘字段能夠正常返回。 因爲 graphql
是由字段(field)聚合而成,這在 graphql
中體現地很是明顯。
當API處理過程當中發生錯誤時,應該返回 400+ 的 status code
監控首先須要有一個監控系統,我這裏比較推薦 Sentry
,具體如何部署能夠參考個人上一篇文章:如何部署 Sentry。
你也能夠直接在官方註冊使用 SaaS 版本: Sentry 付費。我的免費版每月有 5K 的報錯限額,也足夠我的使用。
相比自建版本,使用 SaaS 免了一些運維的平常工做。最主要的是, 自建系統有功能限制。
這裏有關於 Sentry
的文檔
異常監控除了異常自己之外,還要採集更多一些的指標。
異常監控最重要的目標就是還原異常拋出場景
異常級別: Fatel, Error 以及 Warn。這決定你週日收到報警郵件或報警短信是繼續浪仍是打開筆記本改 Bug。能夠經過 code 來標記
const codeLevelMap = {
ValidationError: 'warn',
DatabaseError: 'error'
}
複製代碼
環境: 生產環境仍是測試環境,早於用戶及測試發現問題,能夠直接讀取應用服務的環境變量
上下文: 如哪一條 API 請求,哪個用戶,以及更詳細的 http 報文信息。能夠直接利用 Sentry 的API上報上下文信息
Sentry.configureScope(scope => {
scope.addEventProcessor(event => Sentry.Handlers.parseRequest(event, ctx.request))
})
複製代碼
用戶: API 錯誤是由那個用戶觸發的
code: 便於對錯誤進行分類
request_id: 便於 tracing,也方便獲取更多的調試信息:在 elk 中查找當前 API 執行的 SQL 語句
const requestId = ctx.header['x-request-id'] || Math.random().toString(36).substr(2, 9)
Sentry.configureScope(scope => {
scope.setTag('requestId', requestId)
})
複製代碼
由上可見,對於採集指標的數據通常來源於兩個方面,http 報文以及環境變量
在本地開發時,每每不須要把異常上報到 Sentry
。Sentry
也提供了 hook 再上報以前對異常進行過濾
beforeSend?(event: Event, hint?: EventHint): Promise<Event | null> | Event | null;
複製代碼
歡迎關注個人公衆號山月行,在這裏記錄着個人技術成長,歡迎交流