原文:Structured Logging: The Best Friend You’ll Want When Things Go Wrong
介紹
在這篇文章里,我們重點介紹結構化日志。我們討論是是什么,為什么好,以及如何構建一個框架更好的與我們當前基於Elastic stack
的日志后端集成,使我們更好,更高效記日志。
結構化日志是我們竭力做的很大一部分,結構化日志能讓我們減少bug解決時間(MTTR),中斷時幫助開發人員更快地緩解問題。
什么是日志?
日志是包含有關系統中發生一些事件的幾行文本信息,並且起着幫助我們了解后端正在發生的事情的重要作用。日志通常放置於重要事件的代碼中(例如:成功操作某些數據庫,或者指派司機給乘客),或我們感興趣留意的代碼中。
當有錯誤時,正常開發者做的第一件事情就是查看日志——有點像瀏覽系統的歷史,並且找出發生了什么。因此,在服務中斷、錯誤、構建失敗時,日志成為開發人員最好的朋友。
現在的日志具有不同的格式和功能
- 日志格式:從基於鍵-值(像syslog)到非常結構化和詳細(像JSON)。由於日志主要用於開發者的眼睛,因此日志詳細和結構化程度決定了開發者查詢和閱讀日志的速度。數據越結構化——每行日志就越大,盡管更易於查詢和包含更豐富的信息。
- 等級日志(或日志等級):不同等級對應着不同重要性的日志。可見性可限制單個等級,僅限於某些重要性或等級以上的日志(如:僅記錄WARN和更高等級)。通常日志等級在生產環境中是靜態的,查找DEBUG等級的日志通常需要重新部署。
- 日志集后端:日志有不同的日志集后端,也意味着不同的后端(如:
Splunk
,Kibana
等)決定了日志的樣式或者用他們能做些什么。一些人可能比其他人使用的更多。 - 因果順序:日志可能也可能不會保存寫入的實際時間。這很重要,因為時間的確切程度決定了我們通過日志預測事件順序的准確程度。
- 日志關聯:我們服務於后端服務的無數請求。能看到與特定請求或特定事件相關的所有日志,幫助我們深入到特定請求的相關信息中(例如:試圖登記騎乘的特定乘客)。
將此與過多可用的日志庫結合起來,很容易讓開發人員懵逼,無法決定使用什么。此外,每個庫都有自己的優缺點,因此討論可能很快變得主觀化和極端化——因此,為你的程序選擇適當的庫和后端非常重要。
我們在Grab中使用不同類型的日志庫。然而,隨着需求的變化——我們也發現我們自己正在重新評估日志策略。
Grab中日志的狀況
Grab的Golang服務的數量持續增長。大多數服務使用syslog
鍵值格式的日志,由於簡單,並且容易讀寫,因此是服務端程序中最常見的格式。所有這些日志可能是少量的公共庫實現,不同的服務直接引用這些庫來使用。
我們使用基於雲的SaaS供應商作為這些日志的前端,應用程序產出的日志寫入文件中並發送給我們的日志供應商,從而可以實時查看和查詢。很長一段時間里使用的非常不錯,也無任何磕絆。
然而,隨着時間推移,我們的日志清單上升到了前所未有的等級,發現我們自己正在重新審視並且重新評估如何記日志。出現的一些問題:
- 減少日志量的努力在某些程度上是成功的——但也是艱巨而痛苦的。一部分原因是幾乎所有的日志都是單一的日志等級——
INFO
這個問題不是在單個服務中,而是在所有服務都很普遍。為了緩解,有些服務對日志抽樣,有些服務完全刪除了日志。后者會后患無窮,因此我們必須改善日志等級。
- 當時對我們來說使用供應商有點昂貴,也有些顧慮——主要受限於DSL(查詢語言)。有很多優秀的開源的替代方案——
Elastic stack
是其中的一個。我們的工程師確信我們可以管理我們的日志基礎架構並更好地管理成本——這導致了提議構建Elastic堆棧日志集群。 Elasticsearch比我們當時的供應商強得多,而且我們當前的庫不足以充分利用其功能,因此我們需要在日志中有更好的結構並輕松與Elastic堆棧集成的庫。 - 我們的日志庫中有些小問題:
- 單一的初始化方案更難做單元測試
- 單一的日志接口減少了日志核心功能的擴展性,因為幾乎所有服務直接導入日志接口
- 不支持多寫的開箱即用。
- 如果我們寫個日志庫,必須要解決這些問題——並鼓勵使用最佳實踐
- Grab的關鍵路徑(單個訂單流程請求經過的服務數量)大小已經增長了。平均,單個訂單請求涉及的微服務——每一個都不同。因此,我們大規模的運營時,很有必要對單個請求容易地查看流經的所有的服務日志——然而這不是我們的日志庫自動完成的功能。因此,我們也想要更容易、更好的日志關聯。
- 日志是某個時間點的事件。事件的順序給與我們系統發生了什么的完整歷史。然而,我們Golang服務的核心日志庫沒有保存日志的產生時間(而是寫入時間)。這導致了在幾微妙內產生的日志造成混亂。這不僅使開發者的生活更困難,而且幾乎無法准確的獲得系統的歷史事件。這就是我們想改進和啟用因果排序的日志——是了解系統事件的關鍵一步。
為什么改變?
如上所述,我們知道怎么記日志會有些問題。為了最好的解決問題,並且在不影響現有的架構和服務盡量的解決問題,決定從頭啟動一個新庫。這個庫應該能解決已知的問題,也包含修改現有的庫無法實現的功能。扼要重述,我們想解決的:
- 增加日志等級
- 更好的日志結構
- 容易集成到Elastic stack
- 鼓勵使用最佳實踐
- 日志關聯更容易、更好
- 改進並啟用日志的因果排序,以便更好地了解服務分配
調查結構化日志。結構化日志在全世界非常受歡迎,廣泛被采用。容易的集成到我們的Elastic stack中,也解決了我們的很多痛點。
結構化日志
記住我們之前的問題和需求,我們用Golang新建了一個庫,有一下功能:
動態日志等級
允許我們在運行時從配置管理系統改變初始化的日志等級——這是之前無法做到和被鼓勵的。
現在,日志等級更有實際意義。現在開發人員可以用常用的WARN
或者INFO
部署,當出現問題時,僅更改配置就能更新日志的等級到DEBUG
,並且調試時他們的服務能輸出更多的日志。這也有助於我們控制日志成本。我們支持和我們的配置管理系統簡單容易低集成。
日志結構一致性
日志天生是無結構化的,不像數據庫模式的死板或者自由格式的文本那樣無結構化。我們 Elastic stack
后端主要基於帶有映射(像松散的模式)的索引(類似於表)。為此,我們需要用一致性結構的JSON輸出(例如,在相同JSON字段下不能輸出整數和字符串,因為這會導致Elasticsearch索引失敗)。另外,我們意識到我們的主要目標之一是控制日志成本,因為幾乎每個字段的結構和索引都沒有意義——只添加對我們有用的結構。
為了解決這些,我們構建一個允許我們確定地為日志添加結構。這是建立在我們可以用特殊的字段名和類型添加鍵值對的架構之上。根據該模式生成代碼,並使用生成的代碼確保事物的一致的格式且不會中斷。我們稱這種模式(鍵名和類型對的集合)為Common Grab Log Schema (CGLS)。我們僅向CGLS中添加結構是很重要的——CGLS中包含的所有內容在不同的字段中格式化,其他內容在生成的JSON中的單個字段中格式化。這有助於保持我們的結構一直並且易於使用Elastic stack
。
支持使用Grab-Kit
即插即用
我們通過對Grab-Kit
內部支持進行初始化,使用簡單並且開箱即用,因此,開發者無需修改即可使用。此外,作為整體的一部分,我們基於追蹤中存在的請求ID添加了自動的日志關聯,這確保了具有該跟蹤ID的特定請求生成所有日志。
可配置的日志格式
我們主要的需求是構建一個有足夠的表現和一致性,以便更好的與Elastic stack
后端集成,在下游無需經過花哨的解析。因此,該日志庫具有的表現力和可配置性足以允許任何日志格式(我們可以對不同功能的用例寫不同的日志格式,例如,開發設置中的可讀格式和產品設置中的輸出JSON
格式),默認是輸出JSON
格式。這確保了我們可以生成與Elastic stack兼容的日志輸出,但仍然可以針對不同的用例進行配置。
支持不同格式的多寫
作為日志庫功能擴展的一部分,我們需要足夠的可配置性,以便能夠發送不同的日志到有不一樣的設置的不同地方。例如,異步發送易讀的格式的FATAL
日志到Slack
,同時將所有的常用日志發送的我們的Elastic stack
后端。該日志庫包括支持將這些”核心“連接到任意可能的程度——確保這些日志器被用在此類高度專業化的情況。
開發中類似生產環境的日志
開發者從一開始就看到了控制台日志,然而,有結構化的JSON
日志一般認為是產品的日志,而且更易於搜索。為了更好的在開發過程中利用,並且讓開發者直接在Kibana
中看到他們的日志,我們提供了docker化版本的Kibana
,可以在本地運行以接收結構化日志。這可以讓開發者直接使用結構化日志並且在Kibana
中看到——就像生產環境中那樣。
這個日志庫讓我們用更好的方式打日志。最顯而易見的影響是我們能簡單的訪問日志,能夠使用更好的過濾和條件來更好的查詢。
因果順序
有精確歷史記錄的事件讓在生產環境的系統中調試問題更容易——因為僅看到歷史記錄就能很快的猜測出錯誤原因並且修復。為此,結構化日志庫在日志器中添加了精確的寫入時的納秒時間戳。這與類似JSON結構化的格式結合讓根據這些字段排序所有的日志成為可能——因此我們以他們發生的確切的順序看日志——在日志中實現因果順序。這是看起來的低調,但是使調試容易的強大功能。
但為什么要結構化記日志?
現在你已經知道了我們日志策略背后的歷史和原因,讓我們總結下從中獲得的福利。
一開始,有明確定義和結構化(像JSON)的日志有很多好處,包括但不僅限於:
-
更好的根本原因分析: 使用結構化日志,我們可以提取和執行更強大的查詢,這對於非結構化的日志是不可能的。開發人員可以在查找與情況相關的日志時進行更多信息性查詢。不僅於此,日志關聯和因果順序對更好的理解分布式日志成為可能。不像無結構的數據,我們僅局限於全文或者少數的日志類型,結構化日志達到了全新的水平。
-
更高的透明度或更好的可觀察性:使用結構化日志,提高了系統發生情況的可見性——因為現在你可以用更好,更有變現力的方式記錄信息。這可讓你更透明的觀察系統發生的情況,並且讓你系統在更長的周期內更容易的維護和調試。
-
更好的標准化:使用單一,定義明確,結構化的方式去記日志使我們的日志標准化——這減少了通過日志確定系統發生的事件的認知,並且更易采用。而不是通過100種不同類型的日志,而是只有一種格式。這也是日志庫的目標之一——通過Golang后端服務日志庫的標准化使用。
我們還獲得額外的好處:
-
動態日志等級: 使代碼中的日志等級富有意義——我們可以用基線設置部署,並且僅當我們需要的時候切換到更低的等級(debug等級的日志)。有助於我們降低日志成本,同樣也減少開發人員在調試時通常需要搜查的無關日志。
-
日志中面向未來的一致性: 通過采用通用模式,確保我們堅持使用相同的模式,即使明天我們的日志基礎架構改變——我們做好未來的准備。我們可以簡單在我們的日志器中暴露一個函數,而不是手動地指定要記錄的內容。
-
開發中類似生產的日志環境: docker化的Kibana使開發人員享受到與生產環境中的Kibana同樣的好處。這也更激勵開發認識使用
Elastic stack
並探索它的功能,像基於日志數據構建儀表盤,有更好的查看方式,等等。
希望你喜歡這篇文章並發現它的有用之處。歡迎提出意見和更正。