title: "OpenTelemetry at Scale: Collector Gateways, Tail-Based Sampling & Trace-Log-Metric Correlation in Production" date: 2026-09-15 tags: [Kubernetes, OpenTelemetry, Observability, Prometheus, Grafana, Tempo, Loki, SRE, Platform Engineering] cover: ../images/opentelemetry-collector-scale-tail-sampling-cover.png

Cover

OpenTelemetry at Scale: Collector Gateways, Tail-Based Sampling & Trace-Log-Metric Correlation in Production

1. Topic of the Day

Every platform team that survives its first multi-vendor observability migration learns the same lesson: instrumentation is not the hard part, the pipeline is. Auto-instrumenting a service to emit spans, metrics, and logs takes an afternoon. Operating the infrastructure that collects, enriches, samples, routes, and ships that telemetry — without bankrupting the observability budget or dropping the one trace that would have explained last night's incident — is a permanent, evolving platform engineering problem.

OpenTelemetry (OTel) exists to solve the vendor-lock-in half of this problem: a single, CNCF-graduated (as of May 2026) instrumentation and wire-protocol standard (OTLP) that decouples "how services emit telemetry" from "which backend stores and queries it." That decoupling is now table stakes — traces, metrics, and logs are stable across all major OTel SDKs, and Grafana Tempo, Loki, Prometheus/Thanos, Jaeger, Datadog, Elastic, and every other serious observability vendor speak OTLP natively.

The harder, still-actively-evolving half is the Collector — the component that sits between instrumented workloads and storage backends, and specifically the architecture question every org above a few hundred services eventually confronts: how do you run tail-based sampling (the only sampling strategy that reliably keeps error traces and slow traces while discarding routine successful ones) at a scale where a single collector cannot hold all of a trace's spans in memory, without either dropping the traces that matter or paying to store 100% of everything forever. Layered on top is the correlation problem — a trace, a metric, and a log line about the same request need to reference each other well enough that an on-call engineer can pivot between all three during an incident, which is a schema and pipeline design discipline, not something that happens automatically just because all three signals use OTLP.

This session covers the production collector topology — agent tier, gateway tier, tail-sampling tier — the tradeoffs that shape it, and where OpenTelemetry's own 2026 roadmap (the Profiles signal entering public alpha, memory-efficiency work in the tail-sampling processor) is heading next.

2. Real Business Problem

Scenario: A 600-service platform team migrated off a proprietary APM agent to OpenTelemetry eighteen months ago, motivated by per-host licensing costs and a desire to stop being locked into one vendor's query language. Adoption succeeds at the instrumentation layer — every service ships OTLP traces, metrics, and logs. Then production reality intrudes:

  • Trace storage costs exceed the old APM vendor's bill. With head-based probabilistic sampling at a flat 10%, the team is storing ten times more successful, boring, 40ms-latency traces than they need, while simultaneously — because the 10% decision is made independently per span at ingestion, before anyone knows the request errored — dropping 90% of the error traces and slow traces that actually matter for debugging. Head sampling cannot see the future; it cannot know a trace is "interesting" until the whole trace has completed.
  • A single collector deployment can't hold traces in memory long enough to make a tail-based decision. The team tried enabling the tail_sampling processor on their existing collector fleet and immediately hit OOM kills — spans for a single trace can arrive at different collector replicas (a trace touching twelve microservices produces spans from twelve different pods, load-balanced arbitrarily across the collector Deployment), so no single replica ever sees the complete trace to sample on, and the ones that do see partial traces buffer them in memory until a decision_wait timeout, which balloons memory under load.
  • Incident response takes longer, not shorter, than under the old vendor. During a P1, an SRE finds a spike in http_request_duration_seconds in Grafana, but the dashboard has no exemplar link to a trace, and the trace search UI (keyed by service and time range, not by the metric's label set) takes ten minutes of manual filtering to find a matching slow trace — which, half the time, was one of the 90% that got sampled out anyway.
  • Every team invents its own resource attribute conventions. Team A tags spans with env=prod, Team B with environment=production, Team C doesn't set it at all. Cross-service trace views silently break at team boundaries because there's no enforced semantic-convention schema, and nobody notices until a cross-team incident review needs a unified view that doesn't exist.

The fix requires re-architecting the collector deployment into a topology that can actually make tail-based decisions correctly, keeping the traces that matter without keeping everything, while establishing the correlation and schema discipline that makes the "OTLP for everything" promise actually pay off during an incident instead of merely at procurement time.

3. Production Architecture

Architecture image: blogs/architecture/opentelemetry-collector-scale-tail-sampling-architecture.png

Tier 1 — Node agents (DaemonSet). One OTel Collector agent per node, receiving OTLP from every instrumented pod on that node via localhost or the node's host IP (avoiding a Service hop for the highest-volume traffic path). The agent tier's job is enrichment and cheap cost control, not decisions: the k8sattributes processor stamps pod, namespace, deployment, and node identity onto every span/metric/log from the Kubernetes API's pod-to-IP mapping; memory_limiter protects the agent itself from OOM under a traffic spike; a filelog receiver tails container stdout for logs that aren't emitted via the OTel logging SDK. Crucially, head-sampling at this tier is probabilistic and coarse (e.g., 100% → 10%) purely as a cost control against pipeline volume upstream of the real decision — the actual keep/drop call for tail-based policies happens one tier up, never here.

Tier 2 — Gateway pool, split into a routing tier and a stateful tail-sampling tier. This split is the architectural detail most teams get wrong on the first attempt. A stateless load-balancing exporter tier receives from all agents and re-routes every span using a consistent hash on trace_id, guaranteeing that all spans belonging to one trace land on the same downstream tail-sampling collector replica, regardless of which node or agent originally produced them. Only with this guarantee in place can the stateful tail-sampling collector tier hold a complete trace in memory, evaluate its policy stack (100% of traces containing an ERROR status, 100% of traces above a p99 latency threshold, explicit 100% retention for business-critical paths like /checkout, a flat 5% sample of everything else, and an explicit drop rule for high-volume, zero-value traffic like health checks), and make one correct decision per trace instead of a fragmented, per-span guess.

Tier 3 — Signal-specific gateways. Metrics get their own gateway pool running aggregation and cardinality-control processors before remote_write to Prometheus/Thanos — traces and metrics have fundamentally different cardinality and retention profiles and should not share a pipeline stage that has to compromise between them. Logs get a gateway that runs PII/secrets redaction and stamps trace_id/span_id correlation before shipping to Loki. As of 2026, a Profiles gateway is an emerging fourth lane — the OTel Profiles signal entered public alpha in March 2026, targeting GA around Q3 2026, and represents continuous CPU/heap profiling (eBPF-collected stack samples, OTLP-native, round-tripping losslessly with pprof) as a first-class signal alongside traces, metrics, and logs.

Control plane: an OpAMP (Open Agent Management Protocol) supervisor pushes collector pipeline configuration to the entire agent fleet without a redeploy — this is what makes it operationally feasible to roll out a sampling policy change across thousands of DaemonSet pods. Collector CRDs (via the OpenTelemetry Operator) are GitOps-managed through ArgoCD, and a schema registry enforces resource-attribute and semantic-convention consistency across teams — the direct fix for the env vs. environment drift in Section 2.

Security boundaries: every hop — app-to-agent, agent-to-gateway, gateway-to-backend — is mTLS, typically with SPIFFE/SPIRE-issued workload identities rather than static certificates. Multi-tenancy is enforced at the gateway ingress with per-tenant OTLP auth tokens and a tenant label injected before any downstream processor sees the data, so per-team cost and quota enforcement (the Cost & Cardinality Guard) has a reliable dimension to key on.

Why this shape, and how it evolves: the routing/tail-sampling split exists purely because tail-based sampling is a stateful operation on a naturally sharded key (trace_id), and Kubernetes' default Service load-balancing has no concept of "route by payload content" — the load-balancing exporter fills exactly that gap. As trace volume grows, the tail-sampling tier's memory footprint (proportional to num_traces × in-flight trace size × decision_wait) becomes the dominant cost and scaling constraint, which is precisely the pressure that produced 2026's biggest upstream improvement in this space: disk-backed trace buffering.

4. Solution Design

Design decisions and alternatives:

Decision Alternative Why this choice
Two-tier gateway (stateless LB + stateful tail-sampling) Single collector tier running tail_sampling directly A single tier can't guarantee all spans of one trace land on the same replica; the load-balancing exporter's consistent hash on trace_id is a prerequisite for correct tail decisions at more than one replica.
Tail-based sampling with an explicit policy stack Flat head-based probabilistic sampling Head sampling can't see trace outcome before deciding; it structurally over-retains boring traffic and under-retains the errors and slow requests that justify keeping traces at all.
Disk-backed span buffering (pebbletailstorage) for high-volume services In-memory-only trace buffer In-memory buffering forces a hard tradeoff between decision_wait/num_traces and OOM risk; moving the buffer to a Pebble LSM store on disk lets storage scale with disk capacity instead of pod memory, at roughly 2x CPU cost — a favorable trade at high volume.
Signal-specific gateway pools (traces / metrics / logs / profiles) One shared gateway pipeline for all signals Traces, metrics, and logs have different cardinality, retention, and processing needs (cardinality control for metrics, PII redaction for logs, stateful buffering for traces); a shared pipeline forces compromises that hurt all three.
OpAMP-managed fleet configuration Redeploying collector DaemonSets/Deployments for every config change Config changes (sampling policy tweaks, new processors) are frequent; redeploying a DaemonSet across every node for each change is slow and creates unnecessary rollout risk versus a dynamic config push.
Schema registry enforcing semantic conventions No enforcement, convention-by-documentation Undocumented or unenforced attribute naming drifts silently across teams (the env/environment problem) and breaks cross-service correlation exactly when it's needed most — during a cross-team incident.

Scalability considerations: the routing tier scales horizontally and statelessly (HPA on CPU/queue depth is sufficient). The tail-sampling tier scales by adding replicas and re-hashing trace_id space across them — adding replicas without care for hash redistribution can transiently split in-flight traces, so scale-up events need to be paired with a decision_wait-aware drain strategy, not a naive rolling update.

Cost implications: tail-based sampling is a direct cost lever — a well-tuned policy stack (100% errors/slow, single-digit percent baseline) can cut trace storage volume by 80-95% versus retain-everything while increasing the retention rate of traces an engineer will actually query during an incident. The disk-backed buffer trades object/block storage and roughly double CPU for the ability to run much larger num_traces/decision_wait windows without OOM risk — worth it once buffer memory becomes the binding constraint.

Security implications: the redaction processor in the logs gateway is the single most consequential security control in this pipeline — logs are the signal most likely to accidentally carry PII or secrets (a stack trace with a connection string, a debug log with a request body), and catching that before egress, not after, is materially cheaper than a post-hoc data-deletion request against every backend it landed in.

Performance implications: the routing tier adds one network hop and one hash computation per span versus sending directly to a single gateway tier — negligible at typical span sizes, but worth explicitly measuring against your own latency SLOs for the pipeline itself, since pipeline latency is additive to the "time until an alert can fire on this signal," which matters for tight-SLO systems.

5. Deep Technical Walkthrough

Internal working — a single trace's path through the pipeline:

  1. A request enters the system; the OTel SDK in the ingress service starts a root span, and W3C traceparent propagation carries the trace context across every downstream service call — this is what lets spans generated by twelve different services in twelve different pods all carry the same trace_id.
  2. Each service's SDK batches its spans and exports via OTLP/gRPC to the local node's Collector agent (DaemonSet), typically over localhost to avoid an extra network hop and avoid the agent becoming a shared bottleneck across nodes.
  3. The agent's processor chain runs: memory_limiter first (to reject work before OOM, not after), k8sattributes to enrich with pod/namespace/node metadata pulled from a watch on the Kubernetes API, batch to coalesce into efficient network payloads, and (optionally) a probabilistic head-sampler for coarse volume control before the real decision downstream.
  4. The agent's loadbalancing exporter computes a consistent hash of the span's trace_id and forwards to the specific gateway routing-tier replica that hash maps to — every span for this trace, regardless of which agent/node it originated on, converges on the same downstream replica.
  5. The tail-sampling collector holds the trace's spans in memory (or, at high volume, in the disk-backed pebbletailstorage extension) for up to decision_wait (commonly 10-30 seconds, tuned against your longest-realistic-trace duration), accumulating spans as they arrive out of order and from different services with different network latencies.
  6. At the decision_wait deadline (or, with the span-ingest sampling strategy contributed upstream in 2026, potentially earlier — releasing traces once an ingest-time signal already makes the decision knowable, rather than always waiting the full window), the policy stack evaluates in order: does this trace contain an ERROR-status span? Does its root span duration exceed the p99 latency threshold? Does it match a business-critical route allowlist? If none of those, does it survive the baseline percentage sample? The first matching policy's decision governs the whole trace, atomically.
  7. Kept traces are exported to Tempo/Jaeger (object-storage backed); dropped traces are discarded entirely — no partial retention, since a trace with only some of its spans is close to useless for root-cause work.
  8. In parallel, the metrics and logs gateways process their own signals independently but with shared resource-attribute enrichment (from the same k8sattributes processor upstream), which is what makes a Grafana dashboard's Prometheus exemplar able to link to the matching trace in Tempo — the correlation is a join on shared attributes (trace_id on the metric exemplar, service.name/k8s.pod.name elsewhere), not automatic.

Failure scenarios and recovery: if the tail-sampling tier falls behind (CPU-bound on hashing/policy evaluation, or memory-pressured on buffered traces), the memory_limiter processor refuses new spans before OOM rather than after — a graceful, visible failure (refused-span metrics spike) instead of a silent crash-loop that drops everything currently buffered. If the gateway-to-backend export fails (Tempo unavailable), the sending_queue with persistent (disk-backed) storage retries with backoff rather than dropping data outright, trading latency for durability during a downstream outage.

Scaling behavior: the routing tier scales linearly with span ingest volume. The tail-sampling tier's scaling is bounded by num_traces (how many concurrent in-flight traces one replica can buffer) — at high fan-out (many concurrent traces, each touching many services), this is the tier that needs the most careful capacity planning, and the one where the 2026 disk-backed storage extension changes the math most favorably.

6. Production Troubleshooting

Symptom: Grafana dashboards show a latency spike, but Tempo trace search returns nothing for the affected time window.

Investigation path a senior SRE would follow:

  1. Check the tail-sampling policy stack first, not the storage backend. Query the collector's own internal metrics (otelcol_processor_tail_sampling_sampling_trace_dropped_total and otelcol_processor_tail_sampling_sampling_policy_evaluation_error_total) — a spike in dropped traces during exactly the incident window usually means the latency-threshold policy's threshold value is stale relative to a service's now-shifted baseline latency, or the ERROR-status policy isn't matching because the failing service returns a non-2xx HTTP status without setting the span's OTel status to ERROR (a common instrumentation gap — HTTP status code and span status are not automatically the same thing in every SDK/framework integration).
  2. Check for hash-space skew. If one trace pattern (e.g., a hot customer ID, a specific route) accounts for disproportionate volume, the consistent hash on trace_id won't cause skew (trace_id is high-cardinality by nature), but if the load-balancing exporter is misconfigured to hash on a lower-cardinality key by mistake, a handful of gateway replicas can become hot while others sit idle — check per-replica CPU/memory across the tail-sampling Deployment for an uneven distribution.
  3. Check decision_wait against actual trace completion time. otelcol_processor_tail_sampling_sampling_trace_removal_age compared against the service's real p99 end-to-end latency reveals whether traces are being evaluated (and released, with an incomplete span set) before all their spans have actually arrived — a classic hidden data-loss mode that looks like "traces exist but are missing service hops" rather than "traces are outright missing."
  4. Check memory_limiter refusal metrics on both agent and gateway tiers. otelcol_processor_refused_spans_total spiking during the incident window means the pipeline itself became the bottleneck during the very traffic spike you're trying to diagnose — a self-inflicted observability blind spot precisely when observability matters most, and a strong argument for provisioning collector headroom above steady-state, not tightly to it.
  5. Cross-check the Cost & Cardinality Guard's alerting. If a dependent team recently shipped a change that added a high-cardinality attribute (a raw user ID or request UUID as a span attribute, not just as an event), cardinality explosion in the metrics pipeline can starve shared collector resources across every tenant sharing that gateway pool — check for a correlated spike in unique label-set counts on the metrics gateway around the same time.

Root cause, commonly: either a policy configuration gap (latency threshold not scaled with the service, error-status mapping gap) or resource exhaustion on the tail-sampling tier during exactly the traffic pattern that also caused the underlying incident — both of which point to the same fix: policy stacks and capacity plans need to be validated against real incident traffic patterns periodically, not set once at rollout and left alone.

7. Hands-on Lab

Goal: stand up a minimal two-tier OTel Collector pipeline on a local Kubernetes cluster, demonstrate tail-based sampling keeping error traces while dropping successful ones, and validate trace-to-metric correlation.

# Prerequisites: kind, helm, kubectl

kind create cluster --name otel-lab

# Install the OpenTelemetry Operator (manages Collector CRDs)
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm install otel-operator open-telemetry/opentelemetry-operator \
  -n otel-system --create-namespace --set admissionWebhooks.certManager.enabled=false

# Deploy a minimal Tempo + Prometheus + Grafana stack for backends
helm repo add grafana https://grafana.github.io/helm-charts
helm install tempo grafana/tempo -n observability --create-namespace
helm install prometheus grafana/prometheus -n observability
helm install grafana grafana/grafana -n observability

cat <<'EOF' | kubectl apply -f -
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: gateway
  namespace: otel-system
spec:
  mode: deployment
  replicas: 2
  config:
    receivers:
      otlp:
        protocols: {grpc: {}, http: {}}
    processors:
      memory_limiter: {check_interval: 2s, limit_mib: 512}
      tail_sampling:
        decision_wait: 10s
        num_traces: 50000
        policies:
          - name: keep-errors
            type: status_code
            status_code: {status_codes: [ERROR]}
          - name: keep-slow
            type: latency
            latency: {threshold_ms: 500}
          - name: baseline-sample
            type: probabilistic
            probabilistic: {sampling_percentage: 5}
      batch: {}
    exporters:
      otlp/tempo:
        endpoint: tempo.observability.svc:4317
        tls: {insecure: true}
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, tail_sampling, batch]
          exporters: [otlp/tempo]
EOF

# Generate synthetic traffic: a mix of fast/successful and slow/error requests
kubectl run loadgen --image=ghcr.io/otel-example/loadgen:latest --restart=Never \
  -- --duration=120s --error-rate=0.05 --p99-latency-ms=800 --target=otlp-gateway.otel-system.svc:4317

# Validate: query Tempo for ERROR-status traces (should be ~100% retained)
kubectl exec -n observability deploy/tempo -- \
  wget -qO- 'http://localhost:3100/api/search?tags=status.code=ERROR' | jq '.traces | length'

# Validate: compare total generated traces vs. traces actually stored (should show ~5-15% retention, weighted toward errors/slow)
kubectl logs -n otel-system deploy/gateway-collector | grep tail_sampling

# Cleanup
kubectl delete namespace otel-system observability
kind delete cluster --name otel-lab

What to validate: that ERROR-status and >500ms-latency traces show materially higher retention than the flat baseline percentage (confirming the tail-sampling policy stack works as intended, not just passing through everything or nothing), that memory_limiter refusal counters stay at zero under the lab's load profile (headroom is adequate), and that a Prometheus exemplar on a latency histogram bucket links to a trace_id that is actually retrievable in Tempo — the end-to-end correlation check that matters most in a real incident.

8. Production Case Study

Large-scale engineering organizations converge on the routing-tier/tail-sampling-tier split described here for a structural reason: at high service counts and trace fan-out, there is no way to make a correct tail-based sampling decision without first guaranteeing trace affinity, and the load-balancing-exporter pattern is the accepted solution across the OpenTelemetry ecosystem rather than a bespoke one-off. Organizations operating at Uber- and Netflix-scale trace volumes have long run some variant of this shape even before OpenTelemetry standardized it — proprietary tracing systems at that scale (Uber's Jaeger, originally built in-house before donation to CNCF) pioneered exactly this problem: route by trace ID, sample at the edge of storage, not at the edge of instrumentation.

The 2026 upstream contribution from Elastic — cutting tail-sampling memory usage by up to 65% via disk-backed span buffering and an ingest-time sampling strategy that releases traces before the full decision_wait elapses when a decision is already knowable — reflects the same cost pressure every org running tail-based sampling at scale eventually hits: buffering complete traces in memory is the single most expensive part of the whole pipeline, and any architecture change that reduces that cost without sacrificing sampling accuracy compounds directly into either lower infrastructure spend or the ability to retain traces longer/more accurately at the same spend. Companies with mature observability platforms treat cache-hit-rate-style metrics for their sampling pipeline (retention rate by error/latency/baseline category, policy evaluation error rate) as first-class SLIs for the observability platform itself, not just for the services it observes — recognizing that the observability pipeline is production infrastructure with its own reliability requirements, not a side project.

9. Architecture Review

Strengths: the routing/tail-sampling split correctly solves the fundamental sharding problem tail-based sampling has at more than one collector replica; signal-specific gateway pools avoid forcing traces, metrics, and logs to share processing tradeoffs that don't fit all three; OpAMP-driven fleet configuration makes policy iteration operationally cheap enough to actually happen (versus a redeploy-per-change model nobody wants to run often, which in practice means nobody tunes the sampling policy after initial rollout).

Weaknesses: the tail-sampling tier remains a stateful, memory/disk-sensitive component whose capacity planning is genuinely harder than the largely-stateless rest of the pipeline — it's the one tier where "just add replicas" isn't a free scaling lever, because adding replicas changes the hash-space partitioning and can transiently affect in-flight traces during a scale event. The schema-registry/semantic-convention enforcement is a governance process as much as a technical control, and governance processes are the parts of a platform most likely to decay without active ownership.

What fails first at 10x scale: the tail-sampling tier's memory (or disk I/O, if using the Pebble-backed buffer) under num_traces growth, and the metrics gateway's cardinality-control processors under label-set growth from more services — not the stateless routing tier or the agent tier, both of which scale close to linearly with node/pod count.

How it changes at very large (100M-user-product) scale: expect the tail-sampling tier to shard not just by trace_id hash but by a coarser dimension first (region, tenant, or service-mesh boundary) to keep any single tail-sampling replica pool's num_traces bounded to a manageable working set, mirroring the same regional-sharding pattern used for metrics federation (Thanos) elsewhere in this series. The Profiles signal, once GA, likely gets its own independently-scaled gateway lane from day one at this scale, rather than being bolted onto an existing pipeline — continuous profiling's data volume and query patterns are different enough from traces/metrics/logs to warrant it.

What would be redesigned: invest earlier in per-signal, per-tenant collector resource quotas and dashboards (mirroring the Cost & Cardinality Guard) rather than a single fleet-wide observability-of-observability view, and build automated policy-drift detection — alerting when a tail-sampling policy's retention rate for a given category (errors, slow, baseline) shifts materially without a corresponding, deliberate config change, since that shift is usually the first visible sign of either a traffic-pattern change or an instrumentation regression upstream.

10. Best Practices

Reliability means treating the Collector fleet — agent, routing, and tail-sampling tiers alike — as production infrastructure with its own SLOs, not a best-effort sidecar to the "real" workloads: memory_limiter tuned with real headroom, refusal and drop metrics alerted on, and the pipeline's own latency (span-generated-to-queryable) tracked as a first-class SLI. Scalability means keeping the three independently-scaling tiers — stateless agents, stateless routing, stateful tail-sampling — explicitly separate in both deployment topology and capacity planning, since conflating them (running tail-sampling logic on the same replicas that do routing, for instance) collapses two very different scaling profiles into one and makes both harder to reason about.

On observability of the observability pipeline itself, track sampling retention rate by policy category (not just overall trace volume) as the leading indicator that catches instrumentation regressions (a service silently stops setting span status to ERROR) before someone notices missing traces during an actual incident. On security, redact PII/secrets in the logs gateway before egress, not after, and issue workload identities via SPIFFE/SPIRE rather than static mTLS certificates so identity rotation doesn't require a manual certificate-management process. On cost, tune tail-sampling policy thresholds against real service latency baselines (not one global threshold for every service, which either over-retains fast services or under-retains slow ones), and treat cardinality growth in the metrics pipeline as a cost and reliability signal requiring the same review discipline as a schema migration. On maintainability, manage every Collector pipeline config as GitOps-reviewed code, and enforce semantic-convention schema centrally rather than per-team, because attribute-naming drift is nearly invisible until a cross-team correlation query needs it to not exist.

11. Common Production Mistakes

The most common mistake is enabling tail_sampling on a collector tier that hasn't first solved trace affinity — teams see the processor exists, turn it on, and are confused when sampling behaves inconsistently, because without the load-balancing-exporter tier in front of it, each replica only ever sees a fragment of most traces and makes decisions on incomplete data. A close second is setting a single global latency threshold for the "keep slow traces" policy across services with wildly different baseline latencies — a threshold tuned for a 50ms API silently retains almost nothing useful for a 2-second batch-processing service, and vice versa; thresholds need to be set per-service (or per-route) against that service's own baseline, not globally.

A third mistake is treating span status and HTTP status as the same thing — many instrumentation libraries do not automatically set OTel span status to ERROR just because the underlying call returned a 4xx/5xx, which silently defeats an ERROR-based tail-sampling policy for exactly the traces it exists to keep; this needs to be explicitly verified per-framework, not assumed. A fourth is ignoring cardinality on span and log attributes — an unbounded attribute (raw user ID, full request body as a log field) blows up cost and query performance in the metrics/logs pipelines even though traces themselves tolerate high cardinality reasonably well, and the cost shows up downstream, disconnected from the instrumentation change that caused it, which makes root-causing it later far more expensive than reviewing the instrumentation change up front. Finally, teams frequently under-invest in the schema/semantic-convention governance layer, assuming documentation alone will keep attribute naming consistent across teams — it reliably does not, and the fix (a lightweight schema registry or CI check against the OTel semantic conventions spec) is cheap relative to debugging a broken cross-service trace view during an incident review months later.

12. Interview Preparation

Q: Why can't you just enable tail-based sampling on your existing stateless collector Deployment? A: Tail-based sampling requires seeing every span belonging to a trace before deciding whether to keep or drop it, but Kubernetes Service load-balancing routes traffic without any awareness of trace_id — spans for one trace can land on different replicas, each of which only sees a fragment and can't make a correct decision. The fix is a routing tier running a load-balancing exporter that hashes on trace_id, guaranteeing all of one trace's spans converge on the same downstream tail-sampling replica before the stateful decision logic runs.

Q: Explain the tradeoff between decision_wait, num_traces, and memory usage in the tail-sampling processor, and how the 2026 disk-backed storage extension changes it. A: The tail-sampling processor must buffer every in-flight trace's spans in memory until decision_wait elapses (or an earlier ingest-time signal makes the decision knowable), and num_traces bounds how many concurrent traces one replica can hold — increasing either without bound risks OOM under real traffic. The pebbletailstorage extension moves this buffer to a Pebble LSM-tree store on disk, so buffer capacity scales with disk rather than pod memory, letting operators raise decision_wait/num_traces substantially (useful for services with long-running or high-fan-out traces) at roughly double the CPU cost — a favorable trade once memory, not CPU, is the binding constraint.

Q: How do you actually achieve trace-to-metric-to-log correlation in an OpenTelemetry pipeline — is it automatic just because everything uses OTLP? A: No — correlation is a deliberate design choice, not a side effect of a shared wire protocol. It requires: consistent resource attributes (service.name, k8s.pod.name) applied to all three signals from the same enrichment processor upstream; exemplars on metrics that embed the trace_id of a request that contributed to that metric bucket; and trace_id/span_id fields explicitly attached to log records (either by the SDK's logging bridge or a processor). Skipping any of these three leaves you with three signals that all happen to use OTLP but can't actually be pivoted between during an incident.

Q: A team's cross-service trace views are inconsistent — spans from different services don't line up cleanly in the trace viewer. What's your first hypothesis? A: Semantic-convention or resource-attribute drift between teams — one service tagging environment=production and another env=prod, or inconsistent service.name naming, breaks the implicit joins the trace viewer and any cross-service dashboards depend on. This is a governance gap, not a collector bug; the fix is a schema registry or CI-enforced check against the OpenTelemetry semantic conventions specification, applied at instrumentation time, not after the fact.

Q: What's the security argument for issuing workload identities via SPIFFE/SPIRE rather than static mTLS certificates for the collector pipeline? A: Static certificates require manual (or separately-automated) rotation and revocation processes, and a leaked static cert remains valid until someone notices and revokes it. SPIFFE/SPIRE issues short-lived, workload-attested identities automatically re-issued on a rotation schedule, so a compromised identity has a bounded, short window of validity by default, and identity issuance is tied to a verifiable attestation of what's actually running rather than a certificate file that could in principle be copied elsewhere.

13. Latest Industry Updates

OpenTelemetry graduated to full CNCF project status in May 2026, cementing it alongside Kubernetes and Prometheus as foundational, de facto-standard cloud-native infrastructure — traces, metrics, and logs are now stable across all major SDKs, which is the signal most enterprise platform teams were waiting for before committing production migrations off proprietary APM agents. The Collector itself continues rapid iteration — the v1.49.0/v0.143.0 release line in January 2026 continued the project's pattern of stabilizing core receivers/exporters under the v1 API surface while keeping newer, experimental components on the v0 line, a distinction platform teams should track closely when deciding which processors are safe to depend on for long-term pipeline stability versus which are still subject to breaking changes.

On sampling specifically, Elastic's upstream contributions in 2026 — the span-ingest sampling strategy (releasing traces once a decision is knowable rather than always waiting the full decision_wait) and the pebbletailstorageextension (disk-backed span buffering) — cut tail-sampling memory usage by up to 65% in production benchmarks, and both are now available to any team running the upstream Collector, not gated behind a vendor distribution. This matters because it directly lowers the capacity-planning barrier that has historically pushed teams toward head-based sampling (worse decision quality, but predictable memory) over tail-based sampling (better decision quality, historically expensive memory) purely for operational-risk reasons.

The most consequential forward-looking development is the Profiles signal, which entered public alpha in March 2026 as OpenTelemetry's fourth core signal alongside traces, metrics, and logs — continuous, eBPF-collected CPU and heap stack-trace sampling, OTLP-native, round-tripping losslessly with the existing pprof format while cutting wire size roughly 40% via a shared string dictionary. GA is targeted for Q3 2026, and platform teams should treat 2026 as the evaluation window: alpha is explicitly not yet recommended for critical production workloads, but the direction is clear enough that teams designing new collector topologies now should leave an architectural lane for a fourth signal gateway rather than assuming three signals is the permanent shape of the pipeline.

Sources: OpenTelemetry Profiles Enters Public Alpha, OpenTelemetry's profiles signal enters public alpha — ClickHouse, OpenTelemetry tail sampling: 65% less memory with disk storage — Elastic Observability Labs, OpenTelemetry Collector v1.49.0/v0.143.0: What's New in January 2026, OpenTelemetry Project Roadmap, Tail-Based Sampling with the OpenTelemetry Collector

14. Summary & Cheat Sheet

Key concepts: OpenTelemetry standardizes instrumentation and wire protocol (OTLP), decoupling emission from storage backend; the hard production problem is the Collector pipeline topology, specifically making tail-based sampling correct at more than one replica (requires trace-affinity routing) and making trace/metric/log correlation real (requires deliberate shared resource attributes and exemplar/trace_id linking, not automatic just because all three use OTLP).

Architecture in one line: app SDKs emit OTLP → node-agent DaemonSet enriches and coarsely head-samples → load-balancing exporter hashes on trace_id to guarantee affinity → stateful tail-sampling gateway holds complete traces and applies an error/latency/business-path/baseline policy stack → signal-specific gateways (traces, metrics, logs, and emerging profiles) ship to their respective backends, correlated via shared resource attributes and exemplars.

Head vs. tail sampling:

Head-based Tail-based
Decision point At span creation, before outcome known After trace completes, outcome known
Error/slow-trace retention Poor (proportional to sample rate only) Excellent (explicit policy can retain 100%)
Memory/infra cost Low (stateless) Higher (must buffer complete traces)
Requires trace-affinity routing No Yes

Key commands / config:

# tail_sampling policy stack (collector config)
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    policies:
      - {name: keep-errors, type: status_code, status_code: {status_codes: [ERROR]}}
      - {name: keep-slow, type: latency, latency: {threshold_ms: 500}}
      - {name: baseline, type: probabilistic, probabilistic: {sampling_percentage: 5}}
kubectl get opentelemetrycollectors -A                       # collector CRD status
kubectl logs -n otel-system deploy/gateway-collector | grep tail_sampling
# Key otelcol self-metrics to alert on:
#   otelcol_processor_refused_spans_total
#   otelcol_processor_tail_sampling_sampling_trace_dropped_total
#   otelcol_processor_tail_sampling_sampling_policy_evaluation_error_total

Best-practice checklist:

  • Never enable tail-based sampling without a trace-affinity (load-balancing exporter) tier in front of it.
  • Set latency-threshold policies per-service against real baselines, not one global threshold.
  • Verify span status actually reflects error outcomes per-framework — don't assume HTTP status maps automatically.
  • Enrich all three signals (traces, metrics, logs) with the same resource attributes from one shared processor for real correlation.
  • Enforce semantic-convention schema centrally; don't rely on per-team documentation discipline.
  • Move to disk-backed span buffering before memory becomes the tail-sampling tier's binding constraint.

Troubleshooting checklist for "traces missing during an incident":

  1. Check tail-sampling drop/evaluation-error metrics for the incident window before assuming a storage-backend problem.
  2. Verify the failing service's span status is actually set to ERROR, not just its HTTP status code.
  3. Check for hash-space skew or replica-level resource exhaustion on the tail-sampling tier.
  4. Compare decision_wait against real end-to-end trace completion time for that service.
  5. Check memory_limiter refusal counters on both agent and gateway tiers for self-inflicted pipeline bottlenecks during the same traffic spike.