作者:来自 Elastic Carly Richmond
了解前端监测与后端的区别,以及 OpenTelemetry 中客户端 Web 监测的当前状态。
DevOps、SRE 和软件工程团队都需要遥测数据来了解其基础设施和全栈应用的运行状况。我们过去已经介绍过如何使用 OpenTelemetry(OTel)对多种语言生态中的后端服务进行监测。然而,对于前端工具,团队通常仍依赖 RUM agents,或者遗憾地完全没有进行监测,这是因为理解前端运行状况所需的指标存在细微差异。
在这篇博客中,我们将讨论浏览器客户端监测的当前状态,并通过一个示例展示如何使用 OpenTelemetry 浏览器监测对一个简单的 JavaScript 前端进行监测。此外,我们还将介绍 baggage propagators 如何帮助我们通过将后端追踪与前端信号连接起来,构建完整的应用运行图。如果你想直接查看代码,请访问这个 repo。
应用概览
我们在本文中使用的应用叫做 OTel Record Store,是一个用 Svelte 和 JavaScript 编写的简单 Web 应用(我们的实现也兼容其他 Web 框架),它与 Java 后端通信。前后端都将遥测信号发送到 Elastic 后端。
眼尖的读者会注意到,我们前端的信号是通过一个代理和采集器传递的。代理的作用是确保填充适当的跨域请求头,以便信号能够传输到 Elastic,同时也出于传统的考虑,比如安全性、隐私和访问控制。
`
1. events {}
3. http {
5. server {
7. listen 8123;
9. # Traces endpoint exposed as example, others available in code repo
10. location /v1/traces {
11. proxy_pass http://host.docker.internal:4318;
12. # Apply CORS headers to ALL responses, including POST
13. add_header 'Access-Control-Allow-Origin' 'http://localhost:4173' always;
14. add_header 'Access-Control-Allow-Methods' 'POST, OPTIONS' always;
15. add_header 'Access-Control-Allow-Headers' 'Content-Type' always;
16. add_header 'Access-Control-Allow-Credentials' 'true' always;
18. # Preflight requests receive a 204 No Content response
19. if ($request_method = OPTIONS) {
20. return 204;
21. }
22. }
23. }
24. }
`AI写代码
虽然采集器也可以用于添加请求头,但在这个示例中我们让它执行传统任务,如路由和处理。
前提条件
这个示例需要一个 Elastic 集群,可以通过 start-local 本地运行,或使用 Elastic Cloud 或 Serverless。这里我们使用 Elastic Serverless 中的 Managed OLTP 端点。无论使用哪种方式,你都需要指定几个关键的环境变量,这些变量列在 .env-example 文件中:
`
1. ELASTIC_ENDPOINT=https://my-elastic-endpoint:443
2. ELASTIC_API_KEY=my-api-key
`AI写代码
运行应用程序
要运行我们的示例,请按照项目 README 中的步骤操作,简要总结如下:
`
1. # Terminal 1: backend service, proxy and collector
2. docker-compose build
3. docker-compose up
5. # Terminal 2: frontend and sample telemetry data
6. cd records-ui
7. npm install
8. npm run generate
`AI写代码
Java 后端监测
我们不会详细介绍如何使用 EDOT 对 Java 服务进行监测,因为 elastic-otel-java README 中已有很好的入门指南。这里的示例仅用于展示在调查 UI 问题时非常重要的上下文传播。你只需知道我们使用自动监测,通过 OpenTelemetry 协议(即 OTLP)发送日志、指标和追踪信息,所用环境变量如下:
`
1. OTEL_RESOURCE_ATTRIBUTES=service.version=1,deployment.environment=dev
2. OTEL_SERVICE_NAME=record-store-server-java
3. OTEL_EXPORTER_OTLP_ENDPOINT=$ELASTIC_ENDPOINT
4. OTEL_EXPORTER_OTLP_HEADERS="Authorization=ApiKey ${ELASTIC_API_KEY}"
5. OTEL_TRACES_EXPORTER=otlp
6. OTEL_METRICS_EXPORTER=otlp
7. OTEL_LOGS_EXPORTER=otlp
`AI写代码
然后使用 -javaagent 选项初始化监测:
`ENV JAVA_TOOL_OPTIONS="-javaagent:./elastic-otel-javaagent-1.2.1.jar"` AI写代码
客户端监测
现在我们已经确定了前提条件,接下来深入了解我们简单 Web 应用的监测代码。虽然我们会分部分讲解实现细节,完整的解决方案可在 frontend.tracer.ts 中查看。
OpenTelemetry 客户端监测现状
撰写本文时,OpenTelemetry JavaScript SDK 对指标和追踪已稳定支持,日志功能仍在开发中,因此可能会有破坏性变更,具体见其文档:
| Traces | Metrics | Logs |
|---|---|---|
| Stable | Stable | Development |
与许多其他 SDK 不同的是,有说明警告浏览器端的客户端监测仍属实验性质,且大部分尚未确定规范。它可能会发生破坏性变更,许多功能如支持测量 Google Core Web Vitals 的插件仍在开发中,这在 Client Instrumentation SIG 项目板中有所体现。后续章节我们将展示信号采集的示例,以及浏览器特有的监测,包括文档加载、用户交互和 Core Web Vitals 采集。
资源定义
在对 Web UI 进行监测时,需要将我们的 UI 定义为 OpenTelemetry 资源。资源的定义是产生遥测信息的实体。我们希望将 UI 视为系统中的一个实体,与其他实体交互,可以用以下代码指定:
`
1. // Defines a Resource to include metadata like service.name, required by Elastic
2. import { resourceFromAttributes, detectResources } from '@opentelemetry/resources';
4. // Experimental detector for browser environment
5. import { browserDetector } from '@opentelemetry/opentelemetry-browser-detector';
7. // Provides standard semantic keys for attributes, like service.name
8. import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
10. const detectedResources = detectResources({ detectors: [browserDetector] });
11. let resource = resourceFromAttributes({
12. [ATTR_SERVICE_NAME]: 'records-ui-web',
13. 'service.version': 1,
14. 'deployment.environment': 'dev'
15. });
16. resource = resource.merge(detectedResources);
`AI写代码
服务需要一个唯一标识符,这在所有 SDK 中都是通用的。不同于其他实现的是包含了 browserDetector,当它与我们定义的资源属性合并时,会添加浏览器属性,如平台、品牌(例如 Chrome 与 Edge)以及是否使用了移动浏览器:
在跨度(spans)和错误中包含这些信息,对于诊断应用程序与某些浏览器(比如我当工程师时遇到的 Internet Explorer 🤦)的兼容性问题非常有用。
日志
传统上,前端工程师依赖喜欢的浏览器的开发者工具控制台查看日志。由于 UI 日志消息只能在浏览器内访问,而不像后端服务那样转发到某个文件,我们在排查用户问题时就失去了对这部分资源的可见性。
OpenTelemetry 定义了 exporter 的概念,允许我们将信号发送到特定目的地,比如日志。
`
1. // Get logger and severity constant imports
2. import { logs, SeverityNumber } from '@opentelemetry/api-logs';
4. // Provider and batch processor for sending logs
5. import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
7. // Export logs via OTLP
8. import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
10. // Configure logging to send to the collector via nginx
11. const logExporter = new OTLPLogExporter({
12. url: 'http://localhost:8123/v1/logs' // nginx proxy
13. });
15. const loggerProvider = new LoggerProvider({
16. resource: resource, // see resource initialisation above
17. processors: [new BatchLogRecordProcessor(logExporter)]
18. });
20. logs.setGlobalLoggerProvider(loggerProvider);
`AI写代码
一旦 provider 初始化完成,我们需要获取 logger 来发送追踪到 Elastic,而不是使用老掉牙的 console.log('Help!'):
`
1. // Example gets logger and sends a message to Elastic
2. const logger = logs.getLogger('default', '1.0.0');
3. logger.emit({
4. severityNumber: SeverityNumber.INFO,
5. severityText: 'INFO',
6. body: 'Logger initialized'
7. });
`AI写代码
它们现在可以在 Discover 和 Logs 视图中看到,方便我们在调查和处理事件时搜索相关的故障信息:
追踪 - traces
追踪在诊断 UI 问题时的优势不仅在于了解 Web 应用内部发生了什么,还在于看到调用背后复杂服务的连接关系和耗时。要对基于 Web 的应用进行监测,我们需要使用 WebTraceProvider 和 OTLPTraceExporter,方法类似于日志和指标的 exporter 使用方式:
`
1. /* Packages for exporting traces */
3. // Import the WebTracerProvider, which is the core provider for browser-based tracing
4. import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
6. // BatchSpanProcessor forwards spans to the exporter in batches to prevent flooding
7. import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
9. // Import the OTLP HTTP exporter for sending traces to the collector over HTTP
10. import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
12. // Configure the OTLP exporter to talk to the collector via nginx
13. const exporter = new OTLPTraceExporter({
14. url: 'http://localhost:8123/v1/traces' // nginx proxy
15. });
17. // Instantiate the trace provider and inject the resource
18. const provider = new WebTracerProvider({
19. resource: resource,
20. spanProcessors: [
21. // Send each completed span through the OTLP exporter
22. new BatchSpanProcessor(exporter)
23. ]
24. });
`AI写代码
接下来我们需要注册 provider。在 Web 环境中稍有不同的是配置传播方式。OpenTelemetry 中的上下文传播(Context propagation)指的是在服务和进程之间传递上下文的概念,在我们这里,它允许我们将 Web 信号与后端服务信号关联起来。通常这都是自动完成的。下面代码片段中,有 3 个概念帮助我们实现传播:
`
1. // This context manager ensures span context is maintained across async boundaries in the browser
2. import { ZoneContextManager } from '@opentelemetry/context-zone';
4. // Context Propagation across signals
5. import {
6. CompositePropagator,
7. W3CBaggagePropagator,
8. W3CTraceContextPropagator
9. } from '@opentelemetry/core';
11. // Provider instantiation code omitted
13. // Register the provider with propagation and set up the async context manager for spans
14. provider.register({
15. contextManager: new ZoneContextManager(),
16. propagator: new CompositePropagator({
17. propagators: [new W3CBaggagePropagator(), new W3CTraceContextPropagator()]
18. })
19. });
`AI写代码
第一个是 ZoneContextManager,它负责在异步操作中传播上下文,比如跨度和追踪。Web 开发者会熟悉 zone.js,这是许多 JS 框架用来提供跨异步任务持久执行上下文的框架。
此外,我们使用 CompositePropagator 组合了 W3CBaggagePropagator 和 W3CTraceContextPropagator,以确保按照 W3C 规范在信号之间传递键值对属性。对于 W3CTraceContextPropagator,它允许按照规范传播 traceparent 和 tracestate HTTP 请求头。
自动监测
开始监测 Web 应用最简单的方法是注册 Web 自动监测模块。撰写本文时,文档指出可以通过这种方式配置以下监测模块:
- @opentelemetry/instrumentation-document-load
- @opentelemetry/instrumentation-fetch
- @opentelemetry/instrumentation-user-interaction
- @opentelemetry/instrumentation-xml-http-request
每个模块的配置可以作为参数传递给 registerInstrumentations,下面的示例展示了如何配置 fetch 和 XMLHTTPRequest 监测:
`
1. // Used to auto-register built-in instrumentations
2. import { registerInstrumentations } from '@opentelemetry/instrumentation';
4. // Import the auto-instrumentations for web, which includes common libraries, frameworks and document load
5. import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
7. // Enable automatic span generation for document load and user click interactions
8. registerInstrumentations({
9. instrumentations: [
10. getWebAutoInstrumentations({
11. '@opentelemetry/instrumentation-fetch': {
12. propagateTraceHeaderCorsUrls: /.*/,
13. clearTimingResources: true
14. },
15. '@opentelemetry/instrumentation-xml-http-request': {
16. propagateTraceHeaderCorsUrls: /.*/
17. }
18. })
19. ]
20. });
`AI写代码
以 @opentelemetry/instrumentation-fetch 监测为例,我们可以看到 HTTP 请求的追踪,传播器还确保跨度能与我们的 Java 后端服务连接,呈现处理请求各阶段所花时间的完整图景:
虽然自动监测是获取常见监测的好方法,但我们也可以直接实例化监测模块,正如本文后续部分将展示的。
文档加载监测
Web 前端特有的一个考虑是加载资源(如图片、JavaScript 文件甚至样式表)所花的时间。资源加载时间过长会影响如 First Contentful Paint 之类的指标,从而影响用户体验。OTel Document Load 监测允许在使用 @opentelemetry/sdk-trace-web 包时自动监测资源加载时间。
只需将该监测模块添加到我们通过 registerInstrumentations 提供给 provider 的 instrumentations 数组中即可:
`
1. // Used to auto-register built-in instrumentations like page load and user interaction
2. import { registerInstrumentations } from '@opentelemetry/instrumentation';
4. // Document Load Instrumentation automatically creates spans for document load events
5. import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
7. // Configuration discussed above omitted
9. // Enable automatic span generation for document load and user click interactions
10. registerInstrumentations({
11. instrumentations: [
12. // Automatically tracks when the document loads
13. new DocumentLoadInstrumentation({
14. ignoreNetworkEvents: false,
15. ignorePerformancePaintEvents: false
16. }),
17. // Other instrumentations omitted
18. ]
19. });
`AI写代码
该配置将创建一个名为 documentLoad 的新追踪,方便我们查看文档内资源的加载时间,类似如下:
每个跨度都会附带元数据,帮助我们识别哪些资源加载时间较长,比如这个图片示例,该资源加载耗时为 837 毫秒:
点击事件
你可能会想为什么我们要捕获用户与 Web 应用的交互,用于诊断。能够看到错误的触发点,在事件处理中有助于建立事件时间线,并确定用户是否真的受到影响,这也是真实用户监测(Real User Monitoring)工具的做法。但如果考虑数字体验监测(Digital Experience Monitoring,DEM)领域,软件团队需要了解应用功能的使用详情,以数据驱动的方式理解用户旅程并改进体验。捕获用户事件对两者都很重要。
OTel 的 UserInteraction 监测用于捕获这些事件。与文档加载监测类似,它依赖 @opentelemetry/sdk-trace-web 包,并且配合 zone-js 和 ZoneContextManager 使用时支持异步操作。
像其他监测模块一样,通过 registerInstrumentations 添加:
`
1. // Used to auto-register built-in instrumentations like page load and user interaction
2. import { registerInstrumentations } from '@opentelemetry/instrumentation';
4. // Automatically creates spans for user interactions like clicks
5. import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
7. // Configuration discussed above omitted
9. // Enable automatic span generation for document load and user click interactions
10. registerInstrumentations({
11. instrumentations: [
12. // User events
13. new UserInteractionInstrumentation({
14. eventNames: ['click', 'input'] // instrument click and input events only
15. }),
16. // Other instrumentations omitted
17. ]
18. });
`AI写代码
它会捕获并标记我们配置的用户事件跨度,利用之前配置的传播器,可以将来自其他资源的跨度与用户事件连接起来,类似下面的示例,当用户在输入框添加搜索词时,我们看到对获取记录的服务调用:
指标
有许多不同的测量指标有助于捕获 Web 应用的可用性和性能的有用指标,比如延迟、吞吐量或 404 错误数量。Google Core Web Vitals 是一套标准指标,供 Web 开发者衡量网站的真实用户体验,包括加载性能、对用户输入的响应性和视觉稳定性。鉴于撰写本文时 OTel 浏览器的 Core Web Vitals 插件仍在待办列表中,我们尝试使用 web-vitals JS 库构建自定义监测,将这些指标作为 OTel 指标捕获。
在 OpenTelemetry 中,你可以通过扩展 InstrumentationBase 创建自定义监测,重写构造函数以创建 MeterProvider、Meter 和 OTLPMetricExporter,从而通过代理将 Core Web Vitals 测量发送到 Elastic,如 web-vitals.instrumentation.ts 所示。下面仅展示了 LCP 计量器以简洁起见,但完整示例包含所有 Web Vitals 指标的测量。
`
1. /* OpenTelemetry JS packages */
2. // Instrumentation base to create a custom Instrumentation for our provider
3. import {
4. InstrumentationBase,
5. type InstrumentationConfig,
6. type InstrumentationModuleDefinition
7. } from '@opentelemetry/instrumentation';
9. // Metrics API
10. import {
11. metrics,
12. type ObservableGauge,
13. type Meter,
14. type Attributes,
15. type ObservableResult,
17. } from '@opentelemetry/api';
19. export class WebVitalsInstrumentation extends InstrumentationBase {
21. // Meter captures measurements at runtime
22. private cwvMeter: Meter;
24. /* Core Web Vitals Measures, LCP provided, others omitted */
25. private lcp: ObservableGauge;
27. constructor(config: InstrumentationConfig, resource: Resource) {
28. super('WebVitalsInstrumentation', '1.0', config);
30. // Create metric reader to process metrics and export using OTLP
31. const metricReader = new PeriodicExportingMetricReader({
32. exporter: new OTLPMetricExporter({
33. url: 'http://localhost:8123/v1/metrics' // nginx proxy
34. }),
35. // Default is 60000ms (60 seconds).
36. // Set to 10 seconds for demo purposes only.
37. exportIntervalMillis: 10000
38. });
40. // Creating Meter Provider factory to send metrics
41. const myServiceMeterProvider = new MeterProvider({
42. resource: resource,
43. readers: [metricReader]
44. });
45. metrics.setGlobalMeterProvider(myServiceMeterProvider);
47. // Create web vitals meter
48. this.cwvMeter = metrics.getMeter('core-web-vitals', '1.0.0');
50. // Initialising CWV metric gauge instruments (LCP given as example, others omitted here)
51. this.lcp = this.cwvMeter.createObservableGauge('lcp', { unit: 'ms', description: 'Largest Contentful Paint' });
52. }
54. protected init(): InstrumentationModuleDefinition | InstrumentationModuleDefinition[] | void {}
56. // Other steps discussed later
57. }
`AI写代码
你会注意到在我们的 LCP 示例中,我们创建了一个 ObservableGauge,通过回调函数在读取时捕获数值。启用自定义监测时,可以这样设置,当触发 LCP 事件时,数值会通过 result.observe 发送:
`
1. /* Web Vitals Frontend package, LCP shown as example*/
2. import { onLCP, type LCPMetric } from 'web-vitals';
4. /* OpenTelemetry JS packages */
5. // Instrumentation base to create a custom Instrumentation for our provider
6. import {
7. InstrumentationBase,
8. type InstrumentationConfig,
9. type InstrumentationModuleDefinition
10. } from '@opentelemetry/instrumentation';
12. // Metrics API
13. import {
14. metrics,
15. type ObservableGauge,
16. type Meter,
17. type Attributes,
18. type ObservableResult,
20. } from '@opentelemetry/api';
22. // Other OTel Metrics imports omitted
24. // Time calculator via performance component
25. import { hrTime } from '@opentelemetry/core';
27. type CWVMetric = LCPMetric | CLSMetric | INPMetric | TTFBMetric | FCPMetric;
29. export class WebVitalsInstrumentation extends InstrumentationBase {
31. /* Core Web Vitals Measures */
32. private lcp: ObservableGauge;
34. // Constructor and Initialization omitted
36. enable() {
37. // Capture Largest Contentful Paint, other vitals omitted
38. onLCP(
39. (metric) => {
40. this.lcp.addCallback((result) => {
41. this.sendMetric(metric, result);
42. });
43. },
44. { reportAllChanges: true }
45. );
46. }
48. // Callback utility to add attributes and send captured metric
49. private sendMetric(metric: CWVMetric, result: ObservableResult<Attributes>): void {
50. const now = hrTime();
52. const attributes = {
53. startTime: now,
54. 'web_vital.name': metric.name,
55. 'web_vital.id': metric.id,
56. 'web_vital.navigationType': metric.navigationType,
57. 'web_vital.delta': metric.delta,
58. 'web_vital.value': metric.value,
59. 'web_vital.rating': metric.rating,
60. // metric specific attributes
61. 'web_vital.entries': JSON.stringify(metric.entries)
62. };
64. result.observe(metric.value, attributes);
65. }
66. }
`AI写代码
要使用我们自己的监测模块,需要像在 frontend.tracer.ts 中对现有 Web 监测模块捕获文档和用户事件一样,注册我们的监测模块:
`
1. registerInstrumentations({
2. instrumentations: [
3. // Other web instrumentations omitted
4. // Custom Web Vitals instrumentation
5. new WebVitalsInstrumentation({}, resource)
6. ]
7. });
`AI写代码
lcp 指标以及我们在 sendMetric 函数中指定的属性将被发送到我们的 Elastic 集群:
这些指标由于兼容性问题不会进入用户体验仪表盘,但我们可以创建一个仪表盘,利用这些数值展示各个核心指标的趋势:
总结
在这篇博客中,我们介绍了浏览器客户端监测的现状,并通过示例展示了如何使用 OpenTelemetry 浏览器监测对一个简单的 JavaScript 前端进行监测。想回顾代码,请查看这里的 repo。如果你有任何问题或想与其他开发者交流,可以加入 Elastic 社区。
开发者资源: