What's New in OpenTelemetry Collector v0.157.0 (released 2026-07-21 as the monthly Collector drop, with the receiver/exporter/processor pieces following on the v0.157.0 release branches): two breaking changes plus a major configuration-schema + partial-reload milestone. The two breaking changes both touch the same internal observability surface — the byte-scale histogram bucket boundaries for otelcol_exporter_queue_batch_send_size_bytes and otelcol_processor_batch_batch_send_size_bytes have been replaced with a power-of-2 set spanning 128 B to 16 MiB (#15535, applied in both pkg/exporterhelper and processor/batch). Until v0.157.0, the old boundaries included many small sub-kilobyte buckets that were not useful for byte-scale payloads, and the exporter-side histogram topped out at 6000 bytes so nearly all real observations fell into the +Inf overflow bucket. The new boundaries are powers of two from 128 B to 16777216 (16 MiB), giving a meaningful distribution for real batch payload sizes (including small timeout-flushed batches) and keeping the two metrics directly comparable on the same dashboards. Dashboards or alerts that hard-code specific le values for these histograms will need to be updated. The big milestone is Phase 1 of the component configuration schema roadmap RFC (#14543) — every core component now ships a config.schema.yaml (debug/otlp/otlphttp exporters, otlp receiver, batch/memory_limiter processors, memory_limiter/zpages extensions) generated by the new schemagen tool and checked into the repo. That gives editors real autocompletion + validation on Collector configs without per-component bespoke schemas — the same shape of authoring-time win the JSON-schema-on-REST work in the Weaviate ecosystem produced for vector-database search responses. The other two high-impact changes are experimental resource detection for the Collector's own internal telemetry (#14311, enabled via service::telemetry::resource::detection/development::detectors) and the partial receiver-only reload feature gatesservice.partialReload (Alpha) + service.partialReloadReceivers (Beta) — that together restart only receivers on config reload when non-receiver config sections are unchanged, avoiding unnecessary disruption to processors, exporters, and extensions (#5966). The bug fixes cluster around the gRPC client WaitForReady not being applied (#15615), the empty --feature-gates identifier panic (#15536), the cgroup-v2 slice-valued resource-attribute panic on startup (#15571), and the debug-exporter scope-index labelling bug that printed the parent resource's index instead of its own (#15541). For teams instrumenting LLM pipelines, v0.157.0 is the Collector release where the byte-scale histograms stop lying, the config schema ships with the binary, and reloads stop disrupting the processor + exporter chain.

OpenTelemetry CNCF graduation: what changed in July 2026

OpenTelemetry reached CNCF graduated status in May 2026, and the July 24 CNCF post from Adriana Villela of Dynatrace and Reese Lee of New Relic explains the operational result: OTel is production-ready, independently governed, and backed by more than 12,000 contributions from more than 2,800 companies. The project now gives platform teams a credible instrumentation boundary across traces, metrics, logs, and profiling.

Graduation does not freeze every gen_ai.* attribute. Individual semantic conventions still carry their own stability levels. Keep the convention version in deployment metadata, isolate experimental fields behind one helper, and test the emitted contract before Collector or SDK upgrades.

  • Make OTLP the application boundary. Put backend-specific behavior in Collector exporters rather than vendor SDKs spread through inference services.
  • Prove portability with a canary exporter. Send a small slice to a second backend and compare accepted spans, dropped attributes, query latency, and billable ingestion.
  • Treat profiling as the fourth signal. Correlate CPU and allocation profiles with slow model-serving traces instead of operating a separate profiling island.
  • Keep MCP telemetry adaptable. Record server, method, tool, duration, result, and retries, but hide experimental MCP attribute names behind an adapter.

A future OTel graduation spoke for AI/ML teams will include a minimal Collector profile, a backend migration drill, and a telemetry contract test; this in-place section is the live summary until the indexing gate clears. The main shift is commercial as much as technical: a verified OTel exit path lets a team test another backend without reinstrumenting every service.


Why Traditional Tracing Falls Short for LLMs

Standard distributed traces capture request-response pairs across service boundaries. LLM inference is different:

  • Long, streaming responses — a single prompt generates hundreds of spans as tokens arrive sequentially
  • Non-deterministic output — the same prompt can produce wildly different traces depending on sampling parameters
  • Nested tool calls — an LLM agent can trigger cascading downstream API calls based on generated content
  • Context propagation — prompt history, retrieved documents, and system instructions all affect output but aren't naturally captured in standard spans

OpenTelemetry's semantic conventions were extended in 2024-2025 to cover LLM-specific operations. Understanding these conventions is the first step to building meaningful observability for AI systems.


Setting Up the OTel SDK for LLM Instrumentation

The OpenTelemetry Python SDK provides first-class support for LLM instrumentation through the opentelemetry-instrumentation-openai and opentelemetry-instrumentation-litellm packages.

# Install dependencies
# pip install opentelemetry-api \
#   opentelemetry-sdk \
#   opentelemetry-exporter-otlp \
#   opentelemetry-instrumentation-openai \
#   opentelemetry-instrumentation-litellm

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME

# Initialize the tracer provider with service identity
resource = Resource.create({
    SERVICE_NAME: "llm-inference-service",
    "service.version": "1.0.0",
    "deployment.environment": "production",
})

provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

The resource attributes (service.name, deployment.environment) appear in every span and enable filtering in your observability backend.


Tracing OpenAI-Compatible API Calls

If your inference runs through an OpenAI-compatible endpoint (OpenAI, Azure OpenAI, local vLLM, Ollama, or any LiteLLM proxy), the openai instrumentation package captures spans automatically:

from openai import OpenAI
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# One-line instrumentation — wraps all OpenAI API calls
OpenAIInstrumentor().instrument()

client = OpenAI(api_key="sk-...")

with tracer.start_as_current_span("llm.summary-task") as span:
    span.set_attribute("llm.prompt.template", "Summarize: {text}")
    span.set_attribute("llm.prompt.variables.text", user_input[:200])

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are a technical summarizer."},
            {"role": "user", "content": user_input}
        ],
        temperature=0.3,
        max_tokens=500,
    )

    # Extract response attributes for the span
    span.set_attribute("llm.response.model", response.model)
    span.set_attribute("llm.response.usage.prompt_tokens", response.usage.prompt_tokens)
    span.set_attribute("llm.response.usage.completion_tokens", response.usage.completion_tokens)
    span.set_attribute("llm.response.usage.total_tokens", response.usage.total_tokens)
    span.set_attribute("llm.response.finish_reason", response.choices[0].finish_reason)

    summary = response.choices[0].message.content

The openai instrumentation automatically creates child spans for each API call, capturing token counts, model selection, and latency. No manual span management required for the hot path.


Instrumenting Streaming Responses with Custom Spans

Streaming responses are the hardest case. The OpenAI SDK streams CompletionChunk events; each event contains a partial delta. Naively instrumenting each chunk creates an unmanageable span explosion. Instead, use a single parent span with chunk count attributes:

async def stream_completion(client, prompt: str, model: str = "gpt-4o"):
    with tracer.start_as_current_span("llm.stream-completion") as span:
        span.set_attribute("llm.prompt.length", len(prompt))
        span.set_attribute("llm.model", model)

        stream = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            stream=True,
            stream_options={"include_usage": True}
        )

        full_response = ""
        chunk_count = 0

        # Consume the stream
        for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                content = chunk.choices[0].delta.content
                full_response += content
                chunk_count += 1

        span.set_attribute("llm.response.chunk_count", chunk_count)
        span.set_attribute("llm.response.full_length", len(full_response))
        span.set_attribute("llm.response.first_token_latency_ms", first_token_ms)

        return full_response

The stream_options={"include_usage": True} parameter (available in OpenAI SDK v1.35+) provides final token counts at the end of the stream, avoiding the need to accumulate counts during streaming.


Capturing LLM-as-Tool Calls with Semantic Conventions

Agentic AI systems use LLMs to decide when and how to call external tools. The OpenTelemetry semantic conventions for LLM operations (established in OTEP 0234) define standard attribute names for these interactions:

import json

def trace_tool_call(span, tool_name: str, arguments: dict, result: str, latency_ms: float):
    """
    Record an LLM-initiated tool call using OTel semantic conventions.
    https://opentelemetry.io/docs/specs/semconv/gen-ai/llm-semantic-conventions/
    """
    span.set_attribute("gen_ai.system", "openai")
    span.set_attribute("gen_ai.operation", "chat")
    span.set_attribute("gen_ai.tool.name", tool_name)
    span.set_attribute("gen_ai.tool.call.arguments", json.dumps(arguments))
    span.set_attribute("gen_ai.tool.call.result", result[:500])  # Truncate for storage
    span.set_attribute("gen_ai.tool.call.duration_ms", latency_ms)

The gen_ai.* prefix is the standard namespace for generative AI attributes in OpenTelemetry semantic conventions. These attributes enable you to filter and aggregate by tool name in Grafana, Honeycomb, or any OTel-compatible backend.


Context Propagation Across LLM Pipeline Stages

A production LLM pipeline typically involves multiple stages: prompt templating, retrieval augmentation, inference, and response post-processing. Each stage should propagate the same trace context to enable end-to-end latency analysis:

from opentelemetry.context import attach, detach
from opentelemetry.trace import SpanKind

def process_with_context(user_input: str, retrieved_docs: list[dict]):
    # Extract the current trace context from the incoming request
    # (assumes W3C trace context headers are present in the HTTP request)
    context = extract_context_from_headers(request.headers)

    token = attach(context)

    try:
        with tracer.start_as_current_span(
            "llm.pipeline.full",
            kind=SpanKind.INTERNAL,
        ) as span:
            # Stage 1: Retrieval
            with tracer.start_as_current_span("llm.pipeline.retrieval") as retrieval_span:
                retrieval_span.set_attribute("retrieval.doc_count", len(retrieved_docs))
                retrieval_span.set_attribute("retrieval.avg_doc_length", sum(len(d['content']) for d in retrieved_docs) // max(len(retrieved_docs), 1))

            # Stage 2: Prompt assembly
            with tracer.start_as_current_span("llm.pipeline.prompt-assembly") as prompt_span:
                prompt = assemble_rag_prompt(user_input, retrieved_docs)
                prompt_span.set_attribute("prompt.final_length", len(prompt))

            # Stage 3: Inference (child of pipeline span, sibling of retrieval/prompt)
            with tracer.start_as_current_span("llm.pipeline.inference") as inference_span:
                response_text = call_llm(prompt)
                inference_span.set_attribute("llm.response.length", len(response_text))

    finally:
        detach(token)

The W3C traceparent header propagates context across HTTP boundaries. If your pipeline involves async message queues (SQS, Pub/Sub), embed the serialized context in the message payload:

from opentelemetry.propagate import inject, extract
from opentelemetry.context import Context

def publish_to_queue(queue_url: str, payload: dict, parent_context: Context):
    """Inject trace context into a queue message for cross-service propagation."""
    headers = {}
    inject(headers, context=parent_context)
    # headers now contains traceparent, tracestate for W3C propagation
    payload["_trace_ctx"] = headers
    sqs.send_message(QueueUrl=queue_url, MessageBody=json.dumps(payload))

How do you configure the OTel Collector for AI traces?

The OpenTelemetry Collector processes and exports your AI inference traces. A minimal production config for LLM traces:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1000

  # Filter out health-check spans
  filter:
    spans:
      exclude:
        match_type: strict
        attributes:
          - key: http.target
            value: /healthz

  # Redact PII from prompt content in traces
  transform:
    error_mode: ignore
    trace_statements:
      - context: span
        statements:
          - replace_pattern(attributes["llm.prompt.content"], "user_email=\".*\"", "user_email=\"[REDACTED]\"")
          - replace_pattern(attributes["gen_ai.tool.call.arguments"], "\"api_key\":\".*\"", "\"api_key\":\"[REDACTED]\"")

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  prometheus:
    endpoint: "0.0.0.0:8889"
    namespace: llm_inference
    const_labels:
      service: llm-inference-service

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, filter, transform]
      exporters: [otlp/tempo]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]

The transform processor is critical for compliance — prompts often contain user PII that shouldn't appear in your trace storage backend. If you are using the Collector's byte-scale histograms (the queue and batch send-size metrics the otelcol_exporter_queue_batch_send_size_bytes and otelcol_processor_batch_batch_send_size_bytes histograms surface), note that v0.157.0 replaced the old boundaries with a power-of-2 set spanning 128 B to 16 MiB, so any dashboard or alert that hard-codes the old le values will need to be updated alongside this config. For the broader observability stack around the Collector — pulling in the right sampling, tail-based decisions, and SLO surfaces — the observability 2026 guide walks through the trace + metric + log mix that complements the Collector, and the Prometheus vs Grafana 2026 comparison covers the metric-pipeline side. If you are running the Collector on Kubernetes and want to control the cost of the metric pipeline at scale, the resource-quota and cgroup-v2 GOMAXPROCS interactions that v0.157.0's resource-detector fixes interact with are the same family of behavior the Kubernetes GPU operators guide covers for compute-bound workloads.


Key Metrics to Capture from LLM Pipelines

Beyond traces, your LLM pipeline should emit these metrics for capacity planning and cost monitoring:

Metric Type Description
llm.request.duration Histogram End-to-end latency from prompt receipt to final token
llm.tokens.prompt Counter Total prompt tokens processed
llm.tokens.completion Counter Total completion tokens generated
llm.tokens.cost Counter Estimated API cost in USD
llm.stream.first_token_latency Histogram Time to first token (TTFT)
llm.error.rate Gauge Rate of API errors / rate limit hits
llm.tool.call.duration Histogram Per-tool call latency

Emit these with the OpenTelemetry Metrics API:

from opentelemetry import metrics

meter = metrics.get_meter(__name__)
token_counter = meter.create_counter(
    name="llm.tokens.total",
    description="Total tokens processed",
    unit="1",
)

# Record token usage after each response
token_counter.add(
    response.usage.prompt_tokens,
    {"token.type": "prompt", "model": response.model}
)
token_counter.add(
    response.usage.completion_tokens,
    {"token.type": "completion", "model": response.model}
)

Grafana Dashboard for LLM Pipeline Observability

With traces flowing into Grafana Tempo and metrics into Prometheus/Grafana Cloud, here's the minimum viable dashboard layout for LLM pipeline observability:

Panel 1: Request Volume + Error Rate

sum(rate(llm_inference_requests_total[5m])) by (model)
| vs |
sum(rate(llm_inference_errors_total[5m])) by (model)

Alert threshold: error rate > 1% triggers PagerDuty.

Panel 2: Token Cost by Model

sum(increase(llm_tokens_total[24h])) by (model, token.type) * token_price_per_1k

This panel is essential for FinOps — you should know within 5% accuracy what each model costs per day.

Panel 3: P50/P95/P99 Latency Heatmap

histogram_quantile(0.95, sum(rate(llm_request_duration_bucket[5m])) by (le, model))

P99 > 30s for streaming models is a signal to investigate cold start issues or rate limiting.

Panel 4: Tool Call Frequency

sum(rate(gen_ai_tool_calls_total[5m])) by (tool.name)

Spikes in tool call frequency indicate your agent is entering loops or encountering ambiguous prompts.

Panel 5: First Token Latency Distribution

histogram_quantile(0.50, sum(rate(llm_first_token_latency_bucket[5m])) by (le, model))

TTFT > 5s on a streaming endpoint will feel unresponsive to users. Establish baselines per model version.

Recommended Tool Grafana Cloud

Grafana Cloud is the easiest way to get started with LLM observability — native OTel support, pre-built dashboards for LLM traces, and pay-as-you-go pricing for inference-heavy workloads.


Common Pitfalls in LLM OTel Instrumentation

Pitfall 1: Capturing full prompt content in every span. Prompts can be thousands of tokens. Storing the full prompt in every span quickly inflates your trace storage costs. Instead, record llm.prompt.length and llm.prompt.template, storing the actual prompt in a separate document store keyed by a hash.

Pitfall 2: Missing sampling for high-volume endpoints. If your inference service handles 10,000 requests/minute, 100% trace sampling is expensive. Use Tail-Based Sampling (available in OTel Collector) to capture 100% of error traces but only 1-5% of successful traces.

Pitfall 3: Forgetting to propagate context through async boundaries. LLM pipelines often use background queues (Celery, SQS, RabbitMQ). If you don't serialize and propagate the W3C trace context, your traces will show up as separate unconnected spans rather than a single pipeline trace.

Pitfall 4: Ignoring model version drift. The same model name can return different versions over time (GPT-4-Turbo's underlying weights are updated). Add model.version or model.revision to your resource attributes if your inference provider exposes this information.


Putting It Together: A Complete Instrumented Pipeline

Here's the full pattern — a FastAPI endpoint with complete OTel instrumentation including context propagation, streaming support, and cost attribution:

from fastapi import FastAPI, Request
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.propagate import extract
from opentelemetry import trace, metrics

app = FastAPI(__name__)
FastAPIInstrumentor.instrument_app(app)

meter = metrics.get_meter(__name__)
request_counter = meter.create_counter("http.llm.requests", "LLM HTTP requests")
token_histogram = meter.create_histogram("llm.tokens", "LLM token usage")

@app.post("/v1/chat")
async def chat(request: Request, body: ChatRequest):
    ctx = extract(dict(request.headers))
    with trace.get_tracer(__name__).start_active_span(
        "chat",
        context=ctx,
        attributes={"http.method": "POST", "llm.model": body.model}
    ) as span:
        # Call LLM (instrumented automatically via OpenAIInstrumentor)
        response = client.chat.completions.create(
            model=body.model,
            messages=body.messages,
            stream=body.stream,
        )

        if body.stream:
            return StreamingResponse(stream_tokens(response, span))
        else:
            return format_response(response)

The combination of automatic instrumentation (OpenAIInstrumentor handles the API calls) and manual spans (you control business logic boundaries) gives you complete visibility without excessive overhead.

Advertisement
Advertisement


Conclusion

OpenTelemetry has matured into the standard for LLM observability, but applying it to AI inference pipelines requires understanding its AI-specific semantic conventions. The key patterns — streaming span aggregation, gen_ai.* attributes for tool calls, W3C context propagation across async boundaries, and PII redaction — are what separate production-grade AI observability from toy implementations.

Building this instrumentation now pays compound dividends as your inference volume grows: the same traces that help you debug a single incident become the dataset for capacity planning, cost attribution, and model selection decisions.

Start with the OpenAI instrumentation package (one line of code), add custom spans for your pipeline-specific logic, and wire up a Grafana dashboard for the metrics that matter to your team. From there, expand into tail-based sampling and cost attribution as your observability needs mature.