如何優雅地上報前端監控日誌

發表於2018-04-11
  • 頁面在使用者那裡執行,如果10%的使用者頁面出現問題而自己本地沒有辦法重現?
  • 如何先一步瞭解到前端出現的問題,而不是等使用者反饋?
  • 能不能像檢視服務端日誌一樣來定位前端頁面執行的問題?

前端在業務複雜度越來越高的情況下,本地即使做了充分的測試,依照caniuse做了很多相容,依然無法讓人放心頁面能否正常執行或者執行得怎麼樣。
當一個前端頁面釋出出去了之後,頁面所執行的裝置、瀏覽器、網路環境、使用者操作習慣等等因素都可能是造成頁面不正常的原因。
所以對前端頁面需要做一定的監控,而最可行的前端監控方式就是將頁面的日誌選擇上報到監控日誌伺服器中。

前端日誌上報可以很簡單

對業務邏輯的執行收集了日誌資料之後可以引數的形式構造一個url,再通過一個Image請求傳送到到伺服器就完成了日誌的上報。

這樣一行程式碼就搞定了日誌的上報,然鵝,在生產環境中,日誌上報所延伸的問題要複雜很多。

日誌上報帶來的問題

日誌上報最終是為了服務業務,監控業務的執行狀態,一般而言前端執行的場景中開發者最期望監控的不外乎頁面&API請求是否正常響應頁面js邏輯是否正常執行

為了覆蓋這兩個監控目標,需要通過很多型別的日誌來覆蓋,還有一些特殊場景下,開發者還希望能與具體業務靈活結合,實現自定義上報。所以常見的日誌型別如下
– 頁面&API請求是否正常響應
API呼叫日誌 – API呼叫成功與否及其耗時
頁面效能日誌 – 頁面連線耗時、首次渲染時間、資源載入耗時等
訪問統計日誌 – PV/UV,短時間內斷崖式的量變化很容易反應問題
– 頁面js邏輯是否正常執行
頁面穩定性日誌 – 頁面載入和頁面互動產生的js error資訊
– 業務相關日誌
自定義上報 – 某些業務邏輯的結果、速度、統計值等自定義內容

隨著前端業務的壯大,日誌監控上報的量會快速增加,監控的邏輯也會越來越複雜,而在生產環境中,前端監控的最基本原則是日誌獲取和上報本身不能丟擲異常或者影響頁面效能

這麼多的日誌型別代表了日誌獲取的邏輯複雜,同時各種各樣的瀏覽器和環境會讓這個問題變得更棘手,例如想用console.warn列印異常資訊,但是可能會出現warn函式呼叫報錯;例如捕捉到了error但是error.message全是Script error.

瀏覽器的相容性,前端業務邏輯依賴,日誌上報方式,日誌上報效率,使用者操作習慣,網路環境等因素都可能讓日誌上報產生問題甚至影響業務。這些因素會給日誌上報帶來可靠和效能兩方面的問題

日誌上報的可靠性問題

瀏覽器相容性

在不同端和瀏覽器中,因為相容性的不同,日誌獲取邏輯的和上報方法需要相容多種方式來進行,例如fetch方法方法是否可用,頁面效能(performance)計算是否可以使用NT2標準,這些問題可能會帶來上報邏輯本身報錯汙染業務日誌統計;

上報可靠性

日誌採集sdk可能因為網路原因無法載入,所以安全的方式是sdk注入的位置合理的靠後,那麼頁面開啟到sdk初始化這段時間就會產生漏報;
後端為了業務分離,通常會獨立設定一個日誌採集伺服器,這種情況下日誌上報可能會遇到跨域問題;
使用者的頻繁操作和關閉頁面會可能造成很多已經收集的資料漏報。

日誌上報的效能問題

在一個複雜站點中,這些日誌資料可能會非常多,上報可能會因為瀏覽器併發數量的限制阻塞業務的網路請求,或者影響頁面效能。

更優雅的上報姿勢

姿勢一 隔離業務

資源隔離

為了避免影響業務,那麼理所當然,為了不佔用業務計算資源,日誌上報需要單獨設定後端服務。

同時也不能使用與業務相同的域名,這跟頁面儘量使用CDN引入資源的原理相似,瀏覽器會對同一個域名有一定的併發數限制。

而頁面效能、資源載入、初始化API、PV/UV、初始化js邏輯錯誤等日誌都是頁面初始化的時候觸發上報,這種短時間大量的上報可能會造成網路請求延時。例如chrome對同一個域名的最大併發連線數為6個,如果日誌同時上報了6次以上,就會對同域名的業務造成影響;更壞的情況如頁面有一些錯誤、網路連線質量質量不高會讓日誌上報阻礙頁面渲染。

因此日誌上報可以像使用CDN服務一樣,使用單獨域名和日誌處理服務。
既然使用了不同的域名,那麼跨域問題隨之而來,這需要前後端共同支援。伺服器需要允許外部訪問Access-Control-Allow-Origin:*;前端在進行日誌上報的時候要新增避免跨域標識,如fetch方式:

不同域名一個效能缺點是增加首次DNS解析時間,不過可以通過在頁面新增DNS預解析來避免。

異常隔離

在資源隔離的基礎上,日誌上報的異常處理也需要隔離,日誌本身丟擲的異常絕對不能和業務異常混在一起上報。
進行充分測試的前提下,最簡單粗暴的方式是在整個監控sdk外面新增try...catch...,好處是永遠不會出現sdk本身錯誤上報,不過同時也讓開發者失去了發現sdk問題的途徑。所以兩者兼得的方式是必要的。
這裡提供一個關鍵模組埋點的方法,它對整個前端監控sdk多個關鍵點上埋點並且收集的結果中只標記是否成功。話不多說,直接上示例程式碼:

姿勢二 壓縮請求響應報文

壓縮之前重新審視一下(new Image).src的日誌傳送方式:

HTTP Request: 前端日誌資料以多組key=value的字串形式接在一個Image資源請求的url後面,前端傳送Image請求。
HTTP Responce: 伺服器返回響應結果或者空圖片。

日誌資料直接放到url中的好處是網路傳輸效率高。然而url長度是有限制的,例如IE瀏覽器是2083個字元,同時伺服器也會對url長度進行限制。
類似如下的js error資訊就沒有辦法完整上報,

不僅僅是js error的錯誤棧深還因為urlencode對特殊字元和漢字的轉碼,這兩個因素會使url長度輕鬆突破限制。

另外業務邏輯實際上不關注而且也應關注日誌上報的響應結果,所以這個請求的結果應該儘可能省去。

針對報文壓縮有以下方式:

HTTP/2頭部壓縮

http請求中,每次請求都會傳輸一系列的請求頭來描述請求的資源及其特性,然而實際上每次請求都有很多相同的值,如Host:,user-agent:,Accept等。這些資料能夠佔用到300-800byte的傳輸量,如果攜帶大的cookie,請求頭甚至可以佔用1kb的空間,而實際真正需要上報的日誌資料僅僅只有10~50byte的大小。在HTTP 1.x中,每次日誌上報請求頭都攜帶了大量的重複資料導致效能浪費。
HTTP/2頭部壓縮採用Huffman Code壓縮請求頭,並用動態表更新每次請求不同的資料來把每次請求的頭部壓縮到很小。

HTTP/1.1效果
http1.1.png
HTTP/2.0效果
http2.0.png
頭部壓縮後每條日誌請求的size都大大減小,響應的速度也有提升。

壓縮日誌的長度

最需要壓縮即js error的錯誤棧,錯誤棧當中佔位最多是錯誤定位的檔案地址,而很多錯誤棧有很多相同的檔案,壓縮空間就來源於stack中js檔案的url重複。
一個典型的jserror stack經常會出現這種形式如下:

可考慮把檔案url抽取出來單獨作為一個字典,那麼上報內容可縮減為

即可大大縮減日誌長度。

省去響應體

日誌上報本身只關注日誌有沒有上報,而對上報請求的返回內容並不關注,甚至完全可以不需要返回內容。所以使用HTTP HEAD的方式上報,並且返回的響應體為空,避免響應體傳輸資源損耗。
這時候只需要設定一個nginx伺服器來記錄日誌內容並返回200狀態碼即可。

姿勢三 合併上報

既然一個頁面上報的次數那麼多,一個更容易想到的idea應該是把日誌合併上報來減小請求數量。

HTTP/2多路複用

使用者瀏覽器和日誌伺服器之間產生多次HTTP請求,而在HTTP/1.1 Keep-Alive下,日誌上報會以序列的方式傳輸,會讓後面的日誌上報延時。通過HTTP/2的多路複用來合併上報,節省網路連線的開銷。
螢幕快照 2018-01-17 10.24.32.png

HTTP POST合併

POST請求因為request body可以有更大施展空間,在HTTP POST中只要一次包含多條日誌的內容,那麼相對於一條日誌一次HTTP HEAD請求的方式會更加經濟。

在HTTP POST的基礎上,可以順便解決使用者關掉或者切換頁面造成的漏報問題。
以前常見的解決方式是監聽頁面的unload或者beforeunload事件,並以通過同步的XMLHttpRequest請求或者構造一個特定src<img>標籤來延遲上報。

這種上報的缺點是會給下一個頁面的效能造成影響。更優雅的方式是使用navigator.sendBeacon(),它能夠非同步地傳送日誌資料。

合併前
螢幕快照 2018-01-18 11.10.53.png
合併後(navigator.sendBeacon)
螢幕快照 2018-01-18 11.14.22.png
螢幕快照 2018-01-18 11.18.31.png
理想情況下,合併n個日誌上報耗費的總時間能達到原來的1/n

小結

前端業務場景和瀏覽器的相容性千差萬別,所以日誌上報要相容多種方式;頁面生命週期、業務邏輯影響了日誌是否可獲取和是否漏報,所以對應的日誌型別和上報時機要嚴格把握;前端業務邏輯快速迭代且場景多樣,所以日誌上報要做到與業務解耦合同時可以自定義上報…
這些大大小小的坑促使我們把前端日誌監控沉澱為一個獨立且系統性的工程來做,在打磨這個工程的過程中我們同樣還在探索是否有更加高效、穩定的日誌上報方式。

相關文章