top of page

Tutorial 9: Production Observability for Agents

  • Contributor
  • 4 days ago
  • 3 min read

Agents in production fail in ways you didn't see in development. Observability is how you find and fix what you didn't anticipate.

What You'll Build

A production observability setup: tracing every agent step, alerting on anomalies, dashboards showing health.

Step 1: Trace Every Run (15 min)

def agent(task, trace_id=None):
    trace_id = trace_id or str(uuid4())
    
    log_event("agent_start", {
        "trace_id": trace_id,
        "task": task,
        "timestamp": datetime.now(),
    })
    
    try:
        # ... agent loop ...
        log_event("agent_complete", {
            "trace_id": trace_id,
            "result": result,
            "steps": len(trace),
        })
    except Exception as e:
        log_event("agent_failed", {
            "trace_id": trace_id,
            "error": str(e),
        })
        raise

Every run has a trace ID. Find it later.

Step 2: Trace Each Step (15 min)

def execute_step(trace_id, step_num, tool, input):
    log_event("step_start", {
        "trace_id": trace_id,
        "step": step_num,
        "tool": tool,
        "input": input,
    })
    
    start = time.time()
    
    try:
        result = TOOL_FN[tool](**input)
        log_event("step_complete", {
            "trace_id": trace_id,
            "step": step_num,
            "result_size": len(str(result)),
            "latency_ms": (time.time() - start) * 1000,
        })
        return result
    except Exception as e:
        log_event("step_failed", {
            "trace_id": trace_id,
            "step": step_num,
            "error": str(e),
        })
        raise

Every step's inputs, outputs, errors, latency.

Step 3: Structured Logging (15 min)

Don't log strings. Log structured data:

def log_event(event_type, attributes):
    logger.info(json.dumps({
        "event": event_type,
        "service": "agent",
        "timestamp": datetime.now().isoformat(),
        **attributes,
    }))

Structured logs query well. Strings don't.

Step 4: Send to Observability Platform (varies)

For production:

  • Datadog APM: out-of-the-box distributed tracing

  • Honeycomb: strong for high-cardinality events

  • OpenTelemetry + Tempo/Jaeger: open source

Each call becomes a span; spans nest into traces.

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def execute_step(tool, input):
    with tracer.start_as_current_span(f"step.{tool}") as span:
        span.set_attribute("tool", tool)
        span.set_attribute("input.summary", summarize(input))
        
        try:
            return TOOL_FN[tool](**input)
        except Exception as e:
            span.record_exception(e)
            raise

Step 5: Build the Dashboard (varies)

Key metrics for an agent:

Real-time:
- Active agents (concurrent runs)
- Average task duration
- Step count distribution
- Tool usage frequency

24-hour:
- Total tasks
- Success rate
- Failure rate by cause
- Cost
- Median latency

Trends (7-day):
- Pass rate over time
- Cost trend
- Top failing tasks

Visible. Updated. Watched.

Step 6: Alert on Anomalies (15 min)

Set alerts:

- name: Agent failure rate elevated
  condition: failure_rate > 10% for 5 minutes
  action: Page oncall

- name: Agent cost spike
  condition: hourly_cost > 3x average
  action: Notify team

- name: Agent latency degraded
  condition: p95_latency > 30s for 10 minutes
  action: Notify team

Alerts before issues become incidents.

Step 7: Capture Failure Traces (15 min)

def on_agent_failure(trace_id, error, full_trace):
    # Save the entire trace for debugging
    save_failure({
        "trace_id": trace_id,
        "error": str(error),
        "full_trace": full_trace,
        "timestamp": datetime.now(),
    })

Failures get rich detail. Investigate later.

Step 8: User-Visible Trace ID (10 min)

When something goes wrong:

# In the API response
if error:
    return {
        "error": "Something went wrong",
        "trace_id": trace_id,
    }

User reports the trace ID; support looks up exact details.

Step 9: Sample for Quality Review (15 min)

import random

def maybe_sample(trace):
    # Sample 1% for human review
    if random.random() < 0.01:
        queue_for_review(trace)

Random sampling catches issues automated checks miss.

Step 10: Replay Failures (varies)

For debugging:

def replay_failure(trace_id):
    failure = load_failure(trace_id)
    
    # Re-run with same input
    new_trace = agent(failure["task"])
    
    # Compare
    return compare_traces(failure["full_trace"], new_trace)

Replay catches whether you've actually fixed something.

What You Just Did

You have production-grade observability. Every agent action is traced; failures are captured; anomalies alert; debugging is tractable.

Common Failure Modes

Logs as strings. Hard to query. Always structured.

No alerts. Issue runs for hours; nobody notices.

No trace ID. User report → can't find the run.

Sample size 0. Pure automated metrics; misses qualitative issues.

Slow log ingestion. Real-time issues invisible until tomorrow.

bottom of page