Claude Skills Guide

Claude Code OpenTelemetry Tracing Instrumentation Guide

OpenTelemetry has become the industry standard for observability, providing vendor-neutral APIs, SDKs, and tools for collecting distributed traces, metrics, and logs. When combined with Claude Code’s AI assistance, you can rapidly implement comprehensive tracing in your applications without deep prior knowledge of OpenTelemetry internals.

This guide walks you through setting up OpenTelemetry tracing instrumentation using Claude Code as your coding partner.

Why OpenTelemetry Matters for Modern Applications

Modern applications often consist of multiple microservices communicating across networks. When something goes wrong, pinpointing the exact location of a failure can feel like finding a needle in a haystack. OpenTelemetry solves this by providing distributed tracing—a way to follow a request as it travels through your entire system.

Traditional debugging often involves adding log statements, restarting services, and hoping you captured enough information. With OpenTelemetry, every request gets a unique trace ID that follows it through all services, making it trivial to see exactly where time is being spent and where errors occur.

Claude Code accelerates your OpenTelemetry journey by generating boilerplate code, explaining complex concepts, and helping you debug tracing issues when they arise.

Setting Up OpenTelemetry with Claude Code

Initial Project Configuration

Start by describing your tracing needs to Claude. Be specific about your language, framework, and what you want to achieve:

/opentelemetry Set up OpenTelemetry tracing for a Node.js Express API. I want to trace HTTP requests, database queries, and external API calls.

Claude will generate the initial setup, typically including package installation and basic configuration. For Node.js, this might look like:

npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http

Creating the Tracing Setup File

Claude can generate a proper tracing initialization file tailored to your needs:

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'your-service-name',
    [ATTR_SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter: new OTLPTraceExporter(),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start()
  .then(() => console.log('Tracing initialized'))
  .catch((error) => console.error('Error initializing tracing', error));

process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.error('Error terminating tracing', error));
});

Notice that this code uses standard JavaScript, not Liquid template syntax, so no raw tag is needed.

Manual Instrumentation with Custom Spans

While auto-instrumentation covers many common frameworks, you’ll often need to create custom spans for business logic that frameworks don’t automatically capture.

Creating Spans for Business Logic

Describe your business operations to Claude and ask for custom span creation:

/opentelemetry Create spans for our order processing workflow that includes validation, inventory check, payment processing, and notification sending.

Claude will generate code like this:

const { trace, SpanKind, StatusCode } = require('@opentelemetry/api');
const { meter } = require('@opentelemetry/api');

const tracer = trace.getTracer('order-service');
const orderMeter = meter.getMeter('order-service');

async function processOrder(orderData) {
  return tracer.startActiveSpan('processOrder', async (span) => {
    try {
      // Add order ID to trace for correlation
      span.setAttribute('order.id', orderData.id);
      span.setAttribute('order.total', orderData.total);

      // Validation span
      const validationSpan = tracer.startSpan('validateOrder', {
        kind: SpanKind.INTERNAL,
        parent: span,
      });
      const isValid = await validateOrder(orderData);
      validationSpan.setAttribute('validation.result', isValid);
      validationSpan.end();
      
      if (!isValid) {
        span.setStatus({ code: StatusCode.ERROR, message: 'Order validation failed' });
        span.end();
        return { success: false, error: 'Validation failed' };
      }

      // Inventory check span
      const inventorySpan = tracer.startSpan('checkInventory', {
        kind: SpanKind.CLIENT,
        parent: span,
      });
      const inventoryResult = await checkInventory(orderData.items);
      inventorySpan.setAttribute('inventory.available', inventoryResult.available);
      inventorySpan.end();

      if (!inventoryResult.available) {
        span.setStatus({ code: StatusCode.ERROR, message: 'Insufficient inventory' });
        span.end();
        return { success: false, error: 'Insufficient inventory' };
      }

      // Payment processing span
      const paymentSpan = tracer.startSpan('processPayment', {
        kind: SpanKind.CLIENT,
        parent: span,
      });
      const paymentResult = await processPayment(orderData.payment);
      paymentSpan.setAttribute('payment.id', paymentResult.transactionId);
      paymentSpan.setAttribute('payment.status', paymentResult.status);
      paymentSpan.end();

      // Notification span
      const notificationSpan = tracer.startSpan('sendNotification', {
        kind: SpanKind.PRODUCER,
        parent: span,
      });
      await sendNotification(orderData.customerId, 'order_confirmed');
      notificationSpan.end();

      span.setAttribute('order.status', 'completed');
      span.end();
      return { success: true, orderId: orderData.id };
      
    } catch (error) {
      span.setStatus({ 
        code: StatusCode.ERROR, 
        message: error.message 
      });
      span.recordException(error);
      span.end();
      throw error;
    }
  });
}

Adding Custom Attributes

Make your traces more useful by adding relevant attributes:

function addUserContext(span, user) {
  span.setAttribute('user.id', user.id);
  span.setAttribute('user.email', user.email);
  span.setAttribute('user.tier', user.subscriptionTier);
}

function addRequestContext(span, request) {
  span.setAttribute('http.method', request.method);
  span.setAttribute('http.url', request.url);
  span.setAttribute('http.route', request.route?.path || 'unknown');
  span.setAttribute('http.status_code', response.statusCode);
}

Tracing Database Operations

Database queries are often the biggest source of latency. OpenTelemetry auto-instrumentation captures many queries automatically, but custom spans provide more context.

Tracing with Detailed Query Information

/opentelemetry Add detailed tracing for PostgreSQL queries including query text, execution time, and row counts.
const { trace, SpanKind } = require('@opentelemetry/api');

const dbTracer = trace.getTracer('database');

async function tracedQuery(pool, text, params) {
  const span = dbTracer.startSpan('database.query', {
    kind: SpanKind.CLIENT,
    attributes: {
      'db.system': 'postgresql',
      'db.statement': text,
      'db.operation': text.split(' ')[0].toUpperCase(),
    },
  });

  const startTime = Date.now();
  try {
    const result = await pool.query(text, params);
    span.setAttribute('db.row_count', result.rowCount);
    span.setAttribute('db.execution_time_ms', Date.now() - startTime);
    return result;
  } catch (error) {
    span.setStatus({
      code: StatusCode.ERROR,
      message: error.message,
    });
    span.recordException(error);
    throw error;
  } finally {
    span.end();
  }
}

Context Propagation

When requests span multiple services, trace context must propagate through headers.

W3C Trace Context

The W3C Trace Context standard is now the default:

const { propagation, ROOT_CONTEXT } = require('@opentelemetry/api');

// Extract trace context from incoming request
function extractTraceContext(req) {
  const carrier = {
    traceparent: req.headers['traceparent'],
    tracestate: req.headers['tracestate'],
  };
  return propagation.extract(ROOT_CONTEXT, carrier);
}

// Add trace context to outgoing requests
function injectTraceContext(outgoingOptions) {
  propagation.inject(
    trace.getActiveSpan().spanContext(),
    outgoingOptions.headers || (outgoingOptions.headers = {})
  );
  return outgoingOptions;
}

// Usage with HTTP client
async function callDownstreamService(url, data) {
  const span = trace.getActiveSpan();
  const outgoing = injectTraceContext({
    method: 'POST',
    url,
    headers: {},
  });
  
  return fetch(url, {
    ...outgoing,
    body: JSON.stringify(data),
  });
}

Custom Propagators

For systems using custom headers:

const { TextMapPropagator, W3C_TRACE_CONTEXT_PARENT_HEADER } = require('@opentelemetry/api');

class CustomTracePropagator extends TextMapPropagator {
  inject(context, carrier) {
    const spanContext = context.getValue(SPAN_KEY);
    if (!spanContext) return;
    
    carrier['x-trace-id'] = spanContext.traceId;
    carrier['x-span-id'] = spanContext.spanId;
  }

  extract(context, carrier) {
    const traceId = carrier['x-trace-id'];
    const spanId = carrier['x-span-id'];
    
    if (!traceId || !spanId) return context;
    
    const spanContext = new SpanContext({
      traceId: TraceId.fromHex(traceId),
      spanId: SpanId.fromHex(spanId),
      traceFlags: TraceFlags.SAMPLED,
    });
    
    return context.setValue(SPAN_KEY, spanContext);
  }
}

Sampling Strategies

High-throughput applications may need sampling to control trace volume.

Common Sampling Strategies

const { AlwaysSample, AlwaysOffSampler, ParentBasedSampler } = require('@opentelemetry/sdk-trace-base');

// Always sample for development
const devSampler = AlwaysSample;

// Production: only sample 10% of traces
const prodSampler = new ParentBasedSampler({
  root: new TraceIdRatioBased(0.1),
});

// Sample based on specific criteria
const customSampler = new ParentBasedSampler({
  root: new TraceIdRatioBased(0.1),
  onRootSpanStart: (rootSpan) => {
    // Always sample API requests
    if (rootSpan.attributes['http.url']?.includes('/api/')) {
      return AlwaysSample;
    }
    // Skip health checks
    if (rootSpan.attributes['http.url']?.includes('/health')) {
      return AlwaysOffSampler;
    }
    return new TraceIdRatioBased(0.1);
  },
});

Integration with Claude Code for Debugging

When traces reveal performance issues, Claude can help analyze and resolve them.

Analyzing Trace Data

Share your trace data with Claude for analysis:

/opentelemetry Analyze this trace data showing 3 second latency in our checkout flow. The spans show: validateOrder (50ms), checkInventory (2800ms), processPayment (100ms), sendNotification (50ms). What's causing the bottleneck?

Claude will identify that checkInventory is the bottleneck and suggest optimizations like caching inventory data or using asynchronous processing.

Troubleshooting Common Issues

Common problems Claude can help debug:

Best Practices

Naming Conventions

Use consistent, meaningful span names:

// Good: descriptive, consistent naming
'processOrder'
'database.query'
'http.post:/api/checkout'

// Bad: dynamic values in span names
`processOrder-${orderId}`  // Creates too many unique span names
`query-${Math.random()}`    // Absolutely forbidden

Attribute Guidelines

// Use semantic conventions for standard attributes
const { SemanticAttributes } = require('@opentelemetry/semantic-conventions');

span.setAttribute(SemanticAttributes.DB_SYSTEM, 'redis');
span.setAttribute(SemanticAttributes.DB_STATEMENT, 'GET user:123');
span.setAttribute(SemanticAttributes.HTTP_METHOD, 'GET');
span.setAttribute(SemanticAttributes.HTTP_URL, 'https://api.example.com/users');

// Avoid high-cardinality values as attributes
// Bad: span.setAttribute('user.email', user.email); // Too many unique values
// Good: span.setAttribute('user.id', user.id);

Performance Considerations

// Don't create spans in tight loops
// Instead, batch operations

async function processItems(items) {
  const span = tracer.startSpan('processItems');
  try {
    const batchSpan = tracer.startSpan('batchProcessing', { parent: span });
    // Process all items in batch
    await processBatch(items);
    batchSpan.end();
  } finally {
    span.end();
  }
}

// Use span.addEvent for logging-like information
span.addEvent('Processing item', {
  'item.id': itemId,
  'item.status': 'started',
});

Conclusion

OpenTelemetry tracing provides visibility into your application’s behavior across service boundaries. With Claude Code as your partner, you can rapidly implement comprehensive instrumentation without becoming an OpenTelemetry expert. The key is starting simple with auto-instrumentation, then adding custom spans for your specific business logic.

Remember to iterate: start with basic setup, add meaningful attributes, implement proper context propagation, and refine with sampling strategies as your observability needs grow.

Built by theluckystrike — More at zovo.one