Skip to content
Ayhan Sipahi Ayhan Sipahi

Observability Beyond Metrics: The Art of System Storytelling

Move past green-light dashboards to observability that narrates system behavior, user journeys, and business impact via distributed tracing.

Every dashboard is green, every metric sits inside its threshold, and customers still report broken checkouts. Aggregate metrics describe components one at a time, so a failure that lives in the path between components stays invisible to each of them.

Distributed tracing closes that gap. A single trace follows one request through every service it touches and records the order in which things happened. The default worth reaching for is one complete user journey instrumented end to end, with business context attached to every span.

What a Single Trace Shows

Consider a failure mode that recurs in checkout flows. Infrastructure dashboards look healthy: CPU well below capacity, memory nominal, per-service response times in the low hundreds of milliseconds. Conversion, meanwhile, falls off a cliff.

A trace explains it in one view. A recommendation service loses its cache and starts issuing dozens of API calls per checkout request instead of two. Each call is individually fast, so no per-service latency chart moves. The checkout page waits for all of them, and the user leaves before it settles. The failure exists only in the aggregate of the path, and the path is what a trace records.

// What the per-service dashboards report
interface TraditionalMetrics {
  cpu: "40% average";
  memory: "6GB/8GB";
  responseTime: "200ms p50";
  errorRate: "0.1%";
}

// What one trace of the same request reports
interface TraceView {
  userJourney: "checkout_attempt";
  totalDuration: "8.3 seconds";
  spanCount: 247;              // a healthy checkout sits closer to 15
  criticalPath: {
    service: "recommendation-service",
    operation: "get_related_products",
    calls: 47,                 // the cache miss, made visible
    totalTime: "6.8 seconds"
  };
}

Building Narrative-Driven Observability

The telemetry that pays off describes user interactions across the whole system without breaking them into per-service fragments. Three pieces make that work: journey-scoped spans, business attributes on those spans, and alerting that reads the journey.

The OpenTelemetry Journey Mapper

Here is how services are instrumented to capture complete user journeys:

import { trace, context, SpanStatusCode } from '@opentelemetry/api';
import { BusinessContext } from './business-metrics';

class CheckoutService {
  private tracer = trace.getTracer('checkout-service', '1.0.0');
  
  async processCheckout(userId: string, cart: CartData): Promise<CheckoutResult> {
    // Start with business context, then add technical detail
    const startedAt = Date.now();
    const span = this.tracer.startSpan('user.checkout.attempt', {
      attributes: {
        'user.id': userId,
        'user.tier': await this.getUserTier(userId),
        'business.cart_value': cart.totalValue,
        'business.revenue_impact': cart.totalValue,
        'journey.step': 'checkout_initiated',
        'journey.entry_point': cart.referrer
      }
    });
    
    // Propagate context across service boundaries
    return context.with(trace.setSpan(context.active(), span), async () => {
      try {
        // Each step adds to the story
        span.addEvent('inventory.validation.started', {
          items_to_check: cart.items.length
        });
        
        const inventory = await this.validateInventory(cart);
        
        if (!inventory.allAvailable) {
          // This tells us WHY the checkout failed
          span.setAttributes({
            'failure.reason': 'inventory_unavailable',
            'failure.items': inventory.unavailableItems.join(','),
            'business.impact': 'checkout_abandoned'
          });
          span.setStatus({ 
            code: SpanStatusCode.ERROR, 
            message: 'Inventory check failed' 
          });
          return { success: false, reason: 'out_of_stock' };
        }
        
        // Continue building the narrative...
        span.addEvent('payment.processing.initiated');
        const payment = await this.processPayment(cart, userId);
        
        span.setAttributes({
          'journey.completed': true,
          'business.order_value': payment.amount,
          'journey.total_duration_ms': Date.now() - startedAt
        });
        
        return { success: true, orderId: payment.orderId };
        
      } catch (error) {
        // Capture the failure narrative
        span.recordException(error);
        span.setAttributes({
          'failure.stage': this.getCurrentStage(),
          'failure.recovery_attempted': true,
          'business.impact': 'revenue_lost'
        });
        throw error;
      } finally {
        span.end();
      }
    });
  }
}

From Traces to Business Impact

Connecting a trace to business metrics is what makes it answerable during an incident. The analyzer below turns a trace ID into both readings at once:

class BusinessImpactAnalyzer {
  async analyzeTraceImpact(traceId: string): Promise<ImpactReport> {
    const trace = await this.getTrace(traceId);
    const businessContext = this.extractBusinessContext(trace);
    
    return {
      // Technical story
      technicalNarrative: {
        entryPoint: trace.rootSpan.service,
        failurePoint: this.findFailureSpan(trace),
        cascadeEffect: this.analyzeCascade(trace),
        performanceBottleneck: this.findSlowestPath(trace)
      },
      
      // Business story
      businessNarrative: {
        userIntent: businessContext.journeyType, // "purchase", "browse", etc.
        valueAtRisk: businessContext.cartValue || businessContext.subscriptionValue,
        userSegment: businessContext.userTier,
        conversionStage: this.getConversionStage(trace),
        alternativePaths: this.findAlternativeJourneys(businessContext)
      },
      
      // The combined story
      impact: {
        immediateRevenueLoss: this.calculateImmediateLoss(businessContext),
        projectedChurnRisk: this.predictChurnImpact(trace, businessContext),
        brandDamageScore: this.assessBrandImpact(trace),
        recoveryActions: this.generateRecoveryPlan(trace, businessContext)
      }
    };
  }
}

AI-Powered Pattern Recognition

Full-stack instrumentation produces more trace data than anyone reads. A language model is a reasonable first pass over that volume, because the signal it is good at finding is repetition: the same ordering of events before every failure. It is much weaker at being right the first time, and the published research is specific about how much weaker.

The Retrieval Step

Handing a batch of traces to a model with nothing else attached is the weakest configuration anyone has measured. Chen et al. evaluated RCACopilot at EuroSys ‘24 against 653 incidents collected over one year from Microsoft’s Transport service, and reported a micro-F1 of 0.766 for the full pipeline. The same table gives plain GPT-4 prompting, with no similar past incidents retrieved, a micro-F1 of 0.026. Retrieving matched prior incidents is what carries the result. The prompt is the cheap part.

That paper also reports a macro-F1 of 0.533 against the micro-F1 of 0.766, so accuracy falls off sharply on rare root-cause categories. Those are exactly the intermittent failures worth handing to a model, which is why the gap matters.

The retrieval therefore belongs in the code, not in the prompt text:

class TraceHypothesisBuilder {
  async proposeCause(traces: DistributedTrace[]): Promise<Hypothesis> {
    // The load-bearing step: find past incidents that looked like this one
    const fingerprint = this.fingerprintFailures(traces); // service, operation, error class, span ordering
    const precedents = await this.incidentStore.findSimilar(fingerprint, { limit: 10 });

    if (precedents.length === 0) {
      return { status: 'no_precedent', traceIds: fingerprint.traceIds };
    }

    const answer = await this.llm.analyze({
      instruction:
        'Propose one root cause. Cite the span IDs that support it. ' +
        'Report insufficient evidence when the traces do not support a cause.',
      precedents: precedents.map(p => ({
        summary: p.summary,
        cause: p.rootCause,
        fix: p.remediation
      })),
      failing: this.summarizeSpans(traces) // span names, timings, parent links; not raw JSON
    });

    return {
      status: 'hypothesis',
      cause: answer.cause,
      citedSpanIds: answer.citedSpanIds,      // the only field worth acting on
      precedentIds: precedents.map(p => p.id)
    };
  }
}

There is no confidence score in that return value. A decimal printed next to a root cause reads as calibration, and none of the research cited here supports treating a model’s stated confidence as a calibrated probability over telemetry. The field worth keeping is the list of span IDs, because a span ID can be checked.

Accuracy on Real Incidents

Roy et al. at Microsoft tested LLM agents against 107,000 real production incidents, then had two authors manually grade 100 predictions per method. A retrieval baseline and a chain-of-thought agent were each judged correct 39% of the time, and a ReAct agent 35%. Hallucination rates separate them: among incorrect predictions, the retrieval baseline hallucinated in 49% of cases against 6% for ReAct. The paper’s own summary is that ReAct has the highest precision of the three, at the cost of lower overall accuracy.

RCAEval, a neutral benchmark of 735 failure cases across 11 fault types and 3 microservice systems, is candid about the classical side: “Existing methods mostly obtain moderate results”, with the best average Avg@5 scores of 0.46 and 0.54 across 15 baselines. It carries no LLM baselines at all, so there is no neutral ranking of LLM trace analysis against classical root-cause analysis to appeal to.

Vendors publish a different quantity. Datadog’s engineering write-up on its Bits AI SRE agent reports a reduction in time to resolution of up to 95%, describes an internal benchmark of real incidents with known causes scored by an LLM judge, and gives no accuracy figure. Time to resolution and correctness are not the same measurement, and the write-up reports only the first. The research numbers above do not fill that gap. Hand-graded correctness, F1 and Avg@5 answer different questions and do not combine into one accuracy score.

The pattern worth looking for has a specific shape: a fixed delay between an invalidation event and the failures that follow it, visible only because traces preserve ordering across services. Treat what comes back as a hypothesis rather than a diagnosis, and confirm the mechanism by reading the spans the model cited. Practitioners want the same guarantee: in the Grafana Labs Observability Survey 2026, which polled 1,363 respondents across 76 countries, 95% said it matters that AI shows its reasoning.

The line item is small. Honeycomb published the numbers behind its shipped Query Assistant: a monthly OpenAI bill of about 30 USD, a few hundred dollars per month all in, roughly 5 seconds of average latency and a P99 measured in 30 seconds or higher before tuning. It also published the effect it could measure, which landed on learning rather than on answers. Six weeks in, 26.5% of teams that had used the assistant were still writing queries by hand, against 4.5% of teams that had not.

Context-Aware Alert Reduction

Over-instrumentation ends in alert fatigue, and the field reports it as the dominant blocker. In the Grafana Labs Observability Survey 2026, 30% of respondents named alert fatigue the biggest obstacle to faster incident response, the most common answer by a wide margin.

Story-driven alerting gates on business context before it pages anyone:

class StoryDrivenAlerting {
  async evaluateAlert(anomaly: TraceAnomaly): Promise<AlertDecision> {
    // Don't alert on technical metrics alone
    if (!anomaly.hasBusinessContext()) {
      return { shouldAlert: false, reason: "No business impact detected" };
    }
    
    // Build the complete story
    const story = await this.buildNarrative(anomaly);
    
    // Only alert if the story matters
    const impactScore = this.calculateImpactScore({
      affectedUsers: story.userCount,
      revenueAtRisk: story.potentialLoss,
      customerTier: story.primaryUserSegment,
      timeOfDay: story.isBusinessHours,
      similarIncidents: await this.findSimilarStories(story)
    });
    
    if (impactScore < this.alertThreshold) {
      // Log it, but don't wake anyone up
      await this.logForLaterAnalysis(story);
      return { shouldAlert: false, reason: "Below impact threshold" };
    }
    
    // Create an alert that tells the whole story
    return {
      shouldAlert: true,
      channel: this.getChannelForImpact(impactScore),
      message: this.createNarrativeAlert(story),
      suggestedActions: await this.generatePlaybook(story),
      autoRemediation: this.canAutoRemediate(story)
    };
  }
  
  private createNarrativeAlert(story: IncidentStory): string {
    // A novel incident has no precedent, so it arrives with trace IDs and no cited spans
    const evidence = story.citedSpanIds?.length
      ? `Spans that support it: ${story.citedSpanIds.join(', ')}`
      : `No precedent matched. Traces to read: ${story.traceIds?.join(', ') || 'none recorded'}`;

    return `
      Incident Story:
      
      What's happening: ${story.summary}
      Who's affected: ${story.affectedUsers} users (${story.userSegments})
      Business impact: $${story.revenueImpact}/hour potential loss
      
      The journey that's broken:
      ${story.brokenJourney.map(step => `→ ${step}`).join('\n')}
      
      Root cause hypothesis: ${story.rootCause || 'None proposed'}
      ${evidence}
      
      Similar incident: ${story.previousIncident?.summary || 'No similar incidents found'}
      
      Suggested actions:
      ${story.suggestedActions.map((action, i) => `${i+1}. ${action}`).join('\n')}
    `;
  }
}

What Drives the Cost

Observability spend comes down to three levers, and all three are decisions rather than quoted prices. Retention window sets how much trace storage you pay for. Sampling rate sets how much traffic reaches storage at all. Cardinality sets what the query layer costs, and the business attributes that make traces useful (user tier, campaign ID, cart value) are exactly the high-cardinality fields that drive it up.

Which lever moves the invoice depends on the unit you are billed in, and four common vendors each picked a different one. AWS bills per trace: the CloudWatch pricing page lists X-Ray at 0.000005 USD per trace recorded in US East (N. Virginia), which is 5.00 USD per million, after 100,000 free traces a month. Datadog bills per host plus per indexed span: APM lists at 31 USD per host per month on annual pricing and includes 150 GB of ingested spans and 1 million indexed spans per host per month, with 0.10 USD per GB beyond that. Grafana Cloud bills per GB: for customers starting on or after 13 February 2026 the Application Observability docs list 0.025 USD per host hour and 0.50 USD per GB of traces, logs and profiles, with no included telemetry. Honeycomb bills per event: 20 million events a month on the free plan, with Pro starting at 150 USD for 50 million events.

Run one change through those units and the answers diverge. Ten Datadog APM hosts cost 310 USD a month and carry an allowance of 1,500 GB and 10 million indexed spans, so halving span volume from 1,200 GB to 600 GB saves nothing: both figures sit inside the allowance. Push the same fleet to 2,000 GB and the overage is 500 GB at 0.10 USD per GB, so 50 USD. A halving of the same shape on Grafana Cloud, 600 GB down to 300 GB at 0.50 USD per GB, takes that line from 300 USD to 150 USD on the next invoice. On AWS the arithmetic is per trace instead: 10 million traces recorded leaves 9.9 million billable after the free tier, so 49.50 USD. Grafana’s February 2026 change, which removed included telemetry for new customers, is a reminder that the unit itself can move under a product you already run.

Instrumentation also costs something inside the services themselves: extra spans mean extra allocations and extra traffic to the collector. Tail sampling raises that further, because the components holding traces until a decision can be made “must be stateful systems that can accept and store a large amount of data”, and OpenTelemetry’s documentation warns they can grow to “dozens or even hundreds of compute nodes”. Budget the collector fleet as production infrastructure with its own capacity plan, because it fails the way production infrastructure fails.

Practical Implementation Strategies

Three decisions matter more than which vendor you pick:

Start With One User Journey

Instrumenting everything at once produces broad, shallow coverage that answers nothing. Pick the journey carrying the most value, usually checkout or signup, and instrument it completely:

# Start here, not everywhere
priority_instrumentation:
  phase_1:
    - user_registration_flow
    - checkout_process
    - search_to_purchase
  
  phase_2:
    - admin_operations
    - background_jobs
    - third_party_integrations
  
  phase_3:
    - internal_tools
    - reporting_systems
    - everything_else

Sampling Against Your Billing Unit

Sampling is not optional at scale, and aggressive rates lose less than they sound like they should. OpenTelemetry’s sampling documentation states that “for high-volume systems, it is quite common for a sampling rate of 1% or lower to very accurately represent the other 99% of data”. What the reduction saves depends on the billing unit above: under per-trace or per-GB pricing it shows up on the next invoice, under per-host pricing it shows up only once you have crossed the included allowance.

class SmartSampling {
  getSampleRate(span: Span): number {
    // Always sample errors and high-value transactions
    if (span.status === 'ERROR') return 1.0;
    if (span.attributes['user.tier'] === 'premium') return 1.0;
    if (span.attributes['business.value'] > 1000) return 1.0;
    
    // Sample based on business hours
    const hour = new Date().getHours();
    if (hour >= 9 && hour <= 17) return 0.1;  // 10% during business hours
    
    // Minimal sampling during quiet periods
    return 0.01; // 1% overnight
  }
}

The Team Training Investment

Technical tools are only half the battle. The other half is building a team that thinks in narratives:

interface TeamTrainingPlan {
  week1: "Distributed tracing fundamentals",
  week2: "OpenTelemetry instrumentation workshop",
  week3: "Reading and interpreting trace narratives",
  week4: "Correlating traces with business metrics",
  week5: "AI-assisted incident analysis",
  week6: "Building custom dashboards that tell stories",
  
  ongoing: {
    monthlyReviews: "Analyze interesting incidents together",
    documentationDays: "Everyone writes one observability guide",
    rotationProgram: "Everyone does one week of incident command",
    knowledgeSharing: "Weekly 'trace detective' sessions"
  }
}

Common Pitfalls

Dashboards Nobody Opens

Panel sprawl is the default outcome of dashboard-first observability: a board gets added after every incident and almost none get deleted. The ones that survive follow a user from landing page to purchase, with each step annotated by the business metric it moves. A panel that shows a number without showing whose journey the number belongs to gets opened once and then forgotten.

Traces Without Business Context

Collecting more telemetry rarely makes an incident answerable. Linking traces to business events does. Add campaign_id and promo_code as span attributes and a question like “why did conversion drop during the biggest marketing push?” turns from an investigation into a query.

Trusting AI on Dirty Telemetry

AI-assisted analysis inherits the quality of the traces it reads. Inconsistent span names, missing parent links, and attributes that mean different things in different services all produce confident nonsense. Fix the instrumentation conventions first. A naming convention enforced in a shared library is worth more than a better model.

Principles Worth Keeping

Four hold up regardless of vendor:

  1. Start with business outcomes. Instrumenting revenue-generating paths first delivers a faster return than broad infrastructure coverage.

  2. Invest in trace quality over quantity. Better to have perfect traces for critical paths than mediocre traces for everything.

  3. Build team culture before tools. The best observability stack is useless if the team doesn’t know how to read the stories it tells.

  4. Plan growth around span count. Each newly instrumented service multiplies spans on paths that already existed, so trace volume climbs faster than traffic. Set retention and sampling against that curve or pay the re-architecture cost later.

Future Directions

The Grafana Labs Observability Survey 2026 asked 1,363 practitioners in 76 countries what they want from the next generation of tooling, and the answers point in three directions.

Predictive Narratives

92% of those respondents see value in AI surfacing issues before they cause downtime. That number describes appetite. Whether prediction works is a separate question, and the root-cause research above answers it unkindly: even with the incident already in hand, the graded predictions were correct 39% of the time at best.

The prediction that does ship is arithmetic, not inference. Error-budget burn rate says when the budget runs out, and the Google SRE Workbook publishes the table. Against a 99.9% availability SLO measured over 30 days, a burn rate of 1 (a 0.1% error rate) exhausts the budget in 30 days, a burn rate of 2 in 15 days, a burn rate of 10 (a 1% error rate) in 3 days, and a burn rate of 1,000 (total failure) in 43 minutes. The Workbook’s recommended starting configuration pages at 14.4 times burn measured over a 1-hour window with a 5-minute short window, which is 2% of the budget consumed; pages at 6 times over 6 hours with a 30-minute short window, which is 5%; and files a ticket at 1 times over 3 days with a 6-hour short window, which is 10%.

Commercial forecasting works on the same scale. Datadog’s forecast monitors let you set alert lead time “between 12 hours and 3 months”, with presets at 24 hours, 1 week and 1 month, and the seasonal algorithm “requires at least two seasons of history and uses up to six seasons for forecasting”. No shipped product forecasts a specific transaction failure seconds ahead. A predictive narrative you can act on says the disk fills next Tuesday, or the error budget is gone by Thursday afternoon.

Business-First Instrumentation

The next generation of observability starts from business KPIs and derives the technical metrics that support them.

Autonomous Remediation

Narrow versions of this already ship: systems that detect an issue, match it to a known incident signature, and apply the recorded remediation (restart a pod, roll back a deploy, drain a node). Appetite for the wider version is real but not settled. The same Grafana survey found 77% see value in AI acting autonomously, while 15% say they do not trust AI to execute at all. Correctness on hand-graded predictions tops out at 39% in the research above, so the defensible boundary is to let automation act only where the action is cheap to undo.

Your Next Steps

A workable order of operations:

  1. Pick one critical user journey and instrument it completely with OpenTelemetry
  2. Add business context to every span: user tier, revenue impact, conversion stage
  3. Create your first story-driven dashboard that shows a complete user journey
  4. Experiment with AI analysis on top of retrieved past incidents rather than a raw prompt, and grade its answers before trusting them
  5. Make trace reading a team habit with a regular session where someone walks a recent incident trace end to end

References

Story-driven observability works best in systems where incidents have business consequences visible in the data (revenue drops, conversion falls, SLO breaches that correlate with user behaviour). It is less useful when a service is purely internal with no business-facing metrics to attach. Where basic alerting is still immature, fix that first: a trace explains an incident nobody was paged about only after the fact.

Related posts