Implementing observability in Angular applications presents unique challenges. Understanding how users experience your application and identifying performance bottlenecks requires specialized tools and approaches.
This guide covers implementing OpenTelemetry in Angular applications, with practical code examples for instrumentation, data collection, and integration with observability backends. You'll learn how to track performance metrics, monitor user interactions, and troubleshoot common implementation issues.
OpenTelemetry for Angular Applications
OpenTelemetry Angular refers to the implementation of the OpenTelemetry framework within Angular applications. OpenTelemetry is an open-source observability framework for collecting telemetry data—metrics, logs, and traces—from distributed systems.
When implemented in Angular applications, OpenTelemetry enables:
- Tracking of HTTP requests and responses
- Monitoring of component lifecycle events
- Measurement of key performance metrics
- Collection of custom business events
- Correlation between frontend issues and backend services
This instrumentation provides visibility into your application's behavior, performance patterns, and potential issues.
How to Install and Configure OpenTelemetry Dependencies
Here's how to add OpenTelemetry to your Angular project, starting with the necessary package installation.
Installing Required NPM Packages
First, install the core OpenTelemetry packages for web applications:
npm install @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/context-zone @opentelemetry/instrumentation @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-xml-http-request @opentelemetry/exporter-collector
These packages provide:
@opentelemetry/api
: Core API for instrumenting your code@opentelemetry/sdk-trace-web
: SDK implementation for web browsers@opentelemetry/context-zone
: Zone.js integration for context propagation@opentelemetry/instrumentation-*
: Auto-instrumentation for common operations@opentelemetry/exporter-collector
: Data export to OpenTelemetry collectors
When installing these packages, consider the following:
- Version Compatibility: Ensure all OpenTelemetry packages have compatible versions. It's recommended to use the same version number across all OpenTelemetry packages to avoid integration issues.
- Angular Version Compatibility: Different versions of Angular may require specific versions of OpenTelemetry packages. As a general rule:
- Angular 12+: Compatible with OpenTelemetry 1.0+
- Angular 9-11: Use OpenTelemetry 0.24.0 or later
- Earlier Angular versions may require older OpenTelemetry versions
- Zone.js Considerations: Since Angular uses Zone.js for change detection, the
@opentelemetry/context-zone
package is crucial as it ensures proper context propagation within Angular's Zone.js environment. - Bundle Size Impact: These packages will increase your bundle size. Consider using code splitting and lazy loading to mitigate this impact. You can also implement custom webpack configurations to optimize the bundle:
// webpack.config.js optimizations
module.exports = {
// ... other webpack configuration
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
opentelemetry: {
test: /[\\/]node_modules[\\/](@opentelemetry)[\\/]/,
name: 'opentelemetry',
priority: 10,
reuseExistingChunk: true,
},
},
},
},
};
- Development vs. Production: Consider configuring OpenTelemetry differently based on your environment. For example, you might use more verbose tracing in development and a more efficient sampling approach in production.
After installation, you should verify the packages are correctly installed:
npm list @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/context-zone
With these packages installed, you're ready to create and configure your tracing service.
How to Create an OpenTelemetry Tracing Service
Next, create a service to initialize and configure OpenTelemetry. This service handles the core setup of the tracing infrastructure:
// tracing.service.ts
import { Injectable } from '@angular/core';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { CollectorTraceExporter } from '@opentelemetry/exporter-collector';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
@Injectable({
providedIn: 'root'
})
export class TracingService {
constructor() {
this.initTracing();
}
private initTracing(): void {
// Define service information through resource attributes
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'angular-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
});
// Configure where to send the traces
const exporter = new CollectorTraceExporter({
url: 'http://your-collector-endpoint/v1/traces',
});
// Create the trace provider with our service resource info
const provider = new WebTracerProvider({ resource });
// Configure how spans are processed and exported
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
// Auto-instrumentation for common web operations
registerInstrumentations({
instrumentations: [
new DocumentLoadInstrumentation(),
new FetchInstrumentation({
clearTimingResources: true,
}),
new XMLHttpRequestInstrumentation(),
],
tracerProvider: provider,
});
// Register the provider to make it the active implementation
// Using ZoneContextManager enables context propagation with Angular's Zone.js
provider.register({
contextManager: new ZoneContextManager(),
});
}
}
This service performs several crucial functions:
- Resource Definition:
- Creates a
Resource
object that identifies your service - The
SERVICE_NAME
andSERVICE_VERSION
attributes are the minimum required to properly identify your application in traces - You can add more attributes like deployment environment, region, or instance ID
- Creates a
- Exporter Configuration:
- Sets up a
CollectorTraceExporter
that sends data to your tracing backend - The URL should point to your OpenTelemetry Collector endpoint
- You can add authentication headers, custom headers, and other HTTP options
- In production, you'll typically use HTTPS with proper authentication
- Sets up a
- Tracer Provider Setup:
- Creates a
WebTracerProvider
specifically designed for browser environments - Associates the defined resources with the provider
- The provider is the core component that creates tracers for your application
- Creates a
- Span Processor Configuration:
- Adds a
BatchSpanProcessor
to efficiently process spans - The batch processor collects spans and sends them in batches to reduce network overhead
- You can configure batch size, export frequency, and queue size
- For debugging, you can use
SimpleSpanProcessor
instead for immediate exports
- Adds a
- Auto-Instrumentation Registration:
- Registers built-in instrumentations for document load, fetch, and XMLHttpRequest
- Each instrumentation can be configured with specific options:
DocumentLoadInstrumentation
: Traces page load performanceFetchInstrumentation
: Traces all fetch API requestsXMLHttpRequestInstrumentation
: Traces all XHR requests
- These auto-instrumentations capture data without manual code changes
- Provider Registration:
- Registers the provider as the global tracer provider
- Sets up the
ZoneContextManager
to properly propagate context in Angular's Zone.js - This is critical for maintaining trace context across asynchronous operations in Angular
For a production service, consider adding:
- Error Handling:
try {
// Initialization code
} catch (error) {
console.error('Failed to initialize tracing:', error);
// Fallback or recovery logic
}
- Environment-Specific Configuration:
const isDevelopment = !environment.production;
const sampler = isDevelopment
? new AlwaysOnSampler() // Sample all traces in development
: new TraceIdRatioBasedSampler(0.1); // Sample 10% in production
const provider = new WebTracerProvider({
resource,
sampler
});
- Performance Optimization:
// Increase batch size in production for performance
const batchSize = environment.production ? 100 : 10;
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
maxExportBatchSize: batchSize,
scheduledDelayMillis: environment.production ? 5000 : 1000
}));
This service is the foundation of your tracing infrastructure and should be initialized early in your application's lifecycle.
### Integrating the Tracing Service with Angular Module
Now that you have the tracing service, integrate it into your Angular application by adding it to the app module:
```typescript
// app.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TracingService } from './services/tracing.service';
// Factory function to initialize tracing
export function initializeTracing(tracingService: TracingService) {
return () => { /* Tracing is initialized in the constructor */ };
}
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [
TracingService,
{
provide: APP_INITIALIZER,
useFactory: initializeTracing,
deps: [TracingService],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
This integration ensures that OpenTelemetry is properly initialized before your application starts. Here's a breakdown of what's happening:
- APP_INITIALIZER Token:
- Angular's
APP_INITIALIZER
token allows you to register functions that will run during app initialization - These functions run before your application bootstraps and renders
- This ensures OpenTelemetry is fully configured before any application code executes
- Angular's
- Factory Function Pattern:
- The
initializeTracing
factory function returns another function - This returned function is executed during initialization
- Since our
TracingService
initializes in its constructor, the function body is empty - For services that require async initialization, you would return a Promise here
- The
- Dependencies Injection:
- The
deps
array tells Angular what to inject into the factory function - By specifying
TracingService
, Angular will instantiate it and pass it to the factory - This creates a single instance of the service that's shared across the application
- The
- Multi Provider:
- The
multi: true
option tells Angular that this isn't the onlyAPP_INITIALIZER
- This allows multiple initialization functions to coexist
- Each will be executed during the application startup phase
- The
For more complex applications, consider these advanced integration patterns:
- Conditional Initialization based on Environment:
export function initializeTracing(tracingService: TracingService, env: EnvironmentService) {
return () => {
if (env.enableTracing) {
// Only initialize tracing if enabled in this environment
return tracingService.initialize();
}
return Promise.resolve();
};
}
// In providers array:
{
provide: APP_INITIALIZER,
useFactory: initializeTracing,
deps: [TracingService, EnvironmentService],
multi: true
}
- Async Initialization with Explicit Promise:
export function initializeTracing(tracingService: TracingService) {
return () => {
return tracingService.asyncInitialize().catch(err => {
console.error('Failed to initialize tracing:', err);
// Return resolved promise to continue app initialization despite tracing failure
return Promise.resolve();
});
};
}
- Lazy Loading Module Support: For applications using lazy-loaded modules, you might need to register additional instrumentation:
// feature.module.ts
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { FeatureTracingService } from './feature-tracing.service';
@NgModule({
providers: [FeatureTracingService]
})
export class FeatureModule {
constructor(
@Optional() @SkipSelf() parentModule: FeatureModule,
featureTracingService: FeatureTracingService
) {
if (parentModule) {
throw new Error('FeatureModule is already loaded.');
}
// Register additional instrumentation for this module
featureTracingService.registerInstrumentation();
}
}
By properly integrating the tracing service with your Angular module, you ensure that OpenTelemetry is initialized at the right time in your application's lifecycle, providing consistent and reliable instrumentation.
Tracking Component Lifecycle Events with Custom Spans
To get even more insights, you can track component lifecycle events:
// monitored.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { trace, context, Span } from '@opentelemetry/api';
@Component({
selector: 'app-monitored',
templateUrl: './monitored.component.html'
})
export class MonitoredComponent implements OnInit, OnDestroy {
private tracer = trace.getTracer('angular-components');
private componentSpan: Span;
ngOnInit(): void {
this.componentSpan = this.tracer.startSpan('MonitoredComponent.lifecycle');
// You can add attributes to your span
this.componentSpan.setAttribute('component.route', window.location.pathname);
// Track expensive operations
this.trackExpensiveOperation();
}
trackExpensiveOperation(): void {
const operationSpan = this.tracer.startSpan('expensive-calculation');
try {
// Your expensive operation here
const result = this.calculateSomething();
operationSpan.setAttribute('operation.result', result);
} catch (error) {
operationSpan.recordException(error);
} finally {
operationSpan.end();
}
}
calculateSomething(): number {
// Some calculation
return 42;
}
ngOnDestroy(): void {
if (this.componentSpan) {
this.componentSpan.end();
}
}
}
Implementing User Interaction Tracking
User interactions are critical to understanding how people use your application. Here's how to track them:
// button.component.ts
import { Component } from '@angular/core';
import { trace } from '@opentelemetry/api';
@Component({
selector: 'app-button',
template: '<button (click)="handleClick()">Click me</button>'
})
export class ButtonComponent {
private tracer = trace.getTracer('user-interactions');
handleClick(): void {
const clickSpan = this.tracer.startSpan('button-click');
try {
// Your click handler logic
this.doSomething();
clickSpan.setAttribute('interaction.success', true);
} catch (error) {
clickSpan.recordException(error);
clickSpan.setAttribute('interaction.success', false);
} finally {
clickSpan.end();
}
}
doSomething(): void {
// Business logic here
}
}
Intercepting and Monitoring HTTP Requests
Angular's HttpClient can be wrapped to provide automatic tracing:
// http-interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap, finalize } from 'rxjs/operators';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
@Injectable()
export class TelemetryHttpInterceptor implements HttpInterceptor {
private tracer = trace.getTracer('http-requests');
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const span = this.tracer.startSpan(`HTTP ${req.method} ${req.url}`);
// Add request details as span attributes
span.setAttribute('http.method', req.method);
span.setAttribute('http.url', req.url);
return next.handle(req).pipe(
tap(
event => {
if (event instanceof HttpResponse) {
span.setAttribute('http.status_code', event.status);
if (event.status >= 400) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: `HTTP Error ${event.status}`
});
}
}
},
error => {
if (error instanceof HttpErrorResponse) {
span.setAttribute('http.status_code', error.status);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
}
span.recordException(error);
}
),
finalize(() => {
span.end();
})
);
}
}
Then add this interceptor to your app module:
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TelemetryHttpInterceptor } from './interceptors/http-interceptor';
@NgModule({
// ...
providers: [
// ...
{ provide: HTTP_INTERCEPTORS, useClass: TelemetryHttpInterceptor, multi: true }
]
})
export class AppModule { }
Configuring Exporters for Observability Backends
Once telemetry data is collected, it needs to be exported to an observability backend for analysis. Below are configuration examples for different backend systems:
Last9 Exporter Configuration
At Last9, we’ve built an observability platform for AI-native teams—bringing metrics, logs, and traces into one place. We support high-cardinality observability at scale and integrate with OpenTelemetry and Prometheus for correlated monitoring and alerting.
To send data from OpenTelemetry to Last9, you’ll need to configure the exporter with your specific endpoint and authentication details.
// Configure for Last9
const exporter = new CollectorTraceExporter({
url: 'https://your-last9-endpoint/v1/traces',
headers: {
'Authorization': 'Bearer YOUR_LAST9_API_KEY'
}
});
The CollectorTraceExporter
class accepts various configuration options:
url
: The Last9 endpoint where traces will be sentheaders
: Authentication and additional HTTP headersconcurrencyLimit
: Controls how many concurrent exports can occur (defaults to 15)timeoutMillis
: How long to wait before timing out requests (defaults to 15000ms)
You can also configure additional options like TLS/SSL settings if your Last9 deployment requires secure connections:
// Configure with TLS options
const exporter = new CollectorTraceExporter({
url: 'https://your-last9-endpoint/v1/traces',
headers: {
'Authorization': 'Bearer YOUR_LAST9_API_KEY'
},
// For custom cert validation if needed
clientKeyPem: fs.readFileSync('./client-key.pem', 'utf8'),
clientCertPem: fs.readFileSync('./client-cert.pem', 'utf8'),
});
After setting up the exporter, add it to a span processor and then register it with your tracer provider:
// Create and register a BatchSpanProcessor with the Last9 exporter
const batchProcessor = new BatchSpanProcessor(exporter, {
// Customize batch processing options
maxQueueSize: 100, // Maximum queue size (default: 100)
maxExportBatchSize: 25, // Maximum batch size (default: 512)
scheduledDelayMillis: 5000, // How often to export (default: 5000ms)
exportTimeoutMillis: 30000 // Export timeout (default: 30000ms)
});
// Add the processor to your tracer provider
provider.addSpanProcessor(batchProcessor);
Configuration for Jaeger
Jaeger is a popular open-source distributed tracing system that works well with OpenTelemetry. Developed by Uber and now part of the Cloud Native Computing Foundation (CNCF), Jaeger provides powerful tracing visualization and analysis features.
To configure OpenTelemetry to export data to Jaeger, you have a few options:
- Using the Collector Exporter for Jaeger's HTTP endpoint:
// Configure for Jaeger HTTP endpoint
const exporter = new CollectorTraceExporter({
url: 'http://localhost:14268/api/traces'
});
- Using the dedicated Jaeger Exporter (for more Jaeger-specific features):
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
// Configure the Jaeger exporter
const jaegerExporter = new JaegerExporter({
// Jaeger agent configuration
host: 'localhost', // Default
port: 6832, // Default UDP port for Jaeger agent
// Or use the collector directly
// endpoint: 'http://localhost:14268/api/traces',
// Service metadata
maxPacketSize: 65000, // UDP packet size limit
serviceName: 'angular-frontend'
});
// Add to your tracer provider
provider.addSpanProcessor(new BatchSpanProcessor(jaegerExporter));
When configuring for Jaeger, consider these important points:
- Authentication: If your Jaeger instance requires authentication, add the necessary headers to the exporter configuration.
- Sampling: Jaeger works well with sampling to reduce volume for high-traffic applications.
- Local Development: For local development, you can run Jaeger using Docker:
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one:latest
Once configured, you can access the Jaeger UI at http://localhost:16686
to visualize and analyze your traces.
Configuration for Zipkin
Zipkin is another well-established open-source distributed tracing system. Created by Twitter, it offers a lightweight approach to tracing with an emphasis on ease of use.
To export traces to Zipkin with OpenTelemetry, you'll need to install the dedicated Zipkin exporter package:
npm install @opentelemetry/exporter-zipkin
Then configure the exporter in your tracing service:
// Configure for Zipkin
import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
const zipkinExporter = new ZipkinExporter({
url: 'http://localhost:9411/api/v2/spans', // Default Zipkin collector endpoint
serviceName: 'angular-frontend', // Your service name
// Optional configuration
headers: {}, // Additional HTTP headers if needed
timeout: 10000 // Timeout in milliseconds
});
// Add to your tracer provider
provider.addSpanProcessor(new BatchSpanProcessor(zipkinExporter));
Zipkin configuration considerations:
- Service Name: The
serviceName
parameter is particularly important as it helps identify your application in Zipkin's UI. - Mapping to Zipkin Format: OpenTelemetry automatically handles the conversion from OpenTelemetry's data model to Zipkin's format.
- Running Zipkin: For local development, you can run Zipkin using Docker:
docker run -d -p 9411:9411 openzipkin/zipkin
After setup, you can view your traces in the Zipkin UI at http://localhost:9411
.
For production deployments of both Jaeger and Zipkin, you'd typically:
- Set up the collectors behind a load balancer
- Configure TLS/SSL for secure communication
- Implement proper authentication mechanisms
- Consider using the OpenTelemetry Collector as a middleware to batch, process, and route your telemetry data
Diagnosing and Resolving Common OpenTelemetry Implementation Issues
Even with a solid setup, you might run into some issues. Let's tackle the most common ones:
1. No Data Being Exported
Problem: You've set up everything, but no data is showing up in your backend.
Solution:
- Check that your exporter URL is correct
- Look for CORS issues in the browser console
- Verify network connectivity to your collector
- Make sure the correct headers are set for authentication
// Add debug logging to your exporter
const exporter = new CollectorTraceExporter({
url: 'http://your-collector-endpoint/v1/traces',
headers: { /* Your headers */ },
// Add this to see what's happening
getExportRequestHeaders: () => {
console.log('Sending traces to collector');
return {};
}
});
2. Zone.js Compatibility Issues
Problem: Errors related to Zone.js when initializing OpenTelemetry.
Solution: Make sure you're using the correct version of Zone.js compatible with your Angular version:
npm list zone.js @angular/core
If needed, update your Zone.js version:
npm install zone.js@0.11.x
3. Performance Impact
Problem: Adding OpenTelemetry has slowed down your application.
Solution:
- Use sampling to reduce the volume of data:
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
// Sample only 20% of traces
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.2)
});
const provider = new WebTracerProvider({
resource,
sampler
});
- Batch your spans to reduce network overhead:
// Increase batch size and timeout
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
maxExportBatchSize: 100,
scheduledDelayMillis: 500
}));
4. Memory Leaks
Problem: Spans aren't being properly closed, causing memory leaks.
Solution: Always end your spans, even in error cases:
let span;
try {
span = tracer.startSpan('operation');
// Do work
} catch (error) {
if (span) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
}
throw error;
} finally {
if (span) {
span.end();
}
}
Recommended Practices for Effective OpenTelemetry Implementation
To maximize the benefits of OpenTelemetry in your Angular applications, follow these best practices:
To get the most out of OpenTelemetry in your Angular applications:
1. Create a Semantic Naming Convention
Establish a consistent naming pattern for your spans and attributes:
Entity Type | Naming Convention | Example |
---|---|---|
Component Spans | ComponentName.lifecycle |
UserProfile.lifecycle |
HTTP Requests | HTTP METHOD path |
HTTP GET /api/users |
User Interactions | element-type.action |
button.click |
Custom Business Events | business-domain.event |
checkout.completed |
2. Balance Detail and Volume
Be selective about what you trace to avoid overwhelming your system:
What to Trace | What to Skip |
---|---|
Key user journeys | Every mouse movement |
Performance-critical operations | Trivial operations |
Error-prone areas | Stable, well-tested code |
Business-critical flows | Background refreshes |
3. Add Context to Your Spans
Make your traces more useful by adding context:
// Add user context (anonymized)
span.setAttribute('user.id_hash', hashUserId(userId));
span.setAttribute('user.type', userType);
// Add business context
span.setAttribute('order.value', orderTotal);
span.setAttribute('order.items_count', itemCount);
// Add technical context
span.setAttribute('browser.name', getBrowserName());
span.setAttribute('app.version', appVersion);
Angular Router Integration for Navigation Tracking
Tracking router navigation is essential for understanding user journeys through your application. Here's how to create a service that traces Angular router events:
// router-tracing.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { trace, context, Span } from '@opentelemetry/api';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RouterTracingService {
private tracer = trace.getTracer('angular-router');
private activeNavigationSpan: Span | undefined;
constructor(private router: Router) {
this.setupRouterTracing();
}
private setupRouterTracing(): void {
// Handle navigation start
this.router.events.pipe(
filter(event => event instanceof NavigationStart)
).subscribe((event: NavigationStart) => {
this.activeNavigationSpan = this.tracer.startSpan(`route change to ${event.url}`);
this.activeNavigationSpan.setAttribute('router.url', event.url);
this.activeNavigationSpan.setAttribute('router.navigation_id', event.id);
});
// Handle successful navigation
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.navigation_end', true);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
// Handle navigation cancellation
this.router.events.pipe(
filter(event => event instanceof NavigationCancel)
).subscribe((event: NavigationCancel) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.cancelled', true);
this.activeNavigationSpan.setAttribute('router.cancel_reason', event.reason);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
// Handle navigation errors
this.router.events.pipe(
filter(event => event instanceof NavigationError)
).subscribe((event: NavigationError) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.error', true);
this.activeNavigationSpan.recordException(event.error);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
}
}
The Angular Router is a critical part of single-page applications. Here's how to integrate OpenTelemetry with it:
// router-tracing.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { trace, context, Span } from '@opentelemetry/api';
import { filter } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RouterTracingService {
private tracer = trace.getTracer('angular-router');
private activeNavigationSpan: Span | undefined;
constructor(private router: Router) {
this.setupRouterTracing();
}
private setupRouterTracing(): void {
// Handle navigation start
this.router.events.pipe(
filter(event => event instanceof NavigationStart)
).subscribe((event: NavigationStart) => {
this.activeNavigationSpan = this.tracer.startSpan(`route change to ${event.url}`);
this.activeNavigationSpan.setAttribute('router.url', event.url);
this.activeNavigationSpan.setAttribute('router.navigation_id', event.id);
});
// Handle successful navigation
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.navigation_end', true);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
// Handle navigation cancellation
this.router.events.pipe(
filter(event => event instanceof NavigationCancel)
).subscribe((event: NavigationCancel) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.cancelled', true);
this.activeNavigationSpan.setAttribute('router.cancel_reason', event.reason);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
// Handle navigation errors
this.router.events.pipe(
filter(event => event instanceof NavigationError)
).subscribe((event: NavigationError) => {
if (this.activeNavigationSpan) {
this.activeNavigationSpan.setAttribute('router.error', true);
this.activeNavigationSpan.recordException(event.error);
this.activeNavigationSpan.end();
this.activeNavigationSpan = undefined;
}
});
}
}
Add this service to your app module providers and it will automatically trace all route changes.
Core Web Vitals and Performance Metrics Collection
Beyond tracing, OpenTelemetry can help you collect valuable performance metrics:
// performance.service.ts
import { Injectable } from '@angular/core';
import { trace } from '@opentelemetry/api';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
@Injectable({
providedIn: 'root'
})
export class PerformanceService {
private tracer = trace.getTracer('performance');
measureFirstContentfulPaint(): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
if (entry.name === 'first-contentful-paint') {
const span = this.tracer.startSpan('FCP');
span.setAttribute('performance.fcp.time_ms', entry.startTime);
span.end();
}
}
observer.disconnect();
});
observer.observe({ type: 'paint', buffered: true });
}
measureLargestContentfulPaint(): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
// We only care about the latest LCP value
const lcpEntry = entries[entries.length - 1];
const span = this.tracer.startSpan('LCP');
span.setAttribute('performance.lcp.time_ms', lcpEntry.startTime);
span.setAttribute('performance.lcp.element', lcpEntry.element?.tagName || 'unknown');
span.end();
observer.disconnect();
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}
measureCumulativeLayoutShift(): void {
let cumulativeScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cumulativeScore += entry.value;
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
// Create a span after the page is fully loaded
window.addEventListener('load', () => {
// Wait a bit to capture more layout shifts
setTimeout(() => {
const span = this.tracer.startSpan('CLS');
span.setAttribute('performance.cls.score', cumulativeScore);
span.end();
}, 5000);
});
}
}
Summary and Next Steps
Implementing OpenTelemetry in Angular applications provides detailed visibility into application behavior and performance. This instrumentation helps identify issues earlier, understand user interactions better, and correlate frontend experiences with backend services.
For effective implementation:
- Start with critical user paths
- Maintain consistent naming conventions
- Balance instrumentation detail with performance impact
- Gradually expand coverage as you grow more familiar with the framework
Additional Resources
FAQs
What is the difference between OpenTelemetry and traditional logging?
Traditional logging captures specific events or errors that developers explicitly code. OpenTelemetry, on the other hand, provides a more comprehensive view of your application by automatically capturing distributed traces, metrics, and logs. It gives you context around how requests flow through your system, allowing you to see the relationships between different parts of your application.
Will OpenTelemetry slow down my Angular application?
When implemented correctly, OpenTelemetry adds minimal overhead to your application. Using techniques like sampling and batching can further reduce its impact. The benefits of the insights gained typically outweigh the small performance cost.
How does OpenTelemetry work with existing monitoring tools?
OpenTelemetry is designed to be vendor-agnostic. It provides a standardized way to collect telemetry data that can then be exported to virtually any observability backend. This means you can use OpenTelemetry with your existing monitoring stack, whether that's Last9, Prometheus, Jaeger, or other tools.
Do I need to instrument every part of my application?
No, you don't need to instrument everything. Start with the most critical paths in your application, such as user authentication, checkout processes, or performance-critical features. You can always add more instrumentation later as needed.
How can I filter sensitive data from my traces?
OpenTelemetry allows you to implement processors that can modify spans before they're exported. You can use these to filter out sensitive information:
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
// Create a custom processor to filter sensitive data
class SensitiveDataProcessor implements SpanProcessor {
onStart(span, parentContext) {}
onEnd(span) {
// Remove sensitive attributes
if (span.attributes['http.url'] && span.attributes['http.url'].includes('/auth')) {
span.attributes['http.url'] = '[REDACTED]';
}
// Remove PII
if (span.attributes['user.email']) {
span.attributes['user.email'] = '[REDACTED]';
}
}
shutdown() {
return Promise.resolve();
}
}
// Add the processor to your provider
provider.addSpanProcessor(new SensitiveDataProcessor());