Skip to main content

Distributed Tracing

The Foil JavaScript SDK provides comprehensive distributed tracing for complex AI applications. This guide covers advanced tracing patterns for multi-step agent workflows.

Trace Architecture

A trace represents a complete request or workflow. Each trace contains multiple spans organized in a tree structure:
Trace: handle-user-request
├── Span: agent (root)
│   ├── Span: llm (gpt-4o) - plan generation
│   ├── Span: tool (web-search)
│   ├── Span: tool (calculator)
│   └── Span: llm (gpt-4o) - final response
Every span has:
  • traceId - Links all spans in the same trace
  • spanId - Unique identifier for this span
  • parentSpanId - Links to parent span (if not root)
  • depth - Nesting level (0 for root)

Creating Traces

const result = await tracer.trace(async (ctx) => {
  // ctx is TraceContext with traceId already set
  console.log('Trace ID:', ctx.traceId);

  // All spans created here share this traceId
  return await processRequest(ctx);
}, {
  name: 'process-request',  // Optional trace name
  traceId: 'custom-id',     // Optional: provide your own trace ID
  convoId: 'conv-123',      // Optional: conversation/session ID
  input: userQuery,         // Optional: trace-level input
  properties: {             // Optional: custom metadata
    userId: 'user-123'
  }
});

Managing Span Hierarchy

Automatic Nesting

Spans created within a trace automatically form a hierarchy based on creation order:
await tracer.trace(async (ctx) => {
  const parentSpan = await ctx.startSpan(SpanKind.AGENT, 'orchestrator');

  // These become children of parentSpan
  const child1 = await ctx.startSpan(SpanKind.LLM, 'gpt-4o');
  await child1.end({ output: '...' });

  const child2 = await ctx.startSpan(SpanKind.TOOL, 'search');
  await child2.end({ output: '...' });

  await parentSpan.end({ output: 'done' });
});

Explicit Parent Control

For complex flows, explicitly control parent-child relationships:
await tracer.trace(async (ctx) => {
  const rootSpan = await ctx.startSpan(SpanKind.AGENT, 'root');

  // Create child context for parallel work
  const childCtx = rootSpan.createChildContext();

  // This span is explicitly a child of rootSpan
  const childSpan = await childCtx.startSpan(SpanKind.LLM, 'gpt-4o');
  await childSpan.end({ output: '...' });

  await rootSpan.end({ output: 'done' });
});

Parallel Span Execution

Track parallel operations correctly:
await tracer.trace(async (ctx) => {
  const planSpan = await ctx.startSpan(SpanKind.LLM, 'planner');
  const tools = ['search', 'calculator', 'weather'];
  await planSpan.end({ output: tools });

  // Execute tools in parallel
  const results = await Promise.all(
    tools.map(async (toolName) => {
      return await ctx.tool(toolName, async () => {
        return await executeTool(toolName);
      });
    })
  );

  // Final synthesis
  const synthesisSpan = await ctx.startSpan(SpanKind.LLM, 'synthesizer');
  const finalResponse = await synthesize(results);
  await synthesisSpan.end({ output: finalResponse });
});

Tool Execution Patterns

Simple Tool Wrapping

const result = await ctx.tool('search', async () => {
  return await searchAPI(query);
}, {
  input: { query },
  properties: { source: 'google' }
});

Tool with Error Handling

const result = await ctx.tool('database-query', async () => {
  try {
    return await db.query(sql);
  } catch (error) {
    // Error automatically captured in span
    throw error;
  }
}, {
  input: { sql }
});

Manual Tool Spans

For more control over tool spans:
const toolSpan = await ctx.startSpan(SpanKind.TOOL, 'complex-tool', {
  input: { arg1, arg2 }
});

try {
  const step1 = await doStep1();
  const step2 = await doStep2(step1);

  await toolSpan.end({
    output: step2,
    properties: {
      step1Result: step1,
      executionPath: 'happy-path'
    }
  });

  return step2;
} catch (error) {
  await toolSpan.end({
    error: error.message,
    status: 'error'
  });
  throw error;
}

Multi-Agent Workflows

Track interactions between multiple agents:
const tracer = createFoilTracer({
  apiKey: process.env.FOIL_API_KEY,
  agentName: 'orchestrator'
});

await tracer.trace(async (ctx) => {
  // Orchestrator decides which agent to call
  const planSpan = await ctx.startSpan(SpanKind.LLM, 'gpt-4o');
  const plan = await decidePlan();
  await planSpan.end({ output: plan });

  // Call specialized agent (could be separate service)
  const agentSpan = await ctx.startSpan(SpanKind.AGENT, 'research-agent', {
    properties: { delegatedTo: 'research-agent' }
  });

  // Research agent does its work (nested spans)
  const researchCtx = agentSpan.createChildContext();
  const searchSpan = await researchCtx.startSpan(SpanKind.TOOL, 'web-search');
  const searchResults = await webSearch(plan.query);
  await searchSpan.end({ output: searchResults });

  const analysisSpan = await researchCtx.startSpan(SpanKind.LLM, 'gpt-4o');
  const analysis = await analyzeResults(searchResults);
  await analysisSpan.end({ output: analysis });

  await agentSpan.end({ output: analysis });

  return analysis;
});

Retrieving Traces

Fetch completed traces for debugging or analysis:
// Get a specific trace with all spans
const trace = await tracer.getTrace(traceId);

console.log('Trace:', trace.traceId);
console.log('Spans:', trace.spans.length);

for (const span of trace.spans) {
  console.log(`${span.depth}: ${span.spanKind} - ${span.name}`);
  console.log(`  Duration: ${span.durationMs}ms`);
  console.log(`  Tokens: ${span.tokens?.total || 'N/A'}`);
}

Trace Context Propagation

Pass trace context to external services:
await tracer.trace(async (ctx) => {
  // Pass context to external service via headers
  const response = await fetch('https://another-service.com/api', {
    headers: {
      'x-trace-id': ctx.traceId,
      'x-parent-span-id': ctx.currentParentEventId
    }
  });

  // Continue with response...
});

Performance Tips

Batch Span Operations

For high-throughput scenarios, minimize API calls:
// Good: Use wrapInSpan for automatic management
await ctx.wrapInSpan(SpanKind.LLM, 'batch-llm', async () => {
  return await processBatch(items);
});

// Avoid: Creating many small spans in tight loops
for (const item of items) {
  // This creates too many API calls
  await ctx.startSpan(SpanKind.CUSTOM, 'process-item');
}

Fire-and-Forget Logging

For non-critical telemetry, use the low-level logging API:
import { Foil } from '@foil-ai/sdk';

const foil = new Foil({ apiKey: process.env.FOIL_API_KEY });

// Fire and forget - doesn't wait for response
foil.log({
  model: 'gpt-4o',
  input: messages,
  output: response,
  latency: duration
});

Next Steps