• 中文
  • ENGLISH
如何優雅地上報前端監控日志
2018/02/01
  • 頁面在用戶那里運行,如果10%的用戶頁面出現問題而自己本地沒有辦法重現?
  • 如何先一步了解到前端出現的問題,而不是等用戶反饋?
  • 能不能像查看服務端日志一樣來定位前端頁面運行的問題?

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

前端日志上報可以很簡單

對業務邏輯的執行收集了日志數據之后可以參數的形式構造一個url,再通過一個Image請求發送到到服務器就完成了日志的上報。

(new Image).src = `/r.png?page=${location.href}&param1=${param1}...`;

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

日志上報帶來的問題

日志上報最終是為了服務業務,監控業務的運行狀態,一般而言前端運行的場景中開發者最期望監控的不外乎頁面&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方式:

var url = 'https://arms-retcode.aliyuncs.com/r.png';
fetch(
`${url}?t=perf&page=qar.alibaba-inc.com&load=1168`,
{mode:'no-cors'}
)

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

<link rel="dns-prefetch" href="https://arms-retcode.aliyuncs.com">

異常隔離

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

// 全局標記匯總,初始化為36個點位均為1的數組
var N = 36;
var sdkStat = Array.from({length: N}, () => 1);

/** 日志上報功能模塊
* 對應模塊報錯設置對應點位為0,多個點位為0可以幫助找到錯誤發生鏈路
*/
try{/* sdk module 0*/}catch(){sdkStat[0] = 0;}
/* other modules */
try{/* sdk module 35*/}catch(){sdkStat[35] = 0;}

// 日志上報發送模塊
var statStr = parseInt(sdkStat.join(''),2).toString(36);
(new Image).src = `/r.png?&param1=${param1}&sdkStat=${statStr}...`;

姿勢二 壓縮請求響應報文

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

HTTP Request: 前端日志數據以多組key=value的字符串形式接在一個Image資源請求的url后面,前端發送Image請求。
HTTP Responce: 服務器返回響應結果或者空圖片。

日志數據直接放到url中的好處是網絡傳輸效率高。然而url長度是有限制的,例如IE瀏覽器是2083個字符,同時服務器也會對url長度進行限制。
類似如下的js error信息就沒有辦法完整上報,

$ is not [email protected]://www.example.com.cn/catalog/?spm=a2o4k.customer.0.0.37c1379dmQwdrW&q=pediasure&searchclickposition=hint:3:231
...
[email protected]://www.googletagmanager.com/gtm.js?id=GTM-KTVS7D9&l=shadowDatalayerKi7l:64:32
...

不僅僅是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經常會出現這種形式如下:

obj0.fn0 at (http://loooooooooonnnnnnnnnnng/loooooong/long.js 123:1)
obj1.fn1 at (http://loooooooooonnnnnnnnnnng/loooooong/long.js 234:1)
obj2.fn2 at (http://loooooooooonnnnnnnnnnng/laaaaaang/lang.js 345:1)
...

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

files={'f1':'http://loooooooooonnnnnnnnnnng/loooooong/long.js','f2':'...'}
obj0.fn0 at (f1 123:1)
obj1.fn1 at (f1 234:1)
obj2.fn2 at (f2 345:1)
...

即可大大縮減日志長度。

省去響應體

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

fetch(`${url}?t=perf&page=lazada-home&load=1168`,
{mode:'no-cors',method:'HEAD'}
)

姿勢三 合并上報

既然一個頁面上報的次數那么多,一個更容易想到的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>標簽來延遲上報。

window.addEventListener("unload", uploadLog, false);
function uploadLog() {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/r.png", false); // false表示同步
xhr.send(logData);
}

這種上報的缺點是會給下一個頁面的性能造成影響。更優雅的方式是使用navigator.sendBeacon(),它能夠異步地發送日志數據。

window.addEventListener("unload", uploadLog, false);

function uploadLog() {
navigator.sendBeacon("/r.png", logData);
}

合并前
屏幕快照 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

小結

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

業務實時監控服務 ARMS
應用監控
前端監控
自定義監控

訂閱我們
体彩20选5开奖结果查询