動態

詳情 返回 返回

OpenTelemetry WebSocket 監控終極方案:打通最後一公里 - 動態 詳情

概述

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) return
  const [traceId, parentSpanId] = parseTraceParent(traceParent)
  const tracer = trace.getTracer('Browser')
  // 創建SpanContext
  const spanContext: SpanContext = {
    traceId: traceId,
    spanId: parentSpanId,
    traceFlags: TraceFlags.SAMPLED,
    // traceState: new TraceState(),
    isRemote: true,
  }
  // 包裝SpanContext為Span
  const parentSpan = trace.wrapSpanContext(spanContext)
  // 創建父級上下文 - 修正這一行
  const parentContext = trace.setSpan(context.active(), parentSpan)
  // 創建並啓動子span
  const 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 應用的請求通信有了端到端的可觀測,整個通信過程的性能耗細節一覽無餘。

Add a new 評論

Some HTML is okay.