cd /news/ai-agents/why-console-log-isn-t-enough-when-bu… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-97420] src=dev.to β†— pub= topic=ai-agents verified=true sentiment=Β· neutral

Why console.log Isn't Enough When Building AI Agents

A developer argues that console.log is insufficient for debugging AI agents and proposes structured event tracing with traceId, spanId, and parentSpanId to reconstruct causal relationships. The approach uses JSON-formatted events to build a tree view of agent runs, making failures like cache fallbacks visible even when top-level operations succeed.

read5 min views1 publishedAug 14, 2026

An AI agent fails, so you add a few log statements:

console.log('starting agent');
console.log('tool result', result);
console.log('final answer', answer);

That works until the agent calls tools in parallel, retries one of them, falls back to cached data, and makes several model calls for different purposes. The terminal still contains the events, but it no longer explains the run.

The limitation is not console.log()

itself. The limitation is flat, uncorrelated events. Agent debugging needs identity, parent-child relationships, lifecycle, and safe metadata. Without those, more log lines often create more noise rather than more understanding.

Consider this output:

10:00:01 search started
10:00:01 search started
10:00:02 model started
10:00:02 search completed
10:00:03 search timed out
10:00:03 cache fallback used
10:00:04 model completed

Several important questions remain:

Timestamps describe when events were written. They do not describe causality.

Some request paths are short and sequential, and flat logs are perfectly adequate. Agents become harder to observe because their control flow is often dynamic:

The useful representation is usually a tree:

support_agent
β”œβ”€ classify_question
β”‚  └─ model_call
β”œβ”€ retrieve_context
β”‚  β”œβ”€ vector_search
β”‚  └─ keyword_search
β”œβ”€ check_account
β”‚  β”œβ”€ billing_api       timeout
β”‚  └─ cached_account    fallback
└─ generate_answer
   └─ model_call

The two model calls now have different roles. The fallback belongs to check_account

, and the searches are parallel children of retrieval. The same events are easier to reason about because their relationships are explicit.

The hardest agent failures do not always throw exceptions. A workflow can complete successfully while using the wrong path.

Imagine a quote agent that reports an item as available. Every top-level operation says success

, but the inventory service timed out and the agent used a cache that was a day old. The response is syntactically valid and the HTTP status is 200. The behavior is still wrong for the current user.

A trace can make the path visible:

generate_quote                ok
β”œβ”€ find_product               ok
β”œβ”€ check_inventory            ok
β”‚  β”œβ”€ live_inventory          error: timeout
β”‚  └─ cached_inventory        ok: age_hours=24
└─ compose_quote              ok: inventory_source=cache

Flat logs can capture all of these facts, but only if every line carries enough context to reconstruct the relationships. At that point, you are already building a tracing model.

The first improvement is to give every run and step stable identity.

type AgentEvent = {
  traceId: string;
  spanId: string;
  parentSpanId: string | null;
  event: 'started' | 'completed';
  name: string;
  kind: 'run' | 'model' | 'tool' | 'retrieval' | 'fallback';
  timestamp: string;
  status?: 'ok' | 'error' | 'cancelled';
  durationMs?: number;
  metadata?: Record<string, string | number | boolean | null>;
};

function writeEvent(event: AgentEvent): void {
  console.log(JSON.stringify(event));
}

console.log()

is still the output mechanism. The difference is that the event has a contract. A local script, log processor, test, or trace viewer can group events by traceId

and rebuild the tree from parentSpanId

.

Structured events also make filtering reliable. Searching for a step name in prose logs is fragile; querying kind=tool

and status=error

is not.

Use one start and one completion event for each meaningful span. Completion should include status and duration.

const startedAt = Date.now();

writeEvent({
  traceId,
  spanId,
  parentSpanId,
  event: 'started',
  name: 'search_docs',
  kind: 'retrieval',
  timestamp: new Date(startedAt).toISOString(),
});

try {
  const documents = await searchDocuments(query);

  writeEvent({
    traceId,
    spanId,
    parentSpanId,
    event: 'completed',
    name: 'search_docs',
    kind: 'retrieval',
    timestamp: new Date().toISOString(),
    status: 'ok',
    durationMs: Date.now() - startedAt,
    metadata: { resultCount: documents.length },
  });
} catch (error) {
  writeEvent({
    traceId,
    spanId,
    parentSpanId,
    event: 'completed',
    name: 'search_docs',
    kind: 'retrieval',
    timestamp: new Date().toISOString(),
    status: 'error',
    durationMs: Date.now() - startedAt,
    metadata: {
      errorCategory: error instanceof Error ? error.name : 'UnknownError',
    },
  });

  throw error;
}

This is verbose when written manually, which is why real tracing libraries provide span helpers and async context propagation. The example exposes the information those helpers manage.

Useful metadata explains behavior without copying the payload:

Avoid raw user messages, prompts, model output, tool arguments, tool results, headers, credentials, and retrieved documents by default. They increase risk and often make traces harder to scan.

For example, this metadata is enough to identify a broken context assembly step:

retrieve_docs       result_count=5
build_context       context_tokens=0
generate_answer     input_tokens=412 output_tokens=96

The trace narrows the problem without storing any document text.

Random log wording creates accidental complexity:

tool finished
tool done
completed tool
search returned

Choose controlled names and statuses instead. A small vocabulary such as started

, ok

, error

, cancelled

, and blocked

is easier to aggregate and test. Use a separate error category for timeout, validation, authorization, rate limit, or dependency failure.

Consistency matters more than clever formatting.

Structured logs may be all you need when the agent is small, runs in one process, has a few sequential operations, and does not need a visual timeline or distributed context.

They are a good starting point when:

The important step is to establish the event contract early. Moving structured events to a richer sink later is much easier than parsing years of inconsistent prose logs.

Use a tracing system when the agent has concurrent branches, retries, handoffs, streaming lifecycles, several services, or shared CI rules. Tracing adds capabilities that a terminal stream does not provide naturally:

OpenTelemetry, framework-native integrations, local trace tools, and hosted observability products all use variations of this model. The destination can change; the core mental model remains the same.

This progression avoids a large platform investment before the execution model is understood.

console.log()

remains useful. It is available everywhere and can be a perfectly good sink for structured local events. What it cannot provide by itself is the causal model of an agent run.

Do not respond to a confusing agent by printing more uncorrelated payloads. Add identity, lifecycle, parentage, and safe metadata. Once those relationships are visible, parallel tools, retries, fallbacks, and silent failures become much easier to explain.

The next article will focus on the representation itself: how execution trees differ from flat event streams and how to reconstruct them reliably from span data.

── more in #ai-agents 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/why-console-log-isn-…] indexed:0 read:5min 2026-08-14 Β· β€”