Node 中異常收集與監控

在一個後端服務設計中,異常捕獲是必不可少須要考慮的因素javascript

而當異常發生時,可以第一時間捕捉到而且可以得到足夠的信息定位到問題相當重要 這也是本篇文章的內容前端

剛開始,先拋出兩個問題java

  1. 在生產環境中後端鏈接的數據庫掛了,是否可以第一時間收到通知並定位到問題,而不是等到用戶反饋以後又用了半天時間才找到問題 (雖然運維確定會在第一時間知道數據庫掛了)
  2. 在生產環境中有一條 API 出了問題,可否衡量該錯誤的緊急重要程度,並根據報告解決問題

本文連接:node

異常收集

異常通常發生在如下幾個位置docker

  1. 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 來保持上下文信息數據庫

  2. script 等非 API 層,如拉取配置,數據庫遷移腳本以及計劃任務等後端

另外除了主動捕捉到的異常,還有一些可能漏掉的異常,可能致使程序退出api

process.on('uncaughtException', (err) => {
  console.error('uncaught', err)
})

process.on('unhandledRejection', (reason, p) => {
  console.error('unhandle', reason, p)
})
複製代碼

API 異常結構化

當 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 各字段的示意

code

表示錯誤標識碼,用以對錯誤進行歸類,如用戶輸入數據不合法 (ValidationError),數據庫異常 (DatabaseError),外部服務請求失敗 (RequestError)

根據經驗我把 code 分爲如下幾類

  • ValidationError,用戶輸入不合法
  • DatabaseError,數據庫問題
    • DatabaseUniqueError
    • DatabaseConnectionError
    • ...
  • RequestError,外部服務
    • RequestTimeoutError
  • ForbiddenError
  • AuthError,未受權請求受權資源
  • AppError,業務問題
    • AppBadRequest
    • ...
  • ...

對於數據校驗,數據庫異常與請求失敗,咱們一般會使用第三方庫。 此時能夠根據第三方庫的 Error 來定製 code

message

表示 human-readable 的錯誤信息,但不必定表明它能夠展現在前端。這裏的 human 表明的是開發人員,而非用戶,如如下兩個 message 就不適宜展現在前端

  1. connect ECONNREFUSED postgres.xiange.tech. 不須要把數據庫斷連的消息展現在前端
  2. email is required. 輸入數據校驗,雖然能夠展現給用戶,可是須要展現中文 (國際化)

你能夠根據 code,來決定前端是否能夠展現後端期待它展現的信息,而在前端也能夠根據 code 來進行全局集中處理

info

表示一些針對 code 的更爲詳細的信息

  • 當發送請求失敗的時候,你至少得知道失敗的這個請求長什麼樣子: method,params/body 以及 headers
  • 當用戶輸入數據校驗失敗時,至少得知道是那幾個字段

originalError

originalError 表示由該異常引起的錯誤 API,它每每會包含更加詳細的上下文信息

originalError.stack 表示當前錯誤的堆棧,當異常發生時能夠快速定位問題發生的位置 (雖然 node 有時候拋出的堆棧信息都是本身從未見過的文件)

當在開發和測試環境時,把 originalError 附到 API 中能夠快速定位問題, 當在生產環境時,不要把你的 originalError 也放到 API 裏,你能夠在監控系統中找到完整的錯誤信息

你可使用如下兩個 API 來優化你的 stacktrace

Error.captureStackTrace(error, constructorOpt)
Error.prepareStackTrace(error, structuredStackTrace)
複製代碼

具體使用方法能夠參考 v8 stack trace api

data

表明該接口返回的數據。當 API 報錯時 data 是否是應該返回爲 null

不該該,當 API 報錯時,可能只有部分字段有問題,剩餘字段能夠正常返回。 因爲 graphql 是由字段(field)聚合而成,這在 graphql 中體現地很是明顯。

http status

當API處理過程當中發生錯誤時,應該返回 400+ 的 status code

  • HTTP/1.1 400 Bad Request
  • HTTP/1.1 401 Unauthorized
  • HTTP/1.1 403 Forbidden
  • HTTP/1.1 429 Too Many Requests

監控系統

監控首先須要有一個監控系統,我這裏比較推薦 Sentry,具體如何部署能夠參考個人上一篇文章:如何部署 Sentry

你也能夠直接在官方註冊使用 SaaS 版本: Sentry 付費。我的免費版每月有 5K 的報錯限額,也足夠我的使用。

相比自建版本,使用 SaaS 免了一些運維的平常工做。最主要的是, 自建系統有功能限制

這裏有關於 Sentry 的文檔

指標

異常監控除了異常自己之外,還要採集更多一些的指標。

異常監控最重要的目標就是還原異常拋出場景

  1. 異常級別: Fatel, Error 以及 Warn。這決定你週日收到報警郵件或報警短信是繼續浪仍是打開筆記本改 Bug。能夠經過 code 來標記

    const codeLevelMap = {
      ValidationError: 'warn',
      DatabaseError: 'error'
    }
    複製代碼
  2. 環境: 生產環境仍是測試環境,早於用戶及測試發現問題,能夠直接讀取應用服務的環境變量

  3. 上下文: 如哪一條 API 請求,哪個用戶,以及更詳細的 http 報文信息。能夠直接利用 Sentry 的API上報上下文信息

    Sentry.configureScope(scope => {
      scope.addEventProcessor(event => Sentry.Handlers.parseRequest(event, ctx.request))
    })
    複製代碼
  4. 用戶: API 錯誤是由那個用戶觸發的

  5. code: 便於對錯誤進行分類

  6. 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 報文以及環境變量

Filter

在本地開發時,每每不須要把異常上報到 SentrySentry 也提供了 hook 再上報以前對異常進行過濾

beforeSend?(event: Event, hint?: EventHint): Promise<Event | null> | Event | null;
複製代碼

歡迎關注個人公衆號山月行,在這裏記錄着個人技術成長,歡迎交流

歡迎關注公衆號山月行,在這裏記錄個人技術成長,歡迎交流
相關文章
相關標籤/搜索