Day 17 Week 3 — Week 3

Structured Logging and Tracing for AI Pipelines

Day 17 — Structured Logging and Tracing for AI Pipelines

When an AI system produces a bad answer, you need to know: which model, which prompt version, what context was retrieved, how long each step took, and what the intermediate outputs were. Without structured logging and tracing, debugging AI systems is guesswork.

1. What to Log in an AI System

interface AICallLog {
  // Identity
  traceId: string;         // correlates all steps in one request
  spanId: string;          // unique to this step
  parentSpanId?: string;   // for nested calls

  // What happened
  model: string;           // 'claude-sonnet-4-20250514'
  promptVersion: string;   // 'classify-v2.1.0'
  temperature: number;
  
  // Input (be careful with PII)
  inputTokens: number;
  systemPromptHash: string;  // don't log full prompts in prod — hash them
  contextSources: string[];  // which RAG docs were included
  
  // Output
  outputTokens: number;
  finishReason: string;      // 'end_turn' | 'max_tokens' | 'stop_sequence'
  
  // Performance
  latencyMs: number;
  ttftMs: number;            // time to first token
  
  // Cost
  estimatedCost: number;
  
  // Quality signals
  validationPassed: boolean;
  fallbackUsed: boolean;
  cacheHit: boolean;
  retryCount: number;
}

What NOT to log: full prompts containing user PII, API keys, raw user content in high-volume systems (use hashes + sampling).

2. Structured Logging Implementation

import { randomUUID } from 'crypto';

class AILogger {
  private baseContext: Record<string, unknown>;

  constructor(context: { service: string; environment: string }) {
    this.baseContext = context;
  }

  logAICall(log: AICallLog): void {
    const entry = {
      ...this.baseContext,
      ...log,
      timestamp: new Date().toISOString(),
      level: log.fallbackUsed ? 'warn' : 'info',
    };
    
    // Structured JSON output — parseable by any log aggregator
    console.log(JSON.stringify(entry));
  }

  // Middleware pattern for automatic logging
  wrapLLMClient(client: LLMClient, promptVersion: string): LLMClient {
    return {
      complete: async (params) => {
        const traceId = getActiveTrace() || randomUUID();
        const spanId = randomUUID();
        const start = Date.now();

        try {
          const result = await client.complete(params);

          this.logAICall({
            traceId,
            spanId,
            model: params.model,
            promptVersion,
            temperature: params.temperature ?? 1,
            inputTokens: result.usage.input_tokens,
            outputTokens: result.usage.output_tokens,
            systemPromptHash: hashString(params.system || ''),
            contextSources: params.metadata?.sources || [],
            finishReason: result.stop_reason,
            latencyMs: Date.now() - start,
            ttftMs: result.metrics?.ttft || 0,
            estimatedCost: calculateCost(result.usage, params.model),
            validationPassed: true, // set by caller after validation
            fallbackUsed: false,
            cacheHit: false,
            retryCount: 0,
          });

          return result;
        } catch (err) {
          this.logAICall({
            traceId,
            spanId,
            model: params.model,
            promptVersion,
            temperature: params.temperature ?? 1,
            inputTokens: 0,
            outputTokens: 0,
            systemPromptHash: hashString(params.system || ''),
            contextSources: [],
            finishReason: 'error',
            latencyMs: Date.now() - start,
            ttftMs: 0,
            estimatedCost: 0,
            validationPassed: false,
            fallbackUsed: false,
            cacheHit: false,
            retryCount: 0,
          });
          throw err;
        }
      }
    };
  }
}

3. Distributed Tracing with OpenTelemetry

For multi-step pipelines, OpenTelemetry traces show the full request flow:

import { trace, SpanStatusCode, context } from '@opentelemetry/api';

const tracer = trace.getTracer('ai-pipeline');

async function tracedPipeline(input: PipelineInput): Promise<PipelineOutput> {
  return tracer.startActiveSpan('ai-pipeline', async (rootSpan) => {
    rootSpan.setAttribute('input.type', input.type);
    rootSpan.setAttribute('pipeline.version', '2.1.0');

    try {
      // Each step gets its own span
      const extracted = await tracer.startActiveSpan('extract', async (span) => {
        span.setAttribute('model', 'claude-haiku');
        span.setAttribute('prompt.version', 'extract-v1.0');
        
        const result = await llm.extract(input.document);
        
        span.setAttribute('tokens.input', result.usage.input_tokens);
        span.setAttribute('tokens.output', result.usage.output_tokens);
        span.setAttribute('cost', calculateCost(result.usage));
        span.end();
        return result;
      });

      const analyzed = await tracer.startActiveSpan('analyze', async (span) => {
        span.setAttribute('model', 'claude-sonnet');
        span.setAttribute('context.chunks', extracted.chunks.length);
        
        const result = await llm.analyze(extracted);
        span.end();
        return result;
      });

      rootSpan.setStatus({ code: SpanStatusCode.OK });
      return analyzed;
    } catch (err) {
      rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
      rootSpan.recordException(err);
      throw err;
    } finally {
      rootSpan.end();
    }
  });
}

This produces a trace tree you can visualize in Jaeger, Grafana Tempo, or any OTEL-compatible backend:

ai-pipeline (10.2s)
├── extract (2.1s) — model: claude-haiku, tokens: 500→200, cost: $0.001
├── analyze (5.8s) — model: claude-sonnet, tokens: 2000→800, cost: $0.018
└── summarize (2.3s) — model: claude-haiku, tokens: 800→150, cost: $0.002

4. Useful Dashboards for AI Systems

// Key metrics to track and alert on
const aiDashboardMetrics = {
  // Latency
  'ai.latency.p50': 'Median LLM call latency',
  'ai.latency.p99': 'Tail latency — UX impact',
  'ai.ttft.p50': 'Time to first token (streaming UX)',

  // Quality
  'ai.validation.pass_rate': 'Output validation success rate',
  'ai.fallback.rate': 'How often fallbacks are used',
  'ai.retry.rate': 'Retry frequency (provider health signal)',

  // Cost
  'ai.cost.per_request': 'Average cost per LLM call',
  'ai.cost.daily_total': 'Total daily spend',
  'ai.tokens.input_per_call': 'Context size trends',

  // Provider health
  'ai.provider.error_rate': 'Per-provider error rates',
  'ai.provider.circuit_breaker_state': 'Which providers are currently open',
};

Try This Today

Add structured logging to one LLM call in your codebase. Log: traceId, model, prompt version, input/output tokens, latency, cost, and whether validation passed. Run it for a few hours and query the logs to answer: what's my average cost per call? What's my P99 latency? How often does validation fail? These three numbers are your AI system's vital signs.

Resources