OpenTelemetry and Distributed Tracing in JavaScript
In our Configuring OpenTelemetry in Ruby blog post, we showed how to configure OpenTelemetry in a Ruby on Rails backend. In this post, we’ll cover how to configure OpenTelemetry in the front-end JavaScript in order to measure performance of browser and mobile devices and how to configure distributed tracing to work across the frontend and back end telemetry collection.
Let’s dive in!
The OpenTelemetry JavaScript packages for Node.js and browsers
The packages for OpenTelemetry supporting both Node.js apps and for desktop or mobile browser support can be found at https://github.com/open-telemetry/opentelemetry-js
OpenTelemetry Components used in this example
Resource
A Resource captures information about the entity for which telemetry is recorded.
SemanticResourceAttributes
OpenTelemetry standardizes the naming of certain attributes attached to telemetry collected. These standardized names are called Semantic Conventions in OpenTelemetry.
Propagator, B3Propagator
In order to pass contextual data across any services collecting OpenTelemetry data, you use Propagators that understand how to serialize and deserialize the data between services.
The contextual data transported by the Propagator is called Baggage. There are different formats for Baggage and each service collecting OpenTemetry should use the same baggage format. In this example, the B3 format is used.
Web Trace Provider
The Web TraceProvider supports the automatic tracing of the browser.
Span Processor, Exporter
The Span Processor handles processing of the traces when the trace is finished, including how to export the telemetry data.
WebVitals, DocumentLoader
These are additional telemetry collection libraries that capture telemetry for Core Web Vitals metrics.
Package Installation
You’ll first want to install the packages via `npm` or the appropriate method for your application.
In this example, we’ll be using the following packages in our `package.json`:
@opentelemetry/api
@opentelemetry/context-zone
@opentelemetry/exporter-collector
@opentelemetry/instrumentation-document-load
@opentelemetry/propagator-b3
@opentelemetry/sdk-trace-base
@opentelemetry/sdk-trace-web
Create otel-loader.ts
Imports
// OpenTelemetry
import * as otelCore from "@opentelemetry/core"
import * as otelApi from "@opentelemetry/api"
// Otel Setup (Trace Provider, Processor, Exporter, ...)
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { CollectorTraceExporter } from '@opentelemetry/exporter-collector'
import { registerInstrumentations } from '@opentelemetry/instrumentation'
import { ZoneContextManager } from "@opentelemetry/context-zone"
// Plugins
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'
import { WebVitalsInstrumentation } from './instrumentation/web-vitals-instrumentation'
import { B3Propagator, B3InjectEncoding } from "@opentelemetry/propagator-b3";
Initialization
After importing, we’ll initialize and configure the package in an `initOpenTelemetry()` function
export function initOpenTelemetry() {
// setup Propagator
const propagator = new otelCore.CompositePropagator({
propagators: [
new B3Propagator(),
new B3Propagator({ injectEncoding: B3InjectEncoding.MULTI_HEADER })
]
})
otelApi.propagation.setGlobalPropagator(propagator)
// setup Collector
// Replace YOUR_OPENTELEMETRY_COLLECTOR_ENDPOINT with the URL that accepts OTLP
const collectorOptions = {
url: "https://YOUR_OPENTELEMETRY_COLLECTOR_ENDPOINT/v1/traces",
headers: {
"content-type": "application/json"
}
}
// setup Span Resource
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "scout_apm",
[SemanticResourceAttributes.SERVICE_NAMESPACE]: "scout",
[SemanticResourceAttributes.SERVICE_INSTANCE_ID]: "web",
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.VERSION || "0.0.0",
})
// init provider and exporter
const provider = new WebTracerProvider({ resource })
const exporter = new CollectorTraceExporter( collectorOptions )
// Add Span Processor to provider
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
// The maximum queue size. After the size is reached spans are dropped.
maxQueueSize: 100,
// The maximum batch size of every export. It must be smaller or equal to maxQueueSize.
maxExportBatchSize: 50,
// The interval between two consecutive exports
scheduledDelayMillis: 500,
// How long the export can run before it is canceled
exportTimeoutMillis: 30000,
}))
// Instrumentation
registerInstrumentations({
instrumentations: [
new DocumentLoadInstrumentation(),
new WebVitalsInstrumentation(),
],
tracerProvider: provider,
})
provider.register({
contextManager: new ZoneContextManager().enable() as otelApi.ContextManager,
propagator
})
// Baggage Init
const baggage =
otelApi.propagation.getBaggage(otelApi.context.active()) ||
otelApi.propagation.createBaggage()
baggage.setEntry("sessionId", { value: "session-id-value" })
otelApi.propagation.setBaggage(otelApi.context.active(), baggage)
}
Wrapping up
Now we can load our OpenTelemetry code wherever we want:
import { initOpenTelemetry } from 'otel-loader'
initOpenTelemetry()
That’s all there is for collecting telemetry data with distributed tracing connecting browser traces with back-end application traces! Learn about Scout’s path toward full-stack observability at scoutapm.com/observability.
dave@scoutapp.com