OpenTelemetry Tracing API for JavaScript
This document teaches how to use the OpenTelemetry JavaScript API. To learn how to install and configure the OpenTelemetry JS SDK, see Getting started with OpenTelemetry Node.js and OpenTelemetry for Browsers.
Prerequisites
Before you begin, ensure you have:
- OpenTelemetry JavaScript SDK installed and configured
- Basic understanding of distributed tracing concepts
- A Node.js or browser application ready for instrumentation
OpenTelemetry-JS
OpenTelemetry-JS is the JavaScript implementation of OpenTelemetry. It provides the OpenTelemetry Tracing API which you can use to instrument your application with OpenTelemetry tracing.
Installation
Install the OpenTelemetry API:
# npm
npm install @opentelemetry/api --save
# yarn
yarn add @opentelemetry/api --save
Quickstart
This section demonstrates how to instrument a simple function step by step.
Step 1: Basic Function
Let's instrument the following Redis function:
async function redisGet(key) {
return await client.get(key)
}
Step 2: Add Span Wrapper
Wrap the operation with a span:
const otel = require('@opentelemetry/api')
const tracer = otel.trace.getTracer('app_or_package_name', '1.0.0')
async function redisGet(key) {
return await tracer.startActiveSpan('redis.get', async (span) => {
const value = await client.get(key)
span.end()
return value
})
}
Step 3: Add Error Handling
Record errors and set status code:
async function redisGet(key) {
return await tracer.startActiveSpan('redis.get', async (span) => {
let value
try {
value = await client.get(key)
} catch (exc) {
span.recordException(exc)
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(exc)
})
throw exc
} finally {
span.end()
}
return value
})
}
Step 4: Add Contextual Information
Record contextual information with attributes:
async function redisGet(key) {
return await tracer.startActiveSpan('redis.get', async (span) => {
// Add attributes for better observability
if (span.isRecording()) {
span.setAttributes({
'db.system': 'redis',
'db.operation': 'get',
'db.redis.key': key,
})
}
let value
try {
value = await client.get(key)
span.setStatus({ code: otel.SpanStatusCode.OK })
} catch (exc) {
span.recordException(exc)
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(exc)
})
throw exc
} finally {
span.end()
}
return value
})
}
The operation is now fully instrumented with proper error handling, status codes, and contextual attributes!
Tracer
To start creating spans, you need a tracer. You can create a tracer by providing the name and version of the library or application doing the instrumentation:
const otel = require('@opentelemetry/api')
const tracer = otel.trace.getTracer('app_or_package_name', '1.0.0')
Tracer naming
- Use descriptive names that identify the library or service
- Include version information for better debugging
- Create one tracer per logical component or service
- Use consistent naming across your application
// Good examples
const authTracer = otel.trace.getTracer('auth-service', '2.1.0')
const dbTracer = otel.trace.getTracer('database-layer', '1.0.0')
const apiTracer = otel.trace.getTracer('user-api', '3.2.1')
Creating spans
Once you have a tracer, creating spans is straightforward. Spans represent units of work in your application.
Basic span creation
// Create a span with name and kind
tracer.startActiveSpan(
'operation-name',
{ kind: otel.SpanKind.SERVER },
(span) => {
doSomeWork()
span.end()
},
)
Span kinds
Choose the appropriate span kind based on the operation:
// Server span - for handling incoming requests
tracer.startActiveSpan(
'handle-request',
{ kind: otel.SpanKind.SERVER },
(span) => {
// Handle HTTP request
span.end()
},
)
// Client span - for outgoing requests
tracer.startActiveSpan('api-call', { kind: otel.SpanKind.CLIENT }, (span) => {
// Make API call
span.end()
})
// Internal span - for internal operations
tracer.startActiveSpan(
'process-data',
{ kind: otel.SpanKind.INTERNAL },
(span) => {
// Internal processing
span.end()
},
)
Async operations
For asynchronous operations, ensure proper span lifecycle management:
async function processData() {
return await tracer.startActiveSpan('process-data', async (span) => {
try {
const result = await performAsyncWork()
span.setAttributes({
'operation.result.count': result.length,
'operation.success': true,
})
return result
} catch (error) {
span.recordException(error)
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(error),
})
throw error
} finally {
span.end()
}
})
}
Adding span attributes
To record contextual information, annotate spans with attributes. Attributes provide valuable metadata for debugging and monitoring.
Performance optimization
// Check if span is recording to avoid expensive computations
if (span.isRecording()) {
span.setAttributes({
'http.method': 'GET',
'http.route': '/projects/:id',
'http.status_code': 200,
'user.id': userId,
})
}
Semantic conventions
Use semantic attributes for common operations:
// HTTP operations
span.setAttributes({
'http.method': 'POST',
'http.url': 'https://api.example.com/users',
'http.status_code': 201,
'http.user_agent': userAgent,
})
// Database operations
span.setAttributes({
'db.system': 'postgresql',
'db.statement': 'SELECT * FROM users WHERE id = $1',
'db.operation': 'select',
'db.name': 'production',
})
// Custom business logic
span.setAttributes({
'user.id': '12345',
'order.total': 99.99,
'payment.method': 'credit_card',
})
Adding span events
Annotate spans with events to capture point-in-time occurrences during span execution:
Log events
span.addEvent('log', {
'log.severity': 'error',
'log.message': 'User authentication failed',
'user.id': '123',
'auth.method': 'oauth',
})
Business events
span.addEvent('order.processed', {
'order.id': 'ord_123',
'order.total': 99.99,
'processing.duration_ms': 150,
})
span.addEvent('cache.miss', {
'cache.key': 'user:profile:123',
'cache.ttl': 3600,
})
Exception events
try {
// risky operation
} catch (error) {
span.addEvent('exception', {
'exception.type': error.constructor.name,
'exception.message': error.message,
'exception.stacktrace': error.stack,
})
}
Setting status code
Use status codes to indicate the outcome of operations:
Success status
// Explicitly set success status
span.setStatus({ code: otel.SpanStatusCode.OK })
Error status
try {
// operation that might fail
} catch (err) {
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(err),
})
}
Conditional status
const result = await performOperation()
if (result.success) {
span.setStatus({ code: otel.SpanStatusCode.OK })
} else {
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: result.errorMessage,
})
}
Recording exceptions
OpenTelemetry provides a convenient method to record exceptions, typically used together with setStatus
:
Basic exception
try {
// risky operation
} catch (exc) {
// Record the exception and update the span status
span.recordException(exc)
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: String(exc) })
}
Advanced exception
try {
await processPayment(paymentData)
} catch (exc) {
span.recordException(exc)
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: String(exc) })
span.setAttributes({
'error.type': exc.constructor.name,
'payment.id': paymentData.id,
'payment.amount': paymentData.amount,
})
}
Context management
OpenTelemetry stores the active span in a context and saves the context in pluggable context storage. Context enables automatic parent-child span relationships.
Working with context
// Create a new context with a span
otel.context.with(otel.trace.setSpan(otel.context.active(), span), () => {
// span is active inside this callback
doSomeWork() // Any spans created here will be children of 'span'
})
Getting the active span
// Get the currently active span
const activeSpan = otel.trace.getSpan(otel.context.active())
if (activeSpan) {
activeSpan.addEvent('processing.started')
}
Manual propagation
function processWithContext() {
const currentContext = otel.context.active()
setTimeout(() => {
// Manually propagate context to async callback
otel.context.with(currentContext, () => {
tracer.startActiveSpan('delayed-operation', (span) => {
// This span will be a child of the original active span
span.end()
})
})
}, 1000)
}
Advanced patterns
Conditional instrumentation
function conditionalTrace(operation, shouldTrace = true) {
if (!shouldTrace) {
return operation()
}
return tracer.startActiveSpan('conditional-operation', (span) => {
try {
const result = operation()
span.setStatus({ code: otel.SpanStatusCode.OK })
return result
} catch (error) {
span.recordException(error)
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(error),
})
throw error
} finally {
span.end()
}
})
}
Span decorators
function traced(operationName) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args) {
return tracer.startActiveSpan(operationName, (span) => {
try {
const result = originalMethod.apply(this, args)
span.setStatus({ code: otel.SpanStatusCode.OK })
return result
} catch (error) {
span.recordException(error)
span.setStatus({
code: otel.SpanStatusCode.ERROR,
message: String(error),
})
throw error
} finally {
span.end()
}
})
}
return descriptor
}
}
// Usage
class UserService {
@traced('user.get')
getUser(id) {
return database.findUser(id)
}
}
Querying data
Once your application is instrumented and sending trace data to Uptrace, you can leverage the collected spans for monitoring and debugging:
Span analysis
- Service topology: Visualize how your services communicate
- Performance bottlenecks: Identify slow operations and optimize them
- Error tracking: Monitor error rates and investigate failures
- Dependency mapping: Understand service dependencies and call patterns
Search and filtering
- Operation filtering: Find spans by operation name, service, or duration
- Attribute queries: Search spans using custom attributes and semantic conventions
- Error analysis: Filter spans by status code and exception details
- Performance analysis: Query spans by duration thresholds and percentiles