Back to Blog
OpenTelemetryTest AutomationCI/CDObservabilityQuality Engineering

Your Tests Are Blind. OpenTelemetry Can Fix That.

Most test pipelines are flying blind — they check pass/fail but ignore what the system was actually doing during the test. Wiring OpenTelemetry distributed traces directly into your quality gates turns testing from a binary verdict into a signal-driven feedback loop. Here's how I do it in practice.

May 7, 2026
6 min read
RS
Raju Shanigarapu

Pass/Fail Is Not a Quality Signal

A green test suite is not evidence of a healthy system. It's evidence that your assertions didn't trip. Those are not the same thing.

I've watched services pass every automated test and still silently degrade — p99 latency creeping from 120ms to 800ms, a downstream gRPC call retrying three times before succeeding, a database query plan changing after a schema migration. None of it visible in JUnit reports. All of it captured in traces.

The assertion gap is real: your tests tell you what happened at the boundary; distributed traces tell you why it happened inside. The only reason we've tolerated that gap for so long is that wiring trace data into a test pipeline used to require a non-trivial observability stack. In 2026, it doesn't.

What the OTel Ecosystem Actually Looks Like Now

OpenTelemetry graduated from CNCF and is now the de facto instrumentation standard. As of April 2026, the Collector is at v0.151.0. It exports natively to Datadog, New Relic, Honeycomb, Jaeger, AWS X-Ray, Azure Monitor, and Google Cloud — zero vendor-specific SDK code required. The March 2026 deprecation of OpenTracing compatibility in the OTel spec is the final nail: if you're still running OpenTracing bridges in new integrations, you're accumulating tech debt with a scheduled removal date no earlier than March 2027.

The bigger structural shift is the Profiles signal entering public Alpha on March 26, 2026, co-authored by engineers from Google, Datadog, and Elastic. Elastic donated its eBPF profiling agent to the project, which means you'll soon be able to correlate a flame graph down to a specific CPU-consuming function from a single business transaction ID — no additional instrumentation, low overhead, continuous on Linux. Profiling GA targets Q3 2026. When it ships, the concept of a "test run" as a discrete observability boundary becomes genuinely powerful.

The CI/CD Observability SIG has been shipping semantic conventions for pipeline telemetry since late 2023, meaning your build spans, test spans, and deployment spans can now live in the same trace context as your service spans. That's the architectural primitive that makes signal-driven quality gates possible.

The Architecture: Trace Context Flows Through the Test

Here's the concrete pattern I use. The test process creates a root span before executing any action, propagates that trace context into every HTTP or gRPC call it makes, and then queries the trace backend after the test completes to evaluate what actually happened — not just whether the final assertion passed.

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
import requests, time

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317")))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("qa.pipeline")

def test_checkout_flow(http_client):
    with tracer.start_as_current_span("test.checkout") as span:
        trace_id = format(span.get_span_context().trace_id, '032x')
        span.set_attribute("test.suite", "payment-regression")
        span.set_attribute("test.env", "staging")

        response = http_client.post("/api/checkout", json={"cart_id": "abc123"})
        assert response.status_code == 200

    # Allow spans to flush
    time.sleep(2)

    # Query trace backend for quality gate evaluation
    evaluate_trace_quality(trace_id)

The evaluate_trace_quality function is where the real work happens. It queries Jaeger or Honeycomb for all spans under that trace ID and checks things like: Did any downstream span exceed the SLO threshold? Were there retries that the HTTP layer swallowed? Did a span show a cache miss where a hit was expected?

This is not checking test assertions. This is checking system behavior during the test.

Quality Gates That Actually Mean Something

A quality gate built on trace data can catch things that no assertion can:

Latency regression at the span level. Your checkout test passed, but the payment-service → fraud-check span went from 45ms to 340ms in this build. That's not a test failure yet. It's a signal that should block a production deploy until someone explains it.

Silent retries. The gRPC client retried twice before succeeding. The test passed because it eventually got a 200. The trace recorded three attempts. That's a flaky dependency, not a healthy service.

Cascade depth changes. A feature flag enabled in staging caused a synchronous call to fan out to four services instead of two. Response time is within SLO, but the dependency graph changed. You want to know that before it runs in production under 10x traffic.

None of these show up in a test report. All of them show up in a trace. The gate logic I run in CI looks roughly like this: parse the trace for all spans, compute p99 per service, check retry count attributes, diff the span graph shape against a stored baseline from the previous passing build. If any of those deviate beyond a threshold, the gate fails — with a link to the exact trace in Honeycomb, not a cryptic error message.

The Collector Is Your Pipeline Middleware

Don't underestimate the Collector's role here. At v0.151.0, the filterprocessor and tailsamplingprocessor components let you make intelligent decisions about which traces get forwarded where. In a test pipeline context, I route 100% of traces generated during test execution to a dedicated backend (Jaeger for local, Honeycomb for staging), completely separate from production telemetry. This gives you full fidelity on test-generated traces without polluting production dashboards or running up sampling costs.

The Collector pipeline config for this is straightforward:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  filter/qa:
    traces:
      include:
        match_type: strict
        attributes:
          - key: test.suite
            value: ".*"

exporters:
  jaeger:
    endpoint: jaeger:14250
  honeycomb:
    api_key: ${HONEYCOMB_API_KEY}
    dataset: qa-traces

service:
  pipelines:
    traces/qa:
      receivers: [otlp]
      processors: [filter/qa]
      exporters: [jaeger, honeycomb]

Anything with a test.suite attribute goes to the QA pipeline. Everything else flows through normal production routing. Clean separation, no custom code.

The Profiling Alpha Changes the Endgame

The Profiles signal entering public Alpha is worth paying attention to now, even if you're not instrumenting for it yet. When it reaches GA in Q3 2026, you'll be able to correlate a flame graph to a trace ID — meaning a test that triggered a CPU spike in the auth service will surface which function was responsible, not just that latency increased.

For QA pipelines, this is the difference between "the performance test failed" and "the performance test failed because JWTValidator.decode() is being called 14 times per request instead of once, and here's the stack trace." Root cause from a test run, not from a production incident.

Start tagging your test-generated traces with structured attributes now — test.suite, test.env, test.build_id, test.scenario. That data becomes the correlation key when profiling stabilizes.

Wire One Test First

Don't redesign your pipeline. Pick one high-value integration test — the one that's caught the most real regressions in the last six months — and instrument it end-to-end with OTel trace context. Export to a local Jaeger instance (docker run -p 16686:16686 jaeger), capture the trace ID, and spend 30 minutes manually exploring what the system actually does during that test.

You'll find something surprising in that trace. You always do. That surprise is the argument for doing it at scale — and it's a much more convincing argument than anything I can write here.

Want to build systems that work this way?

I work with QA engineers and engineering teams on automation architecture, framework audits, and AI-powered quality systems.

Get posts like this in your inbox

No fluff. Sharp takes on QA, AI, and engineering — once a week.