OpenTelemetry Tracing API for JavaScript

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:

shell
# 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:

js
async function redisGet(key) {
  return await client.get(key)
}

Step 2: Add Span Wrapper

Wrap the operation with a span:

js
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:

js
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:

js
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:

js
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
js
// 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

js
// 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:

js
// 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:

js
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

js
// 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:

js
// 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

js
span.addEvent('log', {
  'log.severity': 'error',
  'log.message': 'User authentication failed',
  'user.id': '123',
  'auth.method': 'oauth',
})

Business events

js
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

js
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

js
// Explicitly set success status
span.setStatus({ code: otel.SpanStatusCode.OK })

Error status

js
try {
  // operation that might fail
} catch (err) {
  span.setStatus({
    code: otel.SpanStatusCode.ERROR,
    message: String(err),
  })
}

Conditional status

js
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

js
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

js
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

js
// 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

js
// Get the currently active span
const activeSpan = otel.trace.getSpan(otel.context.active())

if (activeSpan) {
  activeSpan.addEvent('processing.started')
}

Manual propagation

js
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

js
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

js
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

What's next?