Observability for Vibe Coders: Logs, Traces, Evals, and Failure Recovery
A practical guide for vibe coders to instrument AI applications and agent loops with structured logging, execution traces, lightweight evals, and deterministic failure recovery.

Contents
- Why traditional APM fails for vibe-coded applications
- The four pillars of AI observability
- Structured logging: What you must capture on every call
- Tracing agent loops and multi-step workflows
- Lightweight evals: How to test prompts before shipping
- Deterministic failure recovery patterns
- Implementation: A production telemetry harness in 80 lines of Python
- FAQ
Why traditional APM fails for vibe-coded applications
When you build software using AI tools like Cursor or code generation models, the development loop feels instantaneous. You describe a feature, generate working code, and wire it into a script or web server.
The illusion shatters the moment your application reaches production.
Traditional Application Performance Monitoring (APM) tools monitor CPU spikes, memory leaks, database connection pools, and HTTP 500 error rates. In standard web software, if code crashes, your APM alerts you with a clear stack trace pointing to a file and line number.
AI workflows fail differently. In our previous guide on when you actually need an agent, we highlighted that introducing probabilistic model loops creates non-deterministic execution paths. When an AI feature fails in production, it often exhibits behaviors that traditional APM tools cannot detect:
- Silent semantic failure: The model returns HTTP 200 with syntactically valid JSON, but the content is wrong, hallucinated, or incomplete.
- Context pollution and prompt drift: As multi-turn sessions accumulate history, the model begins repeating previous mistakes or ignoring core instructions.
- Runaway tool cycles: An agent calls an external API, gets an unexpected schema, tries to repair it by calling another tool, and enters an infinite self-correction loop that burns $50 in API credits in five minutes.
- Tool argument hallucination: The model invokes an external tool or Model Context Protocol server with parameter names that do not exist.
If you only log HTTP 200 OK - 2.4s, you have zero visibility into why your users are seeing degraded outputs. You need observability designed specifically for probabilistic software.
The four pillars of AI observability
To manage AI systems without losing your mind or your budget, you need four distinct layers of visibility and control.
flowchart TD
Pillar1["1. Structured Logging\n(Input tokens, completion tokens, latency, cost, raw payloads)"]
Pillar2["2. Execution Tracing\n(Parent-child spans, tool invocations, multi-turn state drift)"]
Pillar3["3. Automated Evals\n(Schema validation, semantic assertions, regression testing)"]
Pillar4["4. Failure Recovery\n(Exponential backoff, schema fallbacks, hard circuit breakers)"]
Pillar1 --> Pillar2
Pillar2 --> Pillar3
Pillar3 --> Pillar4These four components address separate questions:
- Logs: What exact inputs, outputs, tokens, and latencies occurred during a single invocation?
- Traces: In what order did an agent or workflow invoke tools, and where did the execution graph stall?
- Evals: Did the model output meet strict functional, structural, and quality criteria before reaching the user?
- Recovery: When an inevitable model or network failure occurs, how does the system recover deterministically without manual intervention?
Structured logging: What you must capture on every call
Unstructured text logging (logger.info("Calling OpenAI...")) is useless when debugging non-deterministic AI pipelines. When a user reports that a response was cut in half or contained gibberish, grep-searching freeform text strings will not tell you which model version, prompt template, or token boundary caused the incident.
Every interaction with an LLM must emit a structured JSON payload containing these baseline fields:
| Field | Type | Why It Matters |
|---|---|---|
trace_id | UUID / String | Correlates all steps in a single user session or agent run |
span_id | String | Identifies this specific model or tool invocation |
model | String | Tracks the exact model tag (e.g., gpt-5-6-mini, claude-3-7-sonnet) |
temperature | Float | Eliminates mystery when diagnosing deterministic vs creative drift |
prompt_tokens | Integer | Monitors context bloat and prompt debt |
completion_tokens | Integer | Detects truncated outputs or looping generations |
total_cost_usd | Float | Prevents surprise billing spikes |
latency_ms | Integer | Identifies provider throttling and slow tool calls |
status | String | success, rate_limited, schema_error, or timeout |
raw_response | Object / String | Stores exact payload for post-mortem replay |
At ZeroShot Studio, we emit these events as single-line JSON strings to stdout and a local log file. This allows standard Unix utilities like jq or file shippers to index and aggregate metrics without introducing heavy external software dependencies.
Tracing agent loops and multi-step workflows
A flat log file tells you what happened at step 4, but it cannot show you why step 4 happened.
When building workflows or agent loops, as described in our guide on simple AI workflows before agents, tasks execute as a directed tree of operations. A user submits a query, the system retrieves documentation, calls a model to plan subtasks, executes two tool calls in parallel, feeds the results back into the model, and generates a response.
Tracing captures this execution tree through parent-child spans following the OpenTelemetry model:
sequenceDiagram
autonumber
actor User as User Request
participant Root as Root Workflow Span
participant LLM1 as Planner Call (Span A)
participant Tool as Tool Execution (Span B)
participant LLM2 as Synthesis Call (Span C)
User->>Root: Run research task
Root->>LLM1: Generate plan & tool selection
LLM1-->>Root: Plan ready (tokens: 420, latency: 950ms)
Root->>Tool: Query documentation endpoint
Tool-->>Root: Tool returned 12 records (latency: 180ms)
Root->>LLM2: Synthesize findings into brief
LLM2-->>Root: Completed response (tokens: 880, latency: 1420ms)
Root-->>User: Deliver final outputWhen an agent enters an infinite loop, a trace immediately exposes the culprit:
- Span A spawned Span B.
- Span B failed with a formatting error.
- Span A spawned Span B again with identical input.
- Span B failed again.
Without execution traces, an engineer spends hours guessing which prompt triggered the breakdown. With traces, the faulty tool response is visible in seconds.
Lightweight evals: How to test prompts before shipping
Vibe coders frequently tweak prompt text inside an IDE or configuration file, run one manual test, see that it works, and push the change to production. Three days later, users report that the application started failing on edge cases.
You do not need an enterprise evaluation platform with dozens of simulated judges to catch regressions. You can implement an effective evaluation suite using simple, deterministic assertions written in Python or TypeScript.
A practical eval suite tests three tiers of output verification:
1. Structural Evals (Deterministic)
Use schema validation libraries like Pydantic to guarantee that the model returned every required field with the expected data types:
from pydantic import BaseModel, Fieldclass AnalysisReport(BaseModel): summary: str = Field(..., min_length=20, max_length=300) risk_score: int = Field(..., ge=1, le=10) action_items: list[str] = Field(..., min_items=1)If the model response fails schema validation, the eval fails instantly with zero ambiguity.
2. Guard Rail Evals (Rule-Based)
Check for banned patterns, hallucinated references, or unrendered template tags:
- Does the response contain unrendered placeholders like
[INSERT_NAME_HERE]? - Does the output exceed hard token or character length thresholds?
- Did the model leak system instructions or internal API credentials?
3. Golden Dataset Regression Tests
Maintain a local JSON file containing 20 to 50 realistic input prompts paired with expected invariants. Every time you modify your prompt instructions or upgrade model checkpoints, run the dataset through your eval script.
If your prompt change improves quality on prompt #4 but breaks structured formatting on prompts #12 and #19, your local eval catches the regression before your users do.
Deterministic failure recovery patterns
Observability tells you that something went wrong; failure recovery ensures your application survives the incident.
When an AI pipeline fails, you should never let the exception bubble up to the end user unchecked. In our playbook on debugging AI-generated code without rage, we emphasize isolating failure points. In production, implement these four deterministic recovery mechanisms:
1. Jittered Exponential Backoff
Provider rate limits (HTTP 429) and temporary gateway timeouts (HTTP 502/503) are common. Retrying immediately floods the provider and guarantees repeated rejection. Use exponential backoff with randomized jitter:
delay = min(max_delay, base_delay * (2 ** attempt)) + random.uniform(0, 0.5)2. Two-Pass Schema Repair
If an LLM returns malformed JSON that fails Pydantic validation, do not fail immediately. Make a second, lightweight corrective call that passes the raw invalid string back to the model with the exact validation error:
"The previous output failed validation with this error:
risk_score must be less than or equal to 10. Return only the corrected JSON object matching the schema."
In our production pipelines, a single targeted repair pass resolves more than 85% of JSON parsing issues without crashing the workflow.
3. Graceful Fallback to Deterministic Heuristics
If a model call times out after multiple attempts, fall back to a deterministic rule or cached baseline. For example, if an AI summarizer fails to generate a custom newsletter snippet, fall back to the first two sentences of the source article rather than displaying a blank card or a 500 error.
4. Hard Circuit Breakers
Every agent loop and recursive retry flow must enforce an immutable upper bound:
- Maximum loop turns: 5 iterations.
- Maximum cumulative token consumption: 25,000 tokens.
- Maximum execution time: 30 seconds.
If any threshold is crossed, the circuit breaker trips, halts execution, logs a structured alert, and returns a safe fallback state. A circuit breaker guarantees that a runaway model hallucination never drains your company credit card.
Implementation: A production telemetry harness in 80 lines of Python
Below is a standalone, production-ready telemetry harness. It requires zero third-party APM services and uses standard Python libraries to provide structured logging, hierarchical span tracing, and circuit-breaker enforcement.
import timeimport uuidimport jsonimport loggingfrom typing import Callable, Any, Dictlogging.basicConfig(level=logging.INFO, format="%(message)s")logger = logging.getLogger("zero_telemetry")class TelemetrySpan: def __init__(self, name: str, trace_id: str = None, parent_id: str = None): self.name = name self.trace_id = trace_id or str(uuid.uuid4()) self.span_id = str(uuid.uuid4())[:8] self.parent_id = parent_id self.start_time = 0.0 self.metadata: Dict[str, Any] = {} def __enter__(self): self.start_time = time.perf_counter() return self def __exit__(self, exc_type, exc_val, exc_tb): duration_ms = int((time.perf_counter() - self.start_time) * 1000) status = "error" if exc_type else "ok" payload = { "trace_id": self.trace_id, "span_id": self.span_id, "parent_id": self.parent_id, "span_name": self.name, "duration_ms": duration_ms, "status": status, "metadata": self.metadata } if exc_val: payload["error_message"] = str(exc_val) logger.info(json.dumps(payload)) def set_attribute(self, key: str, value: Any): self.metadata[key] = valueclass CircuitBreaker: def __init__(self, max_turns: int = 5, max_tokens: int = 20000): self.max_turns = max_turns self.max_tokens = max_tokens self.current_turns = 0 self.current_tokens = 0 def record_turn(self, tokens_used: int): self.current_turns += 1 self.current_tokens += tokens_used if self.current_turns > self.max_turns: raise RuntimeError(f"Circuit breaker tripped: turn limit exceeded ({self.current_turns}/{self.max_turns})") if self.current_tokens > self.max_tokens: raise RuntimeError(f"Circuit breaker tripped: token limit exceeded ({self.current_tokens}/{self.max_tokens})")def execute_with_recovery(action_fn: Callable, max_retries: int = 3, fallback_fn: Callable = None): """Executes a function with deterministic retry and optional fallback.""" for attempt in range(1, max_retries + 1): try: return action_fn() except Exception as err: logger.warning(json.dumps({ "event": "retry_attempt", "attempt": attempt, "max_retries": max_retries, "error": str(err) })) if attempt == max_retries: if fallback_fn: logger.info(json.dumps({"event": "triggering_fallback"})) return fallback_fn() raise time.sleep(0.5 * attempt)How to use this in your application
Wrap your workflow entry point in a root span, propagate the trace_id to subtasks, and track token consumption with the circuit breaker:
breaker = CircuitBreaker(max_turns=3, max_tokens=15000)with TelemetrySpan("process_user_document") as root_span: root_span.set_attribute("user_id", "usr_9918") # Step 1: Parse document with TelemetrySpan("parse_text", trace_id=root_span.trace_id, parent_id=root_span.span_id) as parse_span: text = "Extracted input document text..." parse_span.set_attribute("char_count", len(text)) # Step 2: LLM Generation with Circuit Breaker and Telemetry with TelemetrySpan("model_generation", trace_id=root_span.trace_id, parent_id=root_span.span_id) as model_span: # Simulate LLM call breaker.record_turn(tokens_used=1200) model_span.set_attribute("model", "gpt-5-mini") model_span.set_attribute("prompt_tokens", 950) model_span.set_attribute("completion_tokens", 250)Running this yields clean, single-line JSON logs that immediately pinpoint latency, token volume, and failure stages across every run.
FAQ
- Why is traditional APM insufficient for vibe-coded AI applications?
Traditional APM monitors system-level metrics such as CPU load, memory usage, and HTTP status codes. In contrast, AI workflows frequently fail without raising standard server errors. An LLM can return a 200 OK status code while outputting hallucinated data, corrupting database schemas, or consuming 10x its intended token quota. AI observability specifically tracks prompt tokens, model versioning, semantic validation, and tool execution graphs.
- What is the difference between an AI log and an AI trace?
An AI log records a single discrete event in time (such as a model call latency, token count, or tool execution error). An AI trace connects multiple related logs and events into a hierarchical tree, showing parent-child relationships across an entire user request or multi-turn agent loop. Traces reveal where an agent stalled or entered repetitive loops.
- How do you run evals without expensive third-party platforms?
You do not need paid platforms to run effective evals. You can build a local evaluation test runner in Python using Pydantic for schema verification, deterministic regex checks for banned phrases, and a curated JSON dataset of 20 to 50 input-output pairs to test against whenever you update system prompts or model versions.
- What is the most common failure mode in vibe-coded agents, and how do you recover?
The most common failure mode is an unhandled schema mismatch during tool calling, where the model passes invalid arguments to an external function and gets stuck in an infinite retry loop. The best recovery strategy is pairing a hard circuit breaker (maximum 3 to 5 turns) with a two-pass schema repair prompt that hands the exact validation error back to the model for one targeted correction.