Skip to main content
← All Articles · · 7 min read

LLM Observability 2026: How to Monitor AI Applications in Production

A comprehensive guide to LLM observability in 2026 — covering Langfuse, Helicone, Arize AI, OpenTelemetry, token cost tracking, latency monitoring, eval pipelines, and hallucination detection with real code examples.

llmobservabilityai-monitoringlangfuseopentelemetryproduction-aimlops

Shipping an LLM-powered application is the easy part. Keeping it reliable, cost-efficient, and accurate in production is where most teams struggle. LLM observability — the practice of monitoring what your models do at runtime — has become a non-negotiable engineering discipline in 2026.

This guide covers the full spectrum: from instrumenting your first LLM call to building automated eval pipelines that catch regressions before they reach users.


Why LLM Observability Is Different

Traditional APM tools were built for deterministic software. If processPayment() returns the wrong value, the cause is in the code. LLMs break that mental model entirely:

  • The same prompt can produce different outputs on different calls
  • Latency varies wildly based on token count, model load, and streaming behavior
  • Cost is runtime-determined — a runaway agent can burn $1,000 in minutes
  • Quality degrades silently — hallucinations don’t throw exceptions
  • Context window management failures cause subtle, hard-to-diagnose bugs

You need observability at four levels: infrastructure, model, application, and quality.


The LLM Observability Stack in 2026

LayerWhat to TrackTools
InfrastructureLatency, errors, rate limitsLangfuse, Helicone, Arize AI
Token economicsInput/output tokens, cost per requestAll platforms + custom billing
TracesFull prompt/response chainsOpenTelemetry, LangSmith
Quality/EvalsAccuracy, groundedness, toneLangfuse evals, Arize Phoenix

Platform Deep Dive

Langfuse: Open-Source, Self-Hostable

Langfuse is the most popular open-source LLM observability platform. It traces full prompt chains, scores outputs, and integrates with every major LLM SDK.

Strengths:

  • Self-hostable (Postgres + Docker) — critical for data compliance
  • First-class support for multi-step agent traces (parents/children)
  • Built-in eval framework for scoring outputs
  • Native SDK for Python and TypeScript

Setup:

# Self-host with Docker Compose
git clone https://github.com/langfuse/langfuse
cd langfuse
docker compose up -d

Instrumenting OpenAI calls (Python):

from langfuse.openai import openai
from langfuse import Langfuse

langfuse = Langfuse()

# Drop-in replacement — all calls are automatically traced
response = openai.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain quantum entanglement in simple terms"}
    ],
    # Optional: associate with a trace for multi-step visibility
    langfuse_observation_id="call-xyz-123",
)

print(response.choices[0].message.content)

Tracing complex agent chains:

from langfuse.decorators import observe, langfuse_context

@observe(name="research-agent")
def research_and_synthesize(query: str) -> str:
    # Step 1: Search
    search_results = web_search(query)

    # Step 2: Summarize each result
    summaries = []
    for result in search_results[:3]:
        summary = summarize_document(result)
        summaries.append(summary)

    # Step 3: Synthesize
    synthesis = synthesize_summaries(summaries, query)

    # Add custom metadata to the trace
    langfuse_context.update_current_observation(
        metadata={
            "num_sources": len(search_results),
            "query_type": "research",
        },
        usage_details={"model": "gpt-4o", "total_tokens": 4500},
    )

    return synthesis


@observe(name="summarize-document")
def summarize_document(document: str) -> str:
    response = openai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "user", "content": f"Summarize this document:\n\n{document}"}
        ],
    )
    return response.choices[0].message.content

Helicone: Simple, Proxy-Based Observability

Helicone works as a proxy in front of your LLM API. Change one URL, get instant observability — no SDK required.

Setup (Python):

from openai import OpenAI

client = OpenAI(
    api_key="your-openai-key",
    base_url="https://oai.helicone.ai/v1",
    default_headers={
        "Helicone-Auth": f"Bearer {HELICONE_API_KEY}",
        # Tag requests for filtering in dashboard
        "Helicone-Property-Environment": "production",
        "Helicone-Property-Feature": "document-qa",
        "Helicone-User-Id": user_id,  # Per-user cost tracking
    }
)

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": prompt}],
)

Setup (TypeScript):

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: 'https://oai.helicone.ai/v1',
  defaultHeaders: {
    'Helicone-Auth': `Bearer ${process.env.HELICONE_API_KEY}`,
    'Helicone-Property-App': 'my-saas-app',
    'Helicone-Cache-Enabled': 'true',    // Enable semantic caching
    'Helicone-Cache-Bucket-Max-Size': '10', // Cache up to 10 variations
  },
});

Helicone’s semantic caching is a killer feature — it caches similar (not just identical) prompts, cutting costs dramatically for common queries.


Arize AI: Enterprise-Grade ML Observability

Arize AI is the enterprise choice, with deep support for:

  • Phoenix (open-source eval/tracing tool, works standalone)
  • Embedding drift detection
  • Production vs. golden-set comparison
  • Automated alerting on quality degradation

Phoenix quick start (local tracing):

import phoenix as px
from phoenix.otel import register
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# Start local Phoenix server
session = px.launch_app()

# Register OTEL tracer pointing to Phoenix
tracer_provider = register(project_name="my-llm-app")

# Auto-instrument OpenAI
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)

# Now all OpenAI calls are automatically traced
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "What is RLHF?"}],
)

print(f"Phoenix UI: {session.url}")

OpenTelemetry for LLMs

OpenTelemetry (OTel) is becoming the standard instrumentation layer for LLM observability. The OpenLLMetry spec defines semantic conventions for AI/LLM spans.

Why OTel matters: instrument once, export to any backend (Langfuse, Arize, Jaeger, Datadog, etc.)

Full Python example:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# Configure OTel with OTLP exporter (send to Langfuse, Arize, etc.)
provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(
            endpoint="https://cloud.langfuse.com/api/public/otel/v1/traces",
            headers={
                "Authorization": f"Basic {LANGFUSE_ENCODED_KEY}",
            },
        )
    )
)
trace.set_tracer_provider(provider)

# Instrument OpenAI
OpenAIInstrumentor().instrument()

# Custom span with LLM-specific attributes
tracer = trace.get_tracer("my-app")

with tracer.start_as_current_span("rag-pipeline") as span:
    span.set_attribute("llm.system", "openai")
    span.set_attribute("llm.request.model", "gpt-4o")
    span.set_attribute("rag.num_documents", 5)
    span.set_attribute("rag.retrieval_strategy", "hybrid")

    result = run_rag_pipeline(query)

    span.set_attribute("llm.response.token_count.total", result.total_tokens)
    span.set_attribute("rag.answer_relevance_score", result.relevance_score)

TypeScript with OTel:

import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OpenAIInstrumentation } from '@arizeai/openinference-instrumentation-openai';
import { registerInstrumentations } from '@opentelemetry/instrumentation';

const provider = new NodeTracerProvider();
provider.addSpanProcessor(
  new BatchSpanProcessor(
    new OTLPTraceExporter({
      url: 'https://otlp.eu.langfuse.com/v1/traces',
      headers: {
        Authorization: `Basic ${process.env.LANGFUSE_BASE64_KEY}`,
      },
    })
  )
);
provider.register();

registerInstrumentations({
  instrumentations: [new OpenAIInstrumentation()],
});

Token Cost Tracking

Token costs are your LLM application’s variable COGS. Track them at three granularities:

Per-Request Cost Calculation

import anthropic

COST_PER_1K = {
    "claude-3-5-sonnet-20241022": {"input": 0.003, "output": 0.015},
    "claude-3-haiku-20240307": {"input": 0.00025, "output": 0.00125},
    "gpt-4o": {"input": 0.005, "output": 0.015},
    "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
}

def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
    rates = COST_PER_1K.get(model, {"input": 0, "output": 0})
    return (
        (input_tokens / 1000) * rates["input"] +
        (output_tokens / 1000) * rates["output"]
    )


client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain RAG architecture"}],
)

cost = calculate_cost(
    "claude-3-5-sonnet-20241022",
    message.usage.input_tokens,
    message.usage.output_tokens,
)

print(f"Input tokens: {message.usage.input_tokens}")
print(f"Output tokens: {message.usage.output_tokens}")
print(f"Request cost: ${cost:.6f}")

Aggregate Cost Dashboard (TypeScript)

interface LLMUsageRecord {
  timestamp: Date;
  model: string;
  feature: string;
  userId: string;
  inputTokens: number;
  outputTokens: number;
  costUsd: number;
  latencyMs: number;
}

class LLMCostTracker {
  private records: LLMUsageRecord[] = [];

  track(record: LLMUsageRecord): void {
    this.records.push(record);
    // In production: write to your data warehouse (BigQuery, Snowflake, etc.)
  }

  summarize(windowMs: number = 86_400_000): CostSummary {
    const cutoff = new Date(Date.now() - windowMs);
    const recent = this.records.filter(r => r.timestamp > cutoff);

    return {
      totalRequests: recent.length,
      totalCostUsd: recent.reduce((sum, r) => sum + r.costUsd, 0),
      totalInputTokens: recent.reduce((sum, r) => sum + r.inputTokens, 0),
      totalOutputTokens: recent.reduce((sum, r) => sum + r.outputTokens, 0),
      avgLatencyMs: recent.reduce((sum, r) => sum + r.latencyMs, 0) / recent.length,
      costByFeature: this.groupBy(recent, 'feature', 'costUsd'),
      costByModel: this.groupBy(recent, 'model', 'costUsd'),
    };
  }
}

Latency Monitoring and Optimization

LLM latency has two distinct components:

  1. Time to first token (TTFT) — critical for streaming UX
  2. Total generation time — critical for batch/async workflows

Measuring TTFT in Python

import time
from openai import OpenAI

client = OpenAI()

def stream_with_ttft_measurement(prompt: str):
    start_time = time.perf_counter()
    first_token_time = None

    stream = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
    )

    full_response = ""
    for chunk in stream:
        if chunk.choices[0].delta.content:
            if first_token_time is None:
                first_token_time = time.perf_counter()
                ttft_ms = (first_token_time - start_time) * 1000
                print(f"⚡ TTFT: {ttft_ms:.0f}ms")

            full_response += chunk.choices[0].delta.content

    total_time_ms = (time.perf_counter() - start_time) * 1000
    print(f"⏱ Total time: {total_time_ms:.0f}ms")
    return full_response

Latency budget targets (2026 baseline):

WorkloadTarget TTFTTarget Total
Interactive chat<500ms<5s
Auto-complete<200ms<2s
Document analysis<1s<30s
Batch processingN/A<5min

Eval Pipelines: Catching Regressions Before Users Do

Evals are automated tests for LLM quality. Unlike unit tests, they use model-based or heuristic scoring.

Types of Evals

1. Reference-free evals (no ground truth needed):

  • Toxicity detection
  • Hallucination likelihood (self-consistency checks)
  • Response format adherence

2. Reference-based evals (requires golden dataset):

  • Exact match (for structured outputs)
  • Semantic similarity (embeddings comparison)
  • LLM-as-judge (GPT-4o evaluates another model’s output)

LLM-as-Judge Eval (Python)

from openai import OpenAI

client = OpenAI()

EVAL_PROMPT = """You are evaluating an AI assistant's response.

Question: {question}
Response: {response}

Rate the response on:
1. Accuracy (1-5): Is the information factually correct?
2. Completeness (1-5): Does it fully answer the question?
3. Conciseness (1-5): Is it appropriately brief without losing meaning?

Respond with JSON only: {{"accuracy": N, "completeness": N, "conciseness": N, "reasoning": "..."}}"""

def evaluate_response(question: str, response: str) -> dict:
    eval_response = client.chat.completions.create(
        model="gpt-4o",  # Use a strong model as judge
        messages=[{
            "role": "user",
            "content": EVAL_PROMPT.format(question=question, response=response)
        }],
        response_format={"type": "json_object"},
        temperature=0,  # Deterministic for evals
    )

    scores = json.loads(eval_response.choices[0].message.content)
    scores["overall"] = sum([
        scores["accuracy"],
        scores["completeness"],
        scores["conciseness"]
    ]) / 3
    return scores


# Run against a golden dataset
def run_regression_eval(test_cases: list[dict]) -> EvalReport:
    results = []
    for case in test_cases:
        response = generate_response(case["question"])
        scores = evaluate_response(case["question"], response)
        results.append({**case, "scores": scores, "response": response})

    avg_overall = sum(r["scores"]["overall"] for r in results) / len(results)
    return EvalReport(results=results, avg_score=avg_overall, passed=avg_overall >= 3.5)

Hallucination Detection via Self-Consistency

import re
from collections import Counter

def detect_hallucination(prompt: str, n_samples: int = 5) -> float:
    """
    Generate N responses to the same prompt.
    Low consistency across responses = higher hallucination risk.
    """
    responses = []
    for _ in range(n_samples):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,  # Some variation to detect inconsistency
        )
        responses.append(response.choices[0].message.content)

    # Extract key facts (simplified — use NER in production)
    all_numbers = [re.findall(r'\b\d+\b', r) for r in responses]
    flat_numbers = [n for numbers in all_numbers for n in numbers]

    if not flat_numbers:
        return 0.0  # No numbers to check

    # High entropy in numbers = low consistency = high hallucination risk
    counter = Counter(flat_numbers)
    most_common_count = counter.most_common(1)[0][1]
    consistency_score = most_common_count / len(flat_numbers)

    return 1.0 - consistency_score  # Higher = more hallucination risk

Platform Comparison

FeatureLangfuseHeliconeArize AI / Phoenix
Open sourceYes (self-hostable)NoPhoenix: Yes
Proxy-based setupNoYesNo
Multi-step tracesExcellentBasicExcellent
Evals frameworkBuilt-inLimitedBuilt-in
Semantic cachingNoYesNo
Embedding driftNoNoYes
PricingFree + cloudFree tier + usageFree (Phoenix) + Enterprise
Best forSelf-hosted teamsQuick setupEnterprise ML teams

Production Alerting Setup

Connect your observability data to alerts:

# Example: alert when per-hour cost exceeds threshold
import boto3
import json

def check_hourly_budget(tracker: LLMCostTracker, threshold_usd: float = 50.0):
    summary = tracker.summarize(windowMs=3_600_000)  # 1 hour

    if summary.totalCostUsd > threshold_usd:
        # Send SNS alert (AWS) or Slack/PagerDuty webhook
        sns = boto3.client('sns')
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:123456789:llm-cost-alerts',
            Message=json.dumps({
                "alert": "LLM cost threshold exceeded",
                "hourly_cost_usd": summary.totalCostUsd,
                "threshold_usd": threshold_usd,
                "top_features": summary.costByFeature,
            }),
            Subject="⚠️ LLM Cost Alert",
        )

Context Window and Memory Observability

Context window management is one of the most common production failure modes — yet most teams don’t track it at all.

What to Monitor

  • Context utilization: What % of the model’s context window are you using per request?
  • Truncation events: Are you silently dropping conversation history or document chunks?
  • Token distribution: Input vs. output token ratio reveals prompt efficiency problems
import tiktoken

class ContextWindowMonitor:
    MODEL_LIMITS = {
        "gpt-4o": 128_000,
        "gpt-4o-mini": 128_000,
        "claude-3-5-sonnet-20241022": 200_000,
        "claude-3-haiku-20240307": 200_000,
    }

    def __init__(self, model: str):
        self.model = model
        self.limit = self.MODEL_LIMITS.get(model, 8_000)
        try:
            self.encoder = tiktoken.encoding_for_model(model)
        except KeyError:
            self.encoder = tiktoken.get_encoding("cl100k_base")

    def count_tokens(self, messages: list[dict]) -> int:
        total = 0
        for message in messages:
            total += 4  # overhead per message
            total += len(self.encoder.encode(message.get("content", "")))
        return total

    def check_utilization(self, messages: list[dict]) -> dict:
        token_count = self.count_tokens(messages)
        utilization = token_count / self.limit
        return {
            "token_count": token_count,
            "limit": self.limit,
            "utilization_pct": round(utilization * 100, 1),
            "status": "critical" if utilization > 0.9 else "warning" if utilization > 0.75 else "ok",
        }

Distributed Tracing for Multi-Agent Systems

As agentic architectures become common, observability must span multiple LLM calls, tool uses, and sub-agents.

from opentelemetry import trace
from opentelemetry.trace import SpanKind

tracer = trace.get_tracer("agent-framework")

class ObservableAgent:
    def __init__(self, name: str, model: str):
        self.name = name
        self.model = model

    def run(self, task: str, parent_context=None) -> str:
        with tracer.start_as_current_span(
            f"agent.{self.name}",
            kind=SpanKind.CLIENT,
            context=parent_context,
        ) as span:
            span.set_attribute("agent.name", self.name)
            span.set_attribute("agent.model", self.model)
            span.set_attribute("agent.task", task[:200])

            try:
                result = self._execute(task)
                span.set_attribute("agent.status", "success")
                return result
            except Exception as e:
                span.set_attribute("agent.status", "error")
                span.record_exception(e)
                raise

    def _execute(self, task: str) -> str:
        with tracer.start_as_current_span("tool.web_search") as tool_span:
            tool_span.set_attribute("tool.name", "web_search")
            search_results = web_search(task)
            tool_span.set_attribute("tool.result_count", len(search_results))

        with tracer.start_as_current_span("llm.completion") as llm_span:
            llm_span.set_attribute("llm.model", self.model)
            return self._call_llm(task, search_results)

The parent-child span structure means you can see the full agent run in one waterfall view — each tool call, sub-agent delegation, and LLM completion as a nested span.


Building an Observability Dashboard

For production systems, consolidate your LLM metrics into a unified view:

Key metrics to surface on your dashboard:

MetricAlert ThresholdBusiness Impact
P95 latency>3s for interactive chatUser drop-off
Error rate>1%Broken user workflows
Token cost/hour>$50Budget overrun
Eval score trend<0.1 week-over-week dropQuality regression
Context utilization>85% averageTruncation risk
Cache hit rate<50% for repeated queriesCost inefficiency

Stack recommendation for a small team:

  • Langfuse for tracing and evals (self-hosted, free)
  • Grafana for dashboards (pull metrics from Langfuse or your DB)
  • PagerDuty / Slack webhook for cost and error alerts
  • GitHub Actions for nightly eval regression runs

Summary

LLM observability in 2026 is a multi-layer discipline:

  • Langfuse for open-source, self-hostable tracing with first-class eval support
  • Helicone for zero-code proxy instrumentation and semantic caching
  • Arize AI/Phoenix for enterprise ML teams with embedding drift and regression detection
  • OpenTelemetry as the vendor-neutral instrumentation layer
  • Automated evals as your regression test suite for model quality
  • Token cost tracking as your LLM COGS monitoring

The teams winning with LLMs in production aren’t just the ones with the best models — they’re the ones who know exactly what their models are doing, what they cost, and when quality drops.


Next Steps

Explore these DevPlaybook tools for AI development:

DevPlaybook Pro members get access to production-ready LLM monitoring templates, eval datasets, and architecture guides for RAG applications. Upgrade to Pro →

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.