概述
OpenTelemetry,以下簡稱 OTEL,是由 CNCF 托管的“一站式可觀測性標準”,把指標、鏈路、日志三大信號統一為單一 SDK/API,零侵入地采集從瀏覽器、移動端到后端、容器、云服務的全棧遙測數據,并支持 40+ 后端一鍵導出,讓分布式系統的黑盒瞬間變透明。
OpenTelemetry-JS 是 OpenTelemetry 開源的 JavaScript/TypeScript 觀測框架,可在瀏覽器與 Node.js 中無侵入地采集 Traces、Metrics、Logs,自動埋點 HTTP、Fetch、WebSocket、gRPC、數據庫等調用鏈,一鍵導出至 Jaeger、Prometheus、Zipkin 等后端,實現前端到后端的統一可觀測性。
本文章主要通過參考 opentelemetry-js 相關開源方案,經過代碼編寫以及前端業務自埋點改造,演示 OTEL 前端 Span 如何上報到觀測云,以及基于 OTEL 的前端 Span 上報,如何實現在 WebSocket 應用場景的最后一公里探測的最佳實踐。
眾所周知,OTEL 的前端和后端都是通用的 Span 數據上報方式,而觀測云又兼容 OTEL 協議并且 DataKit 開箱即用支持 OTEL Span 數據的上報,因此對于 WebSocket 應用,基于 OTEL 的后端與前端 Span 埋點監控可以在鏈路層面實現完整的端到端的監控。
前端 Span 上報觀測云實踐
功能特性
- OpenTelemetry SDK 初始化
- 基于 trace parent 創建 span
- OTLP 協議數據導出
- 批量 span 處理
- 分布式追蹤上下文傳播
- TypeScript 支持
代碼說明
otel-span/
├── index.ts # 主入口文件
├── create-span.ts # OpenTelemetry span 創建邏輯
├── package.json # 項目依賴配置
├── tsconfig.json # TypeScript 配置
index.ts
index.ts
?作為主入口文件
import { setupOTelSDK, createSpanWithTraceParent } from './create-span'// 初始化 OpenTelemetry SDK
setupOTelSDK()// 使用traceparent創建span, 可以在請求 request header中獲取
const traceParent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
const spanName = 'test-span'console.log('開始創建 span...')
const span = createSpanWithTraceParent(traceParent, spanName)
console.log('span 創建完成!')// 等待一段時間確保 span 被導出
setTimeout(() => {console.log('程序執行完成')process.exit(0)
}, 2000)
create-span.ts
create-span.ts
?用于創建 span 邏輯
import { Resource } from '@opentelemetry/resources'
import { BatchSpanProcessor, WebTracerProvider } from '@opentelemetry/sdk-trace-web'
import { ZoneContextManager } from '@opentelemetry/context-zone'
import { trace, SpanContext, TraceFlags, context } from '@opentelemetry/api'
import { W3CTraceContextPropagator } from '@opentelemetry/core'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'const setupOTelSDK = () => {const resource = Resource.default().merge(new Resource({'service.name': 'test',}))const tracerProvider = new WebTracerProvider({resource: resource,})const traceExporter = new OTLPTraceExporter({url: 'http://127.0.0.1:9529/otel/v1/traces',headers: {},})const spanProcessor = new BatchSpanProcessor(traceExporter, {// 可選配置參數maxExportBatchSize: 100, // 每批最多處理的span數量scheduledDelayMillis: 1000, // 定期導出的間隔時間(毫秒)})// propagation.setGlobalPropagator(new W3CTraceContextPropagator());// 設置上下文傳播器const contextManager = new ZoneContextManager()tracerProvider.addSpanProcessor(spanProcessor)tracerProvider.register({contextManager,propagator: new W3CTraceContextPropagator(),})trace.setGlobalTracerProvider(tracerProvider)}const parseTraceParent = (traceParent: string) => {const parts = traceParent.split('-')if (parts.length !== 4) throw new Error('Invalid trace_parent format')if (parts[0] !== '00') throw new Error('Unsupported trace_parent version')const traceId = parts[1]const parentSpanId = parts[2]if (traceId.length !== 32) throw new Error('traceId must be 32 characters')if (parentSpanId.length !== 16) throw new Error('parentSpanId must be 16 characters')if (!isHex(traceId)) throw new Error('traceId contains invalid hex characters')if (!isHex(parentSpanId)) throw new Error('parentSpanId contains invalid hex characters')return [traceId, parentSpanId]
}const isHex = (s: string) => {return /^[0-9a-fA-F]+$/.test(s)
}const createSpanWithTraceParent = (traceParent: string, spanName: string) => {if (!traceParent) returnconst [traceId, parentSpanId] = parseTraceParent(traceParent)const tracer = trace.getTracer('Browser')// 創建SpanContextconst spanContext: SpanContext = {traceId: traceId,spanId: parentSpanId,traceFlags: TraceFlags.SAMPLED,// traceState: new TraceState(),isRemote: true,}// 包裝SpanContext為Spanconst parentSpan = trace.wrapSpanContext(spanContext)// 創建父級上下文 - 修正這一行const parentContext = trace.setSpan(context.active(), parentSpan)// 創建并啟動子spanconst childSpan = tracer.startSpan(spanName,{// attributes: {// "parsing time": `${10000/1000}μs`// }},parentContext)try {// console.info(`Child span started with trace_id: ${traceId}`);// 業務邏輯...} finally {childSpan.end()}
}export { setupOTelSDK, createSpanWithTraceParent }
擴展說明
添加自定義屬性
const childSpan = tracer.startSpan(spanName,{attributes: {'custom.attribute': 'value','user.id': '12345','operation.type': 'read'}},parentContext
)
添加事件
childSpan.addEvent('operation.started', {'input.size': inputSize
})
設置狀態
childSpan.setStatus({code: SpanStatusCode.OK,message: 'Operation completed successfully'
})
上報測試
1、克隆或解壓項目
https://github.com/lrwh/observable-demo/tree/main/otel-span
數據上報地址使用觀測云的本地的 DataKit 為例。
2、安裝依賴:npm install
3、開發模式試運行:npm run dev
數據上報服務名為“test”,span 名稱為“test-span” 。
4、觀測云 DataKit 數據接收與展示
WebSocket 應用場景實戰
場景描述
某平臺已實現基于 OTEL 的后端 Span 的上報,前端三端的監控是基于觀測云的 SDK 進行了集成,也實現了一定意義上的前端RUM數據和和后端 OTEL 的鏈路數據關聯,但是 WebSocket 長連接打破了傳統的請求-響應模式,傳統 HTTP 的 Trace 是請求粒度的,而 WebSocket 連接可能持續數小時,而且重點是 WebSocket 的 Server 端也會發起一些業務數據推送請求到客戶端時,此時僅通過后端的 OTEL 鏈路無法確定數據什么時候推送到的客戶端,以及客戶端的渲染情況表現如何。
方案與原理
首先,觀測云在三端客戶端(web,安卓,IOS)通過自身的 SDK 集成,會生成基于 w3c_traceparent 的 Span,相關的 traceparent 上下文會傳遞到 WebSocket Server 后端鏈路 Span,當后端的 WebSocket Server 推業務請求數據到客戶端時,會繼續傳播 traceparent 上下文給 OTEL 前端 Span,進而通過補充 OTEL 前端 Span 的最后一公里的數據上報,實現整個 websocket 通信的全鏈路監控以及鏈路不同階段調用的耗時情況。
前端自埋點
- 通過前端埋點來探測 WebSocket Server 端什么時間剛好把業務數據請求發送到客戶端
如下圖所示,在前端業務代碼中定義時,后端傳過來 data.traceparent,隨后即執行核心業務代碼創建 span 的操作,即上述“前端 Span 上報觀測云“章節中類似主入口中的index.ts 的 createSpanWithTraceParent 方法。
const span = createSpanWithTraceParent(traceParent, spanName)
也即是最終會調用 create-span.ts 程序文件中的 createSpanWithTraceParent 方法。
- 通過前端埋點來探測 WebSocket Server 端推送業務請求數據到客戶端之后,客戶端什么時候渲染完成
如下圖,同理,也是 WebSocket Server 端 traceparent 數據傳播到客戶端,即調用 createSpanWithTraceParent(traceParent, spanName) 方法來實現。
更多自埋點類似原理,需要自行選擇合適位置進行埋點。
效果展示
- 拓撲展示
- 鏈路展示
總結
基于 OTEL 前端 Span 的數據上報與自定義埋點改造,解決了 WebSocket 應用場景下 WebSocket Server 到客戶端最后一公里的探測問題,從而使 WebSocket 應用的請求通信有了端到端的可觀測,整個通信過程的性能耗細節一覽無余。