OpenTelemetry Tracing API for JavaScript
Prerequisites
Make sure your exporter is configured before you start instrumenting code. Follow Getting started with OpenTelemetry JavaScript first.
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