JS `OpenTelemetry` Web SDK:前端分布式追踪与可观测性

各位前端的弄潮儿们,大家好!我是你们的老朋友,今天咱们来聊聊一个让你的前端代码瞬间“透明”的秘密武器——OpenTelemetry Web SDK!

啥?你还不知道OpenTelemetry?没关系,咱们从头开始,保证你听完之后,也能成为追踪大师!

一、 啥是OpenTelemetry? 别怕,真不难!

想象一下,你开发了一个超级复杂的网站,用户点了几个按钮,页面转了几圈,最后报错了!你打开控制台,一堆乱七八糟的报错信息,一脸懵逼,不知道问题出在哪? 这时候,你就需要OpenTelemetry了!

OpenTelemetry,简称OTel,是一个开源的可观测性框架,它提供了一套标准的API、SDK和工具,用来生成、收集、处理和导出遥测数据。 啥是遥测数据? 简单来说,就是你的代码运行过程中产生的各种信息,比如:

  • Traces (追踪): 记录一次请求的完整路径,就像侦探追踪犯人一样,可以告诉你请求经过了哪些服务,每个服务花了多少时间。
  • Metrics (指标): 记录你的代码的性能指标,比如CPU使用率、内存占用率、请求响应时间等等。
  • Logs (日志): 记录你的代码运行过程中的各种事件,比如错误信息、警告信息等等。

OTel的目标就是统一这些遥测数据的格式和标准,让你可以在不同的监控工具中使用它们,不再被厂商锁定。

二、 前端为啥需要OpenTelemetry?

你可能会说,后端服务已经用了OTel,前端还需要吗? 当然需要! 前端代码的复杂性越来越高,单页应用、微前端、各种第三方库,一旦出现问题,定位起来非常困难。

  • 性能瓶颈: 页面加载慢、交互卡顿,你得知道是哪个组件、哪个API请求拖了后腿。
  • 用户体验问题: 用户点击没反应、页面显示错误,你得知道是哪个环节出了问题。
  • 错误追踪: 前端代码的错误,往往难以重现,你需要详细的错误上下文信息。

有了OpenTelemetry,你就可以:

  • 监控页面加载时间、API请求时间、资源加载时间等关键指标。
  • 追踪用户操作路径,了解用户行为。
  • 捕获前端错误,并提供详细的错误信息,方便快速定位问题。
  • 将前端遥测数据与后端遥测数据关联起来,实现端到端的追踪。

三、 OpenTelemetry Web SDK: 你的前端追踪利器

OpenTelemetry Web SDK就是专门为前端应用设计的,它可以帮助你收集和导出前端的遥测数据。

1. 安装

首先,你需要安装OpenTelemetry Web SDK:

npm install @opentelemetry/sdk-web @opentelemetry/auto-instrumentations-web @opentelemetry/exporter-jaeger @opentelemetry/context-zone @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction

或者使用 yarn:

yarn add @opentelemetry/sdk-web @opentelemetry/auto-instrumentations-web @opentelemetry/exporter-jaeger @opentelemetry/context-zone @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction

这里我们安装了几个关键的包:

  • @opentelemetry/sdk-web: OpenTelemetry Web SDK的核心包。
  • @opentelemetry/auto-instrumentations-web: 自动插桩包,可以自动收集一些常见的遥测数据,比如页面加载时间、API请求时间等。
  • @opentelemetry/exporter-jaeger: Jaeger导出器,将遥测数据导出到Jaeger。当然,你也可以选择其他的导出器,比如Zipkin、OTLP等。
  • @opentelemetry/context-zone: Context管理,用于在异步操作中传递上下文信息。
  • @opentelemetry/instrumentation-fetch: Fetch API的插桩,用于追踪API请求。
  • @opentelemetry/instrumentation-document-load: 页面加载时间的插桩。
  • @opentelemetry/instrumentation-user-interaction: 用户交互的插桩,例如点击事件。

2. 初始化

接下来,你需要初始化OpenTelemetry Web SDK:

import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-web';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
import { registerInstrumentations } from '@opentelemetry/instrumentation';

const provider = new WebTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-web-app', // 你的应用名称
  }),
});

const exporter = new JaegerExporter({
  endpoint: 'http://localhost:14268/api/traces', // Jaeger的Endpoint
});

provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register({
  contextManager: new ZoneContextManager(),
});

registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation(),
    new DocumentLoadInstrumentation(),
    new UserInteractionInstrumentation(),
  ],
});

console.log('OpenTelemetry initialized');

这段代码做了几件事:

  • 创建TracerProvider: TracerProvider是OpenTelemetry的核心,它负责创建Tracer。
  • 配置Resource: Resource描述了你的应用的信息,比如应用名称、版本等。
  • 创建Exporter: Exporter负责将遥测数据导出到指定的后端,这里我们使用了JaegerExporter。
  • 添加SpanProcessor: SpanProcessor负责处理Span,这里我们使用了BatchSpanProcessor,它可以将多个Span批量导出,提高性能。
  • 注册全局TracerProvider: 将TracerProvider注册为全局TracerProvider,这样你就可以在任何地方使用Tracer了。
  • 注册 Context Manager: ZoneContextManager 解决异步操作中的上下文传递问题。
  • 注册Instrumentations: 注册自动插桩,这样就可以自动收集一些常见的遥测数据了。

3. 手动创建Span

除了自动插桩,你还可以手动创建Span,来追踪一些自定义的操作。

import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('my-custom-tracer');

function doSomething() {
  const span = tracer.startSpan('doSomething');
  // ... 你的业务代码 ...
  span.end();
}

这段代码创建了一个名为doSomething的Span,你可以将你的业务代码放在span.startSpan()span.end()之间,这样就可以追踪doSomething的执行时间了。

4. 导出数据到Jaeger

确保你已经安装并启动了Jaeger。你可以在Docker中运行Jaeger:

docker run -d -p 16686:16686 -p 14268:14268 jaegertracing/all-in-one:latest

然后在浏览器中访问http://localhost:16686,就可以看到Jaeger的UI界面了。

运行你的前端应用,然后刷新页面,点击一些按钮,你就可以在Jaeger的UI界面中看到你的应用的追踪数据了!

四、 进阶技巧: 玩转OpenTelemetry Web SDK

  • 自定义Attributes: 你可以在Span中添加自定义的Attributes,来记录一些业务相关的信息。

    const span = tracer.startSpan('my-operation');
    span.setAttribute('user.id', '123');
    span.setAttribute('product.id', '456');
    span.end();
  • 添加Events: 你可以在Span中添加Events,来记录一些重要的事件。

    const span = tracer.startSpan('my-operation');
    span.addEvent('log', { message: 'Something happened' });
    span.end();
  • 处理错误: 当你的代码发生错误时,你可以将错误信息记录到Span中。

    try {
      // ... 你的业务代码 ...
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: 2, message: error.message }); // 2 代表 Error
    } finally {
      span.end();
    }
  • 采样: 在高流量的场景下,你可以使用采样来减少遥测数据的量。

    const provider = new WebTracerProvider({
      sampler: new AlwaysOffSampler(), // 关闭采样
      // sampler: new AlwaysOnSampler(),  // 总是采样
      // sampler: new TraceIdRatioBasedSampler(0.5), // 按照比例采样,这里是50%
      resource: new Resource({
      }),
    });
  • 与日志系统集成: 你可以将OpenTelemetry与你的日志系统集成,将Span的Context信息添加到日志中,方便你将追踪数据与日志关联起来。

五、 代码示例: 一个完整的例子

import { WebTracerProvider, BatchSpanProcessor, SimpleSpanProcessor, AlwaysOnSampler } from '@opentelemetry/sdk-web';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { trace } from '@opentelemetry/api';

// 配置
const serviceName = 'my-web-app';
const jaegerEndpoint = 'http://localhost:14268/api/traces';

// 初始化 OpenTelemetry
const provider = new WebTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
  }),
  sampler: new AlwaysOnSampler(), // AlwaysOnSampler 总是采样,方便本地调试
});

// 使用 Jaeger Exporter
const exporter = new JaegerExporter({
  endpoint: jaegerEndpoint,
});

// 添加 Span 处理器
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
  maxQueueSize: 256,
  scheduledDelayMillis: 5000,
}));

// 注册 Context 管理器
provider.register({
  contextManager: new ZoneContextManager(),
});

// 注册 Instrumentations
registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation({
      ignoreUrls: [/localhost:3000/], // 忽略对localhost:3000的请求追踪
      propagateTraceHeaderCorsUrls: [ //  跨域请求需要配置
        'https://your-api-domain.com',
      ],
    }),
    new DocumentLoadInstrumentation(),
    new UserInteractionInstrumentation(),
  ],
});

console.log('OpenTelemetry initialized');

// 获取 Tracer
const tracer = trace.getTracer(serviceName, '1.0.0');

// 模拟一个 API 请求
async function fetchData() {
  const span = tracer.startSpan('fetchData');
  try {
    const response = await fetch('https://rickandmortyapi.com/api/character');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    span.setAttribute('character.count', data.results.length);
    console.log('Data fetched successfully:', data);
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    console.error('Failed to fetch data:', error);
  } finally {
    span.end();
  }
}

// 模拟一个用户交互
function handleClick() {
  const span = tracer.startSpan('handleClick');
  try {
    console.log('Button clicked!');
    // 一些业务逻辑
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    console.error('Error handling click:', error);
  } finally {
    span.end();
  }
}

// 在页面加载完成后执行
document.addEventListener('DOMContentLoaded', () => {
  const button = document.createElement('button');
  button.textContent = 'Fetch Data';
  button.addEventListener('click', fetchData);

  const clickButton = document.createElement('button');
  clickButton.textContent = 'Click Me';
  clickButton.addEventListener('click', handleClick);

  document.body.appendChild(button);
  document.body.appendChild(clickButton);
});

这个例子展示了如何使用OpenTelemetry Web SDK来追踪API请求和用户交互。 你可以在Jaeger的UI界面中看到这些追踪数据。

六、 总结

OpenTelemetry Web SDK是一个强大的工具,可以帮助你提升前端的可观测性,快速定位问题,提升用户体验。 虽然配置起来稍微有点复杂,但是一旦配置完成,你就可以享受到它带来的便利。 赶紧试试吧,让你的前端代码变得“透明”起来!

七、 常见问题

问题 解决方法
追踪数据没有显示在Jaeger中 1. 确保Jaeger已经启动并运行。 2. 检查Jaeger的Endpoint是否配置正确。 3. 检查你的代码中是否正确地初始化了OpenTelemetry Web SDK。 4. 检查你的代码中是否正确地创建和结束了Span。 5. 检查你的采样率是否设置得太低,导致Span没有被采样。 6. 检查浏览器控制台是否有错误信息。 7. 检查是否跨域问题,导致请求头没有正确传递。
跨域请求无法追踪 1. 配置propagateTraceHeaderCorsUrls选项,将你的API域名添加到白名单中。 2. 确保你的API服务器允许跨域请求,并且允许携带Trace相关的Header。
性能问题 1. 使用BatchSpanProcessor来批量导出Span,减少网络请求的次数。 2. 使用采样来减少遥测数据的量。 3. 避免在生产环境中使用AlwaysOnSampler,因为它会收集所有的Span。 4. 检查exporter的配置,例如maxQueueSize和scheduledDelayMillis,根据实际情况进行调整。
异步操作中上下文丢失 1. 确保使用了 ZoneContextManager。 2. 检查你的异步操作是否正确地传递了Context。
某些第三方库无法自动插桩 1. 检查OpenTelemetry是否提供了对该第三方库的插桩。 2. 如果OpenTelemetry没有提供对该第三方库的插桩,你可以尝试手动创建Span来追踪该第三方库的执行时间。 3. 可以考虑贡献你自己的插桩,帮助社区更好地支持该第三方库。
如何在单页应用中正确处理路由变化? 1. 在路由变化时,结束当前的Span,并创建一个新的Span。 2. 可以使用HistoryInstrumentation来自动追踪路由变化。
如何将前端追踪数据与后端追踪数据关联起来? 1. 确保前端和后端都使用了OpenTelemetry,并且使用了相同的TraceId。 2. 通过propagateTraceHeaderCorsUrls将Trace相关的Header传递到后端。 3. 在后端代码中,从请求头中获取TraceId,并将其设置为当前Span的ParentId。 4. 使用相同的ServiceName和Resource Attributes,方便在追踪系统中进行关联。

好了,今天的分享就到这里了!希望对你有所帮助。 记住,可观测性是软件开发的重要组成部分,越早开始使用OpenTelemetry,就能越早发现问题,提升你的代码质量和用户体验! 感谢大家的聆听! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注