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 支持

代码说明

bash 复制代码
otel-span/
├── index.ts              # 主入口文件
├── create-span.ts        # OpenTelemetry span 创建逻辑
├── package.json          # 项目依赖配置
├── tsconfig.json         # TypeScript 配置

index.ts

index.ts 作为主入口文件

javascript 复制代码
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 逻辑

typescript 复制代码
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 }

扩展说明

添加自定义属性

php 复制代码
const childSpan = tracer.startSpan(
  spanName,
  {
    attributes: {
      'custom.attribute': 'value',
      'user.id': '12345',
      'operation.type': 'read'
    }
  },
  parentContext
)

添加事件

arduino 复制代码
childSpan.addEvent('operation.started', {
  'input.size': inputSize
})

设置状态

css 复制代码
childSpan.setStatus({
  code: SpanStatusCode.OK,
  message: 'Operation completed successfully'
})

上报测试

1、克隆或解压项目

github.com/lrwh/observ...

数据上报地址使用观测云的本地的 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 方法。

ini 复制代码
const span = createSpanWithTraceParent(traceParent, spanName)

也即是最终会调用 create-span.ts 程序文件中的 createSpanWithTraceParent 方法。

  • 通过前端埋点来探测 WebSocket Server 端推送业务请求数据到客户端之后,客户端什么时候渲染完成

如下图,同理,也是 WebSocket Server 端 traceparent 数据传播到客户端,即调用 createSpanWithTraceParent(traceParent, spanName) 方法来实现。

更多自埋点类似原理,需要自行选择合适位置进行埋点。

效果展示

  • 拓扑展示
  • 链路展示

总结

基于 OTEL 前端 Span 的数据上报与自定义埋点改造,解决了 WebSocket 应用场景下 WebSocket Server 到客户端最后一公里的探测问题,从而使 WebSocket 应用的请求通信有了端到端的可观测,整个通信过程的性能耗细节一览无余。

相关推荐
可观测性用观测云2 天前
观测云接收 OpenTelemetry Collector 数据最佳实践
监控
SRETALK3 天前
夜莺开源监控,模板函数一览
运维·监控·自动化运维
可观测性用观测云7 天前
使用观测云打造企业级监控告警中心
监控
可观测性用观测云9 天前
DataKit 采集器敏感信息加密最佳实践
监控
可观测性用观测云21 天前
Undertow 可观测性最佳实践
监控
可观测性用观测云23 天前
Promtail 对接日志最佳实践
监控
试着25 天前
零基础学习性能测试第二章-JVM如何监控
jvm·学习·零基础·性能测试·监控