AI Tools Compared

Use Claude 3.5 Sonnet for designing instrumentation strategies that span multiple services, GitHub Copilot for generating boilerplate span and metric code while you code, or ChatGPT 4o for quick debugging when span attributes are missing or traces don’t correlate. This guide walks through how each tool handles OpenTelemetry context propagation, sampling decisions, and SDK configuration across Python, Go, and Node.js applications.

OpenTelemetry’s Instrumentation Challenge

Effective observability requires consistent span naming, proper trace context propagation between services, and meaningful attributes on every span. Most teams struggle with:

AI assistants can generate correct instrumentation patterns, but they need context about your system topology and observability goals. Claude excels at this design phase. Copilot works well for routine span additions. ChatGPT helps debug context propagation issues.

Claude 3.5 Sonnet: Designing Observable Services

Claude understands that instrumentation is an architectural decision. Provide your service topology and Claude generates a consistent naming scheme and context propagation strategy.

Example prompt:

I have a Node.js Express API that calls a Python background service
via RabbitMQ, which queries PostgreSQL and calls a third-party
payment API. I need distributed traces that show the full request
path from user browser to database. Design an OpenTelemetry strategy.

Claude’s output includes:

// Express middleware that extracts trace context from HTTP headers
app.use((req, res, next) => {
  const tracer = getTracer('express-server');
  const span = tracer.startSpan('http.server.request', {
    attributes: {
      'http.method': req.method,
      'http.url': req.url,
      'http.target': req.path,
      'http.host': req.hostname,
      'http.client_ip': req.ip,
      'http.user_agent': req.get('user-agent'),
    },
  });

  // Propagate context to downstream services
  const baggage = propagation.getActiveBaggage() || new W3CBaggagePropagation();
  baggage.setEntry('user_id', { value: req.user.id });
  baggage.setEntry('api_version', { value: 'v2' });

  context.with(trace.setSpan(context.active(), span), () => {
    res.on('finish', () => {
      span.setAttributes({
        'http.status_code': res.statusCode,
        'http.response_time_ms': Date.now() - req.startTime,
      });
      span.end();
    });
    next();
  });
});

Claude also generates the message queue instrumentation to maintain trace context:

// RabbitMQ producer: inject trace context into message headers
async function publishJob(job) {
  const tracer = getTracer('rabbitmq-producer');
  const span = tracer.startSpan('messaging.publish', {
    attributes: { 'messaging.system': 'rabbitmq', 'messaging.destination': 'job_queue' },
  });

  const carrier = {};
  propagation.inject(context.active(), carrier, defaultTextMapSetter);

  await channel.sendToQueue('job_queue', Buffer.from(JSON.stringify(job)), {
    headers: { traceparent: carrier.traceparent },
  });

  span.end();
}

// Python consumer: extract trace context from message headers
def process_job(ch, method, properties, body):
    from opentelemetry.trace import get_tracer
    from opentelemetry.propagators import TraceContextPropagator
    from opentelemetry.trace.propagation.tracecontext import TraceContextPropagator

    headers = dict(properties.headers or {})
    ctx = TraceContextPropagator().extract(headers)

    tracer = get_tracer(__name__)
    with tracer.start_as_current_span('messaging.process', attributes={
        'messaging.system': 'rabbitmq',
        'messaging.operation': 'receive'
    }) as span:
        # Process job; trace context automatically flows downstream
        db_result = query_database()
        call_payment_api()

Strengths of Claude:

Limitations:

Best for: Initial observability architecture, migrating from old instrumentation, onboarding new team members to tracing best practices.

GitHub Copilot: Routine Instrumentation While Coding

Copilot excels at generating repetitive instrumentation code. When you start typing span creation, Copilot completes the pattern.

Real example in VS Code:

# You type:
def process_payment(order_id, amount):
    tracer = get_tracer(__name__)
    with tracer.start_as_current_span("payment.process") as span:
        # Copilot suggests:
        span.set_attribute("order.id", order_id)
        span.set_attribute("payment.amount", amount)
        span.set_attribute("payment.currency", "USD")

        # Validate order
        order = fetch_order(order_id)
        span.set_attribute("order.status", order.status)

Strengths:

Weaknesses:

Best for: Adding instrumentation to existing code, completing repetitive span attributes, generating metric counters and histograms.

ChatGPT 4o: Debugging Broken Traces

When traces are missing context or don’t correlate between services, ChatGPT helps diagnose the issue. It’s particularly good at explaining context propagation problems.

Example interaction:

User: "My traces show the Express service and Python worker as
separate traces. They should be connected. The job ID is in the
message but not in the trace context."

ChatGPT 4o suggests:
1. Check that RabbitMQ headers are being injected with trace context
2. Verify Python consumer is extracting from message headers, not just env vars
3. Ensure the propagator matches (W3C Trace Context vs Jaeger)
4. Check that the Python consumer uses context.with() to activate the span

ChatGPT walks through the debugging process systematically, which is faster than searching docs.

Strengths:

Weaknesses:

Best for: Troubleshooting broken traces, explaining why spans aren’t correlating, understanding context propagation concepts.

Tool Comparison Table

Feature Claude Copilot ChatGPT 4o
Span naming consistency 9/10 6/10 7/10
Context propagation design 9/10 3/10 8/10
Attribute schema design 8/10 7/10 6/10
Sampling strategy advice 8/10 4/10 7/10
Real-time completion No Yes No
Cost per developer/month $0-20 $10-19 $20 (Plus)
Debugging broken traces Good Poor Excellent
Multi-service topology understanding Excellent Fair Good

Instrumentation Patterns by Tool

Pattern 1: HTTP Server Instrumentation (Claude)

Claude generates the complete middleware with proper error handling:

func tracingMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx, span := tracer.Start(r.Context(), "http.server.request",
      trace.WithAttributes(
        attribute.String("http.method", r.Method),
        attribute.String("http.target", r.RequestURI),
        attribute.String("http.host", r.Host),
      ))
    defer span.End()

    // Propagate trace context to downstream calls
    newReq := r.WithContext(ctx)

    rr := &responseRecorder{ResponseWriter: w}
    next.ServeHTTP(rr, newReq)

    span.SetAttributes(
      attribute.Int("http.status_code", rr.statusCode),
      attribute.String("http.status_text", http.StatusText(rr.statusCode)),
    )
  })
}

Pattern 2: Database Query Instrumentation (Copilot)

Copilot quickly completes db query spans:

def execute_query(query, params):
    with tracer.start_as_current_span("db.statement") as span:
        span.set_attribute("db.system", "postgresql")
        span.set_attribute("db.name", "orders_db")
        span.set_attribute("db.operation", "SELECT")
        span.set_attribute("db.statement", query[:50])  # truncate for PII

        start = time.time()
        result = cursor.execute(query, params)
        duration = (time.time() - start) * 1000

        span.set_attribute("db.client.connections.usage", 5)
        return result

Pattern 3: Message Queue Instrumentation (Claude)

Claude ensures context propagation across the queue:

# Producer
def send_message(topic, message, context_data):
    carrier = {}
    propagation.inject(context_data, carrier, defaultTextMapSetter)

    producer.send(topic, value=json.dumps({
        'data': message,
        'trace_context': carrier
    }))

# Consumer
def consume_messages():
    for msg in consumer:
        payload = json.loads(msg.value)
        ctx = propagation.extract(payload['trace_context'])

        with tracer.start_as_current_span("queue.process", attributes={
            'messaging.system': 'kafka'
        }, context=ctx) as span:
            process_job(payload['data'])

Sampling Strategies

Claude can design sampling strategies that balance cost and observability:

Head-based sampling (simple but loses context):

sampler = ParentBased(TraceIDRatioBased(0.1))  # 10% of all traces

Tail-based sampling (preserves errors but requires collector):

# Jaeger or OTel Collector config
exporters:
  - name: jaeger
    sampling:
      policies:
      - type: error
        threshold: 1  # Always sample errors
      - type: latency
        threshold: 100ms  # Sample slow requests
      - type: probabilistic
        threshold: 0.01  # 1% of other requests

Configuration Across Environments

Claude generates environment-specific configurations:

# production: verbose for errors, sparse for happy path
OTEL_TRACES_SAMPLER=jaeger_remote
OTEL_TRACES_SAMPLER_ARG=http://otel-collector:14268/sampling

# staging: 10% of all traces for cost control
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1

# development: 100% sampling to catch all issues
OTEL_TRACES_SAMPLER=always_on

Conclusion

Use Claude for designing your observability strategy and generating context propagation patterns across multiple services. Use Copilot for routine span and metric completion while actively coding. Use ChatGPT when traces break and you need to debug context propagation issues. The combination gives you architectural rigor upfront, fast development during implementation, and effective debugging when things go wrong.