在前面幾篇關於 Opentelemetry 的介紹和原理demo中,我們都是以後端的角度來使用 Opentelemetry,無論是 trace、metric、log 等,都是從後端應用中採集得到。如果以前端的角度來看看待 Opentelemetry,會是怎麼樣?
在一個完整的 trace 追蹤鏈路中,前端(client side)通常是發起請求的源頭。當前端接入 OpenTelemetry 的 trace 追蹤時,它將會成為該 trace 中的第一個 span,為後端服務的調用提供上游背景。
如果提到性能監控,那麼這裏就會是跟前端效能有關了,也就是之前提到的 WebVitals:頁面加載時間、首屏渲染時間(FCP)、互動延遲(TTI)等
而在 Opentelemetry JS 中,有以下跟 web 有關的 instrumentations:
opentelemetry-instrumentation-document-load
---監控文檔的載入情況opentelemetry-instrumentation-long-task
---監控長任務對頁面性能的影響opentelemetry-instrumentation-user-interaction
---監控使用者的交互時間opentelemetry-plugin-react-load
---監控React組件的載入情況(不過目前僅支持 class component,因為其原理是擴展 class component、來增加觀測方法)這些工具的確是可以幫助工程師自動收集跟「前端效能」相關的指標,不過我在快速掃過原始碼後,沒有看到利用 Meter
創建相關的 metric data,只有用 trace 和 log 來記錄相關的觀測目標。
對 metrics 的支持還需要工程師手動配置(目前就是直接忽略)
對於前端的 log 記錄,如果需要創建 log 來特別記錄某些事件或行為,可以直接將 JSON 格式的日誌傳送到後端 API,或集成像 Sentry 這類服務來進行處理。這些工具能協助工程師捕獲瀏覽器中的錯誤與行為日誌。
從上述對 trace、metric、log 在前端的意義,可以看出其實如果要觀測前端, Opentelemetry 可以提供的就是把 trace接入、進行從 client side 到 server side、資料庫、第三方api等等的鏈路追蹤,讓其可以在同一個平台進行觀測。
而 metric 和 log data,可能就要另外處理。
所以,Sentry的處理方式,就是利用 opentelemetry 對 tracing 的多種支持,獲取全端鏈路;同時自己二次封裝 web-vitals 來觀測前端效能,同時覆蓋 console 的所有方法來獲取前端工程師在browser 打印的 log。這使得 Sentry 不僅能進行異常監控,還能提供一定程度的可觀測性。
(寫到這裡才發現,之前忘記寫有關Sentry如何實現tracing...)
根據我們之前的 demo 可以知道,在NodeJS runtime 的時候,可以透過覆蓋、攔截 http
/https
方法來修改headers,增加 tracing 相關資料來實現請求鏈路串接。
而在前端,需要考慮是利用 web 原生的 fetch
方法來發起請求、還是基於 XMLHttpRequest 的 axios
來發起請求,來引入不同的 instrumentation,如下:
fetch
,應使用@opentelemetry/instrumentation-fetch
來自動攔截axios
,則應使用 @opentelemetry/instrumentation-xml-http-request
這些工具會自動將 tracing 資訊加入到 HTTP headers 中,確保前端請求能與後端服務進行有效的 trace 追蹤。接下來我們兩者都引入,把fetch
和 axios
都 demo一遍。
目前還沒有針對 web 的 opentelemetry sdk,所以我們要手動註冊相關的 provider 和 exporter:
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const traceProvider = new WebTracerProvider({ resource });
const traceExporter = new OTLPTraceExporter({
// otel collecter trace data endpoint
url: 'http://localhost:4318/v1/traces',
});
接著,我們需要註冊 span 處理器並將其添加到 trace provider 中:
import {
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-web';
const spanProcessor = new SimpleSpanProcessor(traceExporter);
traceProvider.addSpanProcessor(spanProcessor);
traceProvider.register();
然後就是全局註冊我們需要的 instrumentation,目前就是先使用 FetchInstrumentation
和 XMLHttpRequestInstrumentation
:
// Auto-instrumentations
registerInstrumentations({
instrumentations: [
getWebAutoInstrumentations(),
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [
new RegExp(/http:\/\/localhost:3030\/.*/),
],
}),
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [
new RegExp(/http:\/\/localhost:3030\/.*/),
],
}),
],
tracerProvider: traceProvider,
});
而propagateTraceHeaderCorsUrls
設定是為了指定對該 host 的請求增加 TraceHeader,這是 tracing 中至關重要的一步!!
一樣,我們必須在入口文件引入並執行,才能確保完全覆蓋全局:
import { initOtel } from './otel';
initOtel();
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
最後,我們利用直接就已經完成的 opentelemetry collector + Jaeger 的本地 docker-compose,來當作輸出和可視化 tracing data 的服務;同時,也運行之前的 NodeJS +Otel 服務,來查看全鏈路的請求:
可以看到,不論是 fetch 還是 axios,都被納入 trace 鏈路裡面。
透過本篇,我們了解到 Opentelemetry 在前端的作用,無論是請求鏈路或者是效能監控,幾乎都是透過 tracing 的方式來實現。同時也實作了前端接入 Opentelemetry 後,與後端交互的請求鏈路監控。
本文程式碼可以在此 Github repository中查看。
老師,舉手
我想看看裡面span有什麼內容!
在一個完整的 trace 追蹤鏈路中,前端(client side)通常是發起請求的源頭。當前端接入 OpenTelemetry 的 trace 追蹤時,它將會成為該 trace 中的第一個 span,為後端服務的調用提供上游背景。
這是非常正確的,我舉個例子,後端有 tracing 了。後端的我們可以知道每個請求的完整鏈路,甚至每個請求對應到哪個後端服務各自哪些版本跟設定。但我們卻不知道那使用者用的前端是哪個版本?對應的一些資訊是未知的。有沒有可能出現的問題,其實最後只是前端使用者用的版本不對或者瀏覽器版本或者地區問題 XD
雷N大大快別這麼說
在這個 demo 中,後端接收到的headers長這樣:
{
host: 'localhost:3030',
connection: 'keep-alive',
traceparent: '00-342df3a5573d6fb02a8230d2f81d54b7-7e25883e29c82ec6-01',
'sec-ch-ua-platform': '"macOS"',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
'sec-ch-ua': '"Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
'sec-ch-ua-mobile': '?0',
accept: '*/*',
origin: 'http://localhost:5173',
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:5173/',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7'
}