如何在程式碼中打日誌

橙紅年代發表於2019-02-24

該文章同步在個人部落格shymean.com上,歡迎關注

前言

之前對於程式碼中的console.log一直是比較嫌棄的,以致於提交程式碼前一般會通過eslint檢測是否包含了log輸出。

最近一直在處理一個chrome外掛的需求,需要開啟某個url標籤頁,然後根據預先設定的操作行為,在頁面上進行一些列操作(點選選項卡、選擇日期、輸入引數等),然後等待頁面生成報表,實現自動下載的流程。整個過程較為繁瑣,很不方便除錯,在開發中使用了大量的console追蹤頁面的狀態和操作行為的結果,逐漸認識到:日誌應該是開發和除錯中很重要的一環。因此在這裡整理一下“如何在程式碼中打日誌”的問題。

本文僅涉及與開發相關的日誌,如程式碼流程、錯誤日誌監控等,與業務相關的打點、流量統計等方面暫未涉及,也不包括log4js等日誌框架的使用。參考

日誌的分類與分級

日誌的分類

從程式碼的執行時間段對日誌進行分類

  • 在除錯階段,日誌的主要作用是檢查程式碼的邏輯執行,,包括記錄程式碼執行流程、引數、執行結果等,這種日誌一般是開發人員用於除錯程式碼,不會出現在生成環境
  • 在生產環境,日誌的主要作用是為了快速準確定位並解決問題,這種日誌一般記錄程式的執行狀況,特別是非預期的行為、異常情況,這種日誌主要是給開發、維護人員使用

通過日誌的分類,我們可以瞭解到什麼時候需要打日誌

  • 部分重要的核心功能,需要通過日誌確認程式碼執行的整個流程和結果,當遇見某些重要的if...else分支時,此時需要通過日誌來確認程式碼進入哪個分支

  • 在程式碼異常時丟擲的錯誤,預期程式碼應該是不會發生錯誤的,只能通過debug功能來確定問題,此時需要打日誌來記錄程式碼中的異常,良好的系統,是可以通過日誌進行問題定為的。

日誌的分級

如果一個應用的日誌同時也分了多個級別,那麼可以很輕易地分析得到該應用的健康狀況,及時發現問題並快速定位、解決問題,補救損失。

關於日誌的分類,stackoverflow上有一個討論,總結一下,理論上越靠前的等級越嚴重,越應該被記錄

  • 緊急錯誤 fatal,表示需要立即被處理的系統級錯誤,這屬於最嚴重的日誌級別

  • 錯誤 error,影響到程式正常執行、當前請求正常執行的異常情況,如

    • 資源依賴載入失敗導致程式無法啟動
    • 程式碼丟擲未處理的異常,導致程式終止
  • 警告 warn,應該出現但是不影響程式、當前請求正常執行的異常情況,一般打在有容錯機制的時候出現的錯誤情況,如

    • 請求引數未按照預定格式傳入,
    • 系統資源佔用達到警戒線
  • 資訊 info,記錄程式執行的資訊,如流程步驟、呼叫引數、呼叫結果等,如

    • 呼叫第三方服務時,info資訊是十分必要的,因為我們很難追溯到第三方模組發生的問題
    • 對於複雜的業務邏輯,需要記錄程式的執行流程,檢測程式是否按正常邏輯執行
  • 除錯 debug,用於開發除錯程式碼,可以記錄想要知道的全部資訊,但是不能一直出現在生產環境,需要通過開關控制或者在上線前移除除錯日誌

  • 追蹤 trace,特別詳細的系統執行完成資訊,一般不要出現在生產環境

關於日誌的分級,一種觀點是分級應該儘可能詳細,從而更精細地過濾和處理日誌;而另外一種觀點是:日誌應該只有info、error兩個級別,多餘的級別基本上只會造成混亂。

更極端的觀點是:日誌根本不應該分級,在開發過程中,日誌並不是除錯的唯一手段,但是在定位生產環境問題的時候,日誌是唯一的依據,輸出日誌最終的價值就是體現在定位現場問題。如果僅僅只是為了通過過濾開發和生成環境不同的日誌輸出等級,來達到控制日誌輸出數量的目的,倒不如下苦功夫,找到並刪除冗餘日誌。

我的觀點是:這麼詳細的分級也許不是必須的,但在一個專案各個日誌級別的定義應該是清楚明確的,需要團隊的每個開發人員共同遵守。在現在的專案中,我使用到的主要是info記錄程式碼執行流程和error記錄錯誤日誌。我的觀點不一定正確,這可能是目前的經驗還不夠豐富造成的,關於日誌的分級,需要從大量的實踐中繼續學習。

擴充套件閱讀

如何打日誌

前面提到了日誌的分類和級別,大致清楚應該在程式碼的什麼地方編寫對應等級的日誌,接下來就需要了解應該如何編寫日誌了。這也是這篇文章的主要目的:在自己的程式碼中打正確的日誌。

編寫合適的日誌內容

日誌編寫的總體原則是簡單清晰、便於排查問題,因為日誌的主要內容是記錄當前操作做了什麼, 使用的什麼資料. 好的日誌應該被看成文件註釋的一部分。

一般地,每一條日誌資料會包括描述上下文兩部分,此外,日誌可能還需要記錄對應的時間。一條日誌應該易讀, 清晰, 可描述. 要記錄的當前操作做了什麼, 使用的什麼資料,好的日誌應該被看成文件註釋的一部分。


let a = 100
log(a + 200) // 這種沒有描述的日誌只會輸出一個 300,翻日誌的時候可能會一臉懵逼

if(date instanceof Date ){
    ...
}else {
    log('date型別不正確') // 這種沒有上下文的日誌很難定位到具體的輸出點
}

// 日誌應該具有很強的閱讀性
log(`${new Date()}: downloadMonthReport ${year}${month}月的報表已下載完畢,準備開始下一條記錄id:${id}的生成`)
複製程式碼

輸出日誌時,要考慮日誌的使用者,因為不同的使用者對於日誌內容的需求可能是不一樣的,開發可能希望快速定位程式碼位置,運維可能希望從日誌直接獲取系統資源狀態,諸如此類。理想的日誌內容中應該遵循下面規則

  • 不要在日誌中記錄無用的資訊,否則就是浪費時間
  • 不要遺漏日誌使用者所需的訊息,否則該條日誌就沒有意義了

有說法提到打日誌跟寫小學作文一樣:什麼時候在什麼地點發生了什麼事。仔細想想,貌似確實有點道理~

控制日誌數量

生產環境裡執行的程式如果沒有日誌會讓維護者提心吊膽,有太多雜亂無章的日誌也會讓維護者心煩意燥。因此我們需要控制日誌的數量,不要把日誌寫的像流水賬一樣。

不重要的資訊儘量不要列印日誌,通過配置日誌的分級可以在一定程度上解決這個問題,不同等級的日誌在數量級上是不一樣的。

另外的一個問題是,我們不應該在邏輯程式碼中手動控制每一個日誌的輸出,除非某條日誌的效能消耗十分嚴重;相反地,日誌的輸出決定權應該由配置項決定,一般這個工作會交給日誌框架處理

// 最好不要出現下面程式碼
if(shouldLog) log('xxx');

// 而是應該直接明瞭的寫log,至於是否輸出日誌應該在日誌系統本身控制
log('xxx)
複製程式碼

在前端開發中,如果日誌需要上傳,則更不應該頻繁地記錄和上傳日誌,因為如果網頁的pv比較大,則上傳的日誌數量也會非常多,這會增加系統的負荷和分析時的複雜度,這個問題在後面的章節中會進一步講解。

正確處理異常日誌

異常日誌是日誌分類中非常中要的一環,可以幫助我們快速準確定位並解決。打異常日誌時,要記錄關鍵引數,出錯時的關鍵原因,這樣可以快速復現和修復問題,下面是參考整理的關於異常日誌的幾條實踐

  • 記錄程式碼異常而不是使用者異常(如未登入、使用者名稱已存在),使用者異常應該顯示給使用者,程式碼異常才是我們需要關注的
  • 日誌中需要記錄發生異常時的呼叫函式上下文和引數,儘量保留復現問題的現場
  • 記錄異常的描述資訊和呼叫堆疊,以及什麼情況下回丟擲該異常的描述
  • 異常日誌應該擁有較高的日誌級別

什麼時候丟擲異常也是一個比較講究的事情,關於異常日誌,可以參考

前端開發中的日誌

大部分成熟的後端框架都有非常完善的日誌系統,而由於前端程式碼執行在瀏覽器中,這導致前端日誌與服務端日誌有一些根本上的差異,前端日誌主要存在下面一些問題

  • JavaScript語言的動態性導致可能會產生很多被忽略的、未捕獲的異常

  • Chrome開發除錯實在是太方便了,以至於忽略了日誌的重要性~(/掩面)

  • 由於預設的日誌依賴控制檯檢視,在移動端無法直接看見輸出的日誌,雖然可以通過(eruda)(github.com/liriliri/er…)或者vConsole解決這個問題

  • 如果不進行特殊處理,前端日誌無法持久化,重新整理或切換頁面就會導致日誌丟失,導致難以對線上問題進行追溯和分析;

尅前面兩個問題可以歸結到開發者身上,後面的問題其實可以看做同一個問題:前端日誌上報

日誌上報

前端日誌上報有下面幾個理由

  • 為了監控前端應用是否正常執行,通常會在前端收集一些錯誤與效能等資料,但是開發者是無法直接訪問到專案的執行環境的,需要通過將這些資料上報到服務端,然後才能進行分析
  • 日誌上報解決了前端日誌無法持久化的問題,可以通過上報將日誌儲存到檔案、資料庫等地方

一般地,日誌上報都是通過傳送網路請求,將日誌記錄上傳到服務端,因為日誌上報不需要響應處理,只需要把資料上傳即可,常見的上報方式有

  • 0畫素打點上傳,通過構建img的src輸出,在get請求拼接日誌
  • xhr、fetch等網路請求,主動發起網路請求,主要用於提交內容較大的日誌
  • script、link標籤等可以發起網路請求的其他標籤,與畫素打點類似

然而日誌上報會帶來也會帶來下面兩個問題。

第一個問題:如果進行了日誌上報,則每個訪客都可能上報大量相同的日誌,pv過大會導致日誌存量極速增長,因此建議不要頻繁地上傳錯誤日誌,這個問題可以通過增加日誌取樣率解決

function log(msg, sampling = 1){
    if(Math.random() < sampling){
        _log(msg)
    }
}
複製程式碼

第二個問題:日誌上報並不是應用的主要功能邏輯,不應該與其他重要的資源請求競爭網路;但是,日誌又負責上傳應用的錯誤與效能資料,因此也需要保證重要的日誌上傳到服務端。為了處理這個問題,可以使用信標Beacon

  • 使用Beacon表示我們通過瀏覽器傳送了一個不需要響應的POST請求,該請求可以攜帶少量資料,而且信標請求優先避免與關鍵操作和更高優先順序的網路請求競爭,這跟日誌的使用場景十分契合

  • Beacon請求可以有效地合併,保證在移動裝置上的效能。

  • Beacon請求的非阻塞性意味著效能比xhr和fetch都優秀,保證頁面解除安裝之前啟動信標請求,因此可以很方便地的統計頁面unload時的資訊,而不會阻塞頁面的解除安裝

// result 為true代表使用者代理成功地將信標請求加入到佇列中,否則返回false
let result = navigator.sendBeacon(url, data);
複製程式碼

更多Beacon的使用可以參考

前端異常處理

前端異常主要包括:JS語法錯誤和執行時發生的錯誤。利用JavaScript語言和DOM提供的捷徑,可以在前端暴力式獲取錯誤資訊,如 try..catch 和 window.onerror。

try…catch

try {...}catch(e){
    log(e)
}
複製程式碼

try..catch 應該只用來處理程式碼中無法控制的異常,如JSON.parsedecodeComponentURI等系統方法的呼叫,依賴不可控的引數型別。JS程式碼是自己編寫的,應該想辦法增加程式碼的魯棒性,而不是一味地把自己的編碼缺陷丟給catch塊。

let sel = '#J_testBtn'
let btn = document.querySelector(sel)
if(btn && typeof btn.click === 'function'){
    btn.click()
}else {
    console.log(`未找到${sel}的按鈕,無法執行click操作`)
}
複製程式碼

window.onerror

window.onerror = function() {console.log(arguments)}
複製程式碼

使用window.onerror需要注意下面幾個問題

  • 檢測全域性的錯誤,使用該方法最好將其放在所有程式碼的前面,否則同步程式碼丟擲的異常會導致該方法不被執行
  • 跨域之後window.onerror是無法捕獲異常資訊的,會統一返回Script error.,解決方案便是script屬性配置 crossorigin=”anonymous” 並且在伺服器配置CORS。

此外,一般前端框架也會提供一些處理異常的鉤子,如

  • Vue中提供了errorHandler,可以用於指定元件的渲染和觀察期間未捕獲錯誤的處理函式。這個處理函式被呼叫時,可獲取錯誤資訊和 Vue 例項
  • 微信小程式提供了onError,當小程式發生指令碼錯誤或 API 呼叫報錯時觸發。也可以使用 wx.onError 繫結監聽。

目前還有有一些比較專業的應用錯誤監控平臺,如fundebug等,提供錯誤監控和分析等服務。

小結

這篇文章整理了關於在在程式碼中打日誌的一些概念和思考,

  • 要重視日誌,但日誌並不是越多越好,要在合適的地方編寫合適的日誌內容
  • 日誌需要分級,但是如何分級應該根據團隊約定來定
  • 前端日誌可以通過Beacon來上報,這個感覺可以嘗試寫一個日誌庫來實現下

日誌最主要的目的是幫助我們快速定位問題,編寫正確的日誌並不是一件輕鬆的事情,在之後的專案中,希望自己能夠重視這個問題。

相關文章