什麼是好的錯誤訊息?討論一下Java系統中的錯誤碼設計

一  什麼是好的錯誤資訊(Error Message)?

一個好的Error Message主要包含三個部分:
  • Context:  什麼導致了錯誤?發生錯誤的時候程式碼想做什麼?
  • The error itself:  到底是什麼導致了失敗?具體的原因和當時的資料是什麼?
  • Mitigation: 有什麼解決方案來克服這個錯誤,也可以理解為 Solutions
聽起來還是有點抽象,能否給點程式碼? 剛好有一個 jdoctor 的專案,作者來自Oracle Labs[1]  樣例程式碼如下:
ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza") .withLongDescription("Pineapple on pizza would put your relationship with folks you respect at risk.") .withShortDescription("pineapple on pizza isn't allowed") .because("the Italian cuisine should be respected") .documentedAt("https://www.bbc.co.uk/bitesize/articles/z2vftrd") .addSolution(s -> s.withShortDescription("eat pineapple for desert")) .addSolution(s -> s.withShortDescription("stop adding pineapple to pizza"));
這裡的Problem理解為Error沒有問題,核心主要包括以下幾個欄位:
  • context: such as app name, component, status code,使用一個字串描述當時的上下文,如應用名稱 + 元件名稱 +具體的錯誤狀態碼等,這個由你自己決定,當然JSON字串也可以,如 {"app":"uic", "component": "login", "code":"111"}
  • description: Long(Short) to describe error 錯誤描述,有Long和Short兩者
  • because/reason:  explain the reason with data 詳細解釋錯誤的原因,當然必須包含相應的資料
  • documentedAt: error link 錯誤對應的HTTP連線,更詳細地介紹該錯誤
  • solutions: possible solutions 可能的解決方案,如提示訪問者檢查email拼寫是否正確,簡訊的Pass Code是否輸入正確等。
有了這些具體的欄位後,我們理解起來就方便多啦。

二  錯誤碼(Error Code)的設計

各種錯誤處理上都建議使用錯誤碼,錯誤碼有非常多的優勢:唯一性、搜尋/統計更方便等,所以我們還是要討論一下錯誤碼的設計。網上也有不少錯誤碼的設計規範,當然這篇文章也少不了重複造輪子,該設計提供給大家參考,大家自行判斷啊,當然也非常歡迎留言指正。
一個錯誤碼通常包含三個部分:
  • System/App short name: 系統或者應用的名稱,如 RST, OSS等。如果你熟悉Jira的話,基本也是這個規範,Java程式設計師應該都知道HHH和SPR代表什麼吧? 
  • Component short name or code: 系統內部的元件名稱或者編碼,如LOGIN,  AUDIT,001 這些都可以,方便更快地定位錯誤。
  • Status code: 錯誤的狀態碼,這個是一個三位數字的狀態碼,如200,404,500,主要是借鑑自 HTTP Status Code,畢竟絕大多數開發者都瞭解HTTP狀態碼,我們沒有必要再重新設計。
有了上述的規範後,讓我們看一下典型的錯誤編碼長什麼樣子:
  • OSS-001-404:  你應該知道是OSS的某一元件報告資源沒有找到吧
  • RST-002-500:這個是一個元件的內部錯誤
  • UIC-LOGIN-404:這個應該是會員登入時查詢不到指定的賬號
我們採用應用名縮寫, 元件名或者編碼, 狀態值,然後以中劃線連線起來。中劃線比較方便閱讀,下劃線有時候在顯示的時候理解為空格。同時有了標準的HTTP Status Code支援,不用參考文件,你都能猜一個八九不離十。 錯誤碼設計千萬不要太複雜,試圖將所有的資訊都新增進去,當然資訊非常全,但是也增加了開發者理解和使用成本,這個可能要做一個取捨,當然我也不是說目前這種一鍵三連(打賞、點贊加轉發)的結構就最合理,你也可以自行調整。有沒有做心裡研究的同學來說一下,這種三部分組成的方式,是不是最符合人們的認知習慣?如果超過三部分,如4和5,人們能記住和使用的機率是不是就下降的非常多?
還記得前面說的error的context嗎?這裡error code其實就是啟動context的作用,如 UIC-LOGIN-404,錯誤發生在哪裡?錯誤碼幫你定位啦。當時程式碼想幹什麼?錯誤碼也說明啦。雖然說錯誤碼不能完全代表錯誤的上下文,但是其承載的資訊已經足夠我們幫我們瞭解當時的上下文啦,所以這裡error code就是起著context的作用。目前看來至少error code要比 ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza") 中的Hawaiian pizza 作為context更具有說服力,也規範一些。

三  錯誤訊息的編寫格式

錯誤碼設計完畢後,我們還不能用錯誤碼+簡短訊息方式輸出錯誤,不然就出現類似 ORA-00942: table or view does not exist這種情況,你一定會吐槽:"你為何不告訴哪個表或者view?"。所以我們還需要設計一個message格式,能夠將錯誤的context, description, reason, document link, solutions全部包含進來,這樣對開發者會比較友好。這裡我擬定了一個Message的規範,當然大家可以發表自己的意見啊,如下:
longdescription(short desc): because/reason --- document link -- solutions
解釋一下:
  • 錯誤的長描述直接書寫,短描述使用括弧進行包含。這種寫法在合同中非常常見,如阿里雲計算有限公司(阿里雲)  ,你簽署勞動合同時,公司的稱謂基本也是全名(代稱) 這種方式。好多同學會在錯誤日誌中書寫登入失敗,但是登入系統中有多種登入方式,所以遠不如Failed to log in with email and password(Login Failed), Failed to log in with phone and passcode(Login Failed), Failed to log in with oauth2(Login Failed) 更清晰。
  • 錯誤具體原因:  接下來是冒號,然後書寫詳細的原因,如 email [email protected] not found ,gender field is not allowed in package.json 一定要包含具體的資料資訊,包括輸入的,還是和勞動合同一樣,抬頭之後就是你的具體崗位和薪水,雖然合同是格式化的,但是每一個人具體的崗位和薪水是不同的,這些引數都是從外部獲取的。此處有安全同學發問,如何資料脫敏?這個是另外的問題,大多數開發者應該瞭解如何進行mask,這裡我們就跳過。當出現勞動糾紛這個錯誤時,具體原因中的資料,如崗位和薪水等,這樣勞動仲裁局就可以快速定位並解決該"錯誤"。
  • document link: 接下來我們使用三種劃線進行分隔,輸入對應的error link。三劃線作為分隔符在很多的場景中多有使用,如mdx, yaml等,大家不會太陌生。 如果沒有link那就忽略就可以。
  • solutions:自然的文字表述即可,能說明清楚就可以,也是放在三中劃線後。
看一個具體的訊息格式例子:
APP-100-400=Failed to log in system with email andpassword(Email login failed): can not find accountwith email {} --- please refer https://example.com/login/byemail --- Solutions: 1. check your email 2. check your password
上述的APP-100-400的錯誤碼對應的描述基本覆蓋到jdoctor中需要的資訊,可以說對一個錯誤的描述應該非常全啦,而且有一定的格式,也方便後續的日誌分析。

四  組裝和儲存錯誤碼 + Message

有了錯誤碼和message的規範,接下來我們應該如何儲存這些資訊呢?如果是Java,是不是要建立對應的ErrorEnum,然後是一些POJO?這裡個人建議使用properties檔案來儲存錯誤碼和message的資訊。檔名可以直接為ErrorMessages.properties,當然是在某一package下,檔案樣例如下:
### error messages for your AppAPP-100-400=Failed to log in system with email andpassword(Email login failed): can not find accountwith email {0} --- please refer https://example.com/login/byemail --- Solutions: 1. check your email 2. check your passwordAPP-100-401=Failedtologinsystemwith phone and pass(Phone login failed): can not find accountwith phone {0} --- please refer https://example.com/login/byphone --- Solutions: 1. check your phone 2. check your pass code in SMS
為何要選擇properties檔案來儲存error code和message資訊,主要有以下幾個原因:
  • 國際化支援:Java的同學都知道,如果你的錯誤訊息想調整為中文,建立一個ErrorMessages-zh_CN.properties 即可。原文中的建議是Don’t localize error messages,但是考慮到國內大多數程式設計師未必能用英文表達清楚,所以中文也是可以的。題外話:如果中國的程式設計師都能用英文清晰地閱讀文章和表達自己的思想和觀點,我們在計算機方面的水平可能會提升到更高的臺階。
  • 各種語言對properties的檔案解析都有支援,不只是Java,其他語言也有,而且properties檔案本身也不復雜,所以該properties檔案可以給Node.js, Rust等其他語言使用,如果是Java enum和POJO基本就不可能啦。
  • properties檔案格式豐富:支援註釋,換行符,多行轉義等也都沒有問題。
最後最關鍵的是IDE支援非常友好 , 以Java開發者使用的IntelliJ IDEA來說,對Properties檔案的支援可以說是到了極致,如下:
  • error code的自動提示
  • 快速檢視:滑鼠移上去就可以,按下CMD滑鼠移上去也可以, Alt+Space也可以,當然點選直接定位就更不用說啦。
  • 重構和查詢支援:雖然Error Code是字串,但是也是properties的key,所以rename這個error code,所有引用的地方都會rename。還支援find usage,那些地方引用了該error code等,都非常方便。當然如果Error Code在系統中沒有被使用,也會灰色標識。
  • 摺疊自動顯示功能:當你的程式碼處於摺疊狀態時,IDEA直接將message拿過來進行顯示,你在code review的時候方便多啦,也便於你理解程式碼。
  • 直接修改message的值
總之IntellIJ IDEA對properties檔案的支援到了極致,我們也沒有理由不考慮開發者體驗的問題,到處跳來跳去地找錯誤碼,這種傷害程式設計師開發體驗的事情不能做。 當然JetBrains的其他IDE,WebStorm等都有對proproperties檔案編輯支援。

五  程式碼實現

看起來功能挺酷炫的,是不是這種方式錯誤管理要介入一個開發包啊?不需要,你只需要10行程式碼就搞定,如下:
import org.slf4j.helpers.MessageFormatter;publicclassAppErrorMessages {privatestatic final String BUNDLE_FQN = "app.ErrorMessages";privatestatic final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));publicstatic String message(@PropertyKey(resourceBundle = BUNDLE_FQN) String key, Object... params) {if (RESOURCE_BUNDLE.containsKey(key)) { String value = RESOURCE_BUNDLE.getString(key); final FormattingTuple tuple = MessageFormatter.arrayFormat(value, params);return key + " - " + tuple.getMessage(); } else {return MessageFormatter.arrayFormat(key, params).getMessage(); } }}
這樣在任何地方如果你要列印錯誤訊息的時候,這樣log.info(AppErrorMessages.message("APP-100-400","xxx"));就可以。如果你還有想法和log進行一下Wrapper,如 log.info("APP-100-400","xxx");  ,也沒有問題,樣例程式碼如下:
publicclassErrorCodeLoggerimplementsLogger {private Logger delegate;privatestatic final String BUNDLE_FQN = "app.ErrorMessages";privatestatic final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));publicErrorCodeLogger(Logger delegate) {this.delegate = delegate; } @Overridepublicvoidtrace(@PropertyKey(resourceBundle = BUNDLE_FQN) String msg) {delegate.trace(RESOURCE_BUNDLE.getString(msg)); }}
接下來你就可以在log中直接整合error code,非常便捷。上述程式碼我已經寫好,你參考文章末尾的專案地址即可。
最終的日誌輸出如下:
提醒:這裡我們使用了slf4j的MessageFormatter,主要是方便後續的Slf4j的整合,而且slf4j的MessageFormatter比Java的MessageFormat容錯和效能上更好一些。

六  FAQ

1  為何選擇3位的HTTP Status Code作為Error的Status Code?

大多數開發者對HTTP Status Code都比較熟悉,所以看到這些code就大致明白什麼意思,當然對應用開發者也有嚴格的要求,你千萬別將404解釋為內部錯誤,如資料庫連線失敗這樣的,逆正常思維的事情不要做。HTTP status code歸類如下,當然你也可以參考一下 HTTP Status Codes Cheat Sheet[2]。
  • Informational responses (100–199)
  • Successful responses (200–299)
  • Redirection messages (300–399)
  • Client error responses (400–499)
  • Server error responses (500–599)
但是Error Status Code不侷限在HTTP Status Code,你也可以參考SMTP, POP3等Status Code,此外你也自行可以選擇諸如007,777這樣的編碼,只要能解釋的合理就可以啦。
在日常的生活中,我們會使用一些特殊意義的數字或者和數字諧音,以下是一些友情提醒:
  • UIC-LOGIN-666:  太順利啦,完美登入。但是你團隊中有歐美老外的話,他可能理解為理解為惡意登入,登入失敗
  • APP-LOGIN-062:  如果你團隊有杭州土著的話,不要使用62這個數字
  • APP-001-013: 如果該error code要透傳給終端使用者,請不要使用13這個數字,會引發不適
這種有特殊意義的數字或者數字諧音,如520,886,999,95等,如果能使用的恰當非常方便理解或更友好,如透傳給使用者UIC-REG-200(註冊成功),如果調整為UIC-REG-520可能更溫馨一些。總的來說使用這些數字要注意場景,當然比較保險的做法就是參考HTTP,SMTP等設計的status code。

2  properties檔案儲存error code和message,真的比enum和POJO好嗎?

就Java和IntelliJ IDEA的支援來看,目前的配合還是比較好的,如i18n,維護成本等,而且這些ErrorMessages.properties也可以提交到中心倉庫進行Error Code集中管理,如果是Java Enum+POJO對i18n和集中管理都比較麻煩,而且程式碼量也比較大,你從上述的jdoctor的problem builder的就可以看出。當然在不同的語言中也未必是絕對的,如在Rust中,由於enum的特性比較豐富,所以在Rust下使用enum來實現error code可能是比較好的選擇。
#[derive(Debug)]enum ErrorMessages { AppLogin404 { email: String, }, AppLogin405(String),}impl fmt::Display for ErrorMessages { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {// extract enum name parameter// output message from java-properties write!(f, "{:?}", self) }}

3  為何不在Error Code中提供錯誤級別

不少錯誤碼設計中會新增錯誤級別,如 RS-001-404-9 這樣,最後一位表示錯誤的嚴重級別。這樣做沒有問題,但是也要考慮現實因素,如下:
  • 錯誤的級別會動態調整的:如隨著時空的變化,之前非常嚴重的錯誤級別,現在並不那麼嚴重啦。如果資源找不到可能之前非常嚴重,但是現在添加了備份方案,可以從備份伺服器中再查詢一次,所以這個錯誤出現在主服務上可能現在就不是那麼嚴重啦。
  • 不同團隊對錯誤級別的認知不一樣:如OSS-404在OSS團隊的data server上找不到,元資訊都是有的,結果在data server上沒有找到對應的資料,這個是非常嚴重的錯誤。雷卷在業務團隊,如負責Serverless Jamstack,其中的一個檔案缺失,如html, css, image,可能並不是一個大問題,等一會重試下,不行就再上傳一下。我想表達的是同樣的錯誤,在不同團隊中的重要性並不一樣。
如果將錯誤的基本固化到error code中,這個後續你就沒法調整啦,你如果調整了錯誤級別,那就是可能就是另外一個錯誤碼,給統計和理解都會造成問題。我個人是建議錯誤碼中不要包括嚴重級別這些資訊,而是透過外圍的文件和描述進行說明,當然你也可以透過諸如 log.info , log.error來確定錯誤的級別。

4  能否提供共享庫?

由於IntelliJ IDEA並不支援動態的properties檔名稱,如果你用動態的properties檔名稱,就不能進行程式碼提示,查詢等功能也都不能使用,所以必須是這種 @PropertyKey(resourceBundle = BUNDLE_FQN)  靜態的properties檔名方式。就一個Java類,你就受累Copy一下這個Java類,畢竟是一次性的工作,當然你想個性化調整程式碼也更方便,如和Log4j 2.x或自定也的logging框架整合也簡單些。 日誌是專案最基本的需求,所以你建立的專案的時候,就把Error Code對應的程式碼新增到專案模板中,這樣專案建立後就自動包含logging和error code的功能。

5  其他的考量

原文和Reddit上相關的討論也進行了一些整理和說明:
  • 內外有別:如內部開發者的錯誤中可能會包括伺服器的具體資訊,當然給最終消費者,如平臺的FaaS開發者,可能就不能輸出這樣的資訊,有一定的安全風險。
  • 小心在錯誤中暴露敏感資料:輸出到錯誤日誌的資料一定要進行mask,當然也不要影響你定位錯誤,這個要看具體的場景。
  • 不要將錯誤訊息作為 API 契約:在API的場景中,響應錯誤有兩種方式:根據錯誤碼做響應,如REST API;另外一種是根據訊息做出響應,如GraphQL,所以這個你自行選擇。
  • Error Code的一致性:錯誤訊息會輸出給不同的消費者,如REST API,介面等,可能錯誤的提示訊息有所不同,如國際化、脫敏等,但是最好都是相同的error code,也就是front end + backend 共享相同的error code,方便定位錯誤和統計。

七  總結

採用error code + 基於properties檔案儲存error message,這個設計其實就是一個綜合的取捨。如果IDEA不能很好地支援properties檔案,你看到一個Error Code,不能直接定位到錯誤的訊息,相反還需要跳轉來跳轉去找對應的訊息,那麼Enum + POJO可能就是好的選擇。此外error code的設計也非常偏向http status code方案,這個也是主要基於大家對HTTP都非常熟悉,基本上就能猜出大概的意思,相反隨機編碼的數字就沒有這方法的優勢,要去error code中心再去查詢一下,無形中也是浪費開發人員的時間。
最後專案的Demo地址:http://gitlab.alibaba-inc.com/leijuan/java-error-messages-wizard  歡迎留言
[1]https://github.com/melix/jdoctor
[2]https://cheatography.com/kstep/cheat-sheets/http-status-codes/
[3]https://www.morling.dev/blog/whats-in-a-good-error-message/

如何進行CDN以及下載最佳化分析

點選閱讀原文檢視詳情


相關文章