{"id":674,"date":"2026-06-24T05:00:19","date_gmt":"2026-06-24T05:00:19","guid":{"rendered":"https:\/\/struct.ai\/articles\/zipkin-distributed-tracing-production-2026\/"},"modified":"2026-06-24T05:00:19","modified_gmt":"2026-06-24T05:00:19","slug":"zipkin-distributed-tracing-production-2026","status":"publish","type":"post","link":"https:\/\/struct.ai\/articles\/zipkin-distributed-tracing-production-2026\/","title":{"rendered":"How to Use Zipkin for Distributed Tracing in Production"},"content":{"rendered":"<p><em>Written by: Nimesh Chakravarthi, Co-founder &amp; CTO, Struct<\/em><\/p>\n<h2 id=\"key-takeaways\">Key Takeaways for a Production Zipkin Pipeline<\/h2>\n<ul>\n<li>A production Zipkin pipeline routes spans through an OpenTelemetry Collector with 1\u20135% tail sampling, then persists to Elasticsearch and exposes the UI behind nginx with authentication.<\/li>\n<li>Baseline MTTR, trace coverage, and alert volume should be measured before implementation so the pipeline\u2019s 80% triage-time reduction can be quantified.<\/li>\n<li>Log-trace correlation via injected <code>trace_id<\/code> fields lets Struct and engineers pivot directly from alerts to full traces without manual searching.<\/li>\n<li>Continuous monitoring of Collector metrics such as dropped spans and sampling ratios prevents silent pipeline degradation and keeps storage costs predictable.<\/li>\n<li><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>Let Struct handle first-pass investigations<\/strong><\/a> \u2014 connect your Zipkin traces to Struct and let AI complete the initial analysis before your engineer is fully awake.<\/li>\n<\/ul>\n<h2>1. Define Objective and Current State<\/h2>\n<p>Start by auditing three numbers: trace coverage (percentage of services that emit spans), weekly alert volume, and mean time to resolution (MTTR). Teams that skip this step routinely over-instrument low-value services while leaving critical payment or auth flows dark.<\/p>\n<p>Map every service to one of three states: uninstrumented, head-sampled only, or tail-sampled with persistent storage. Services in the first two states are the ones that extend MTTR during incidents because they produce incomplete or missing trace data when failures occur. That is why instrumentation priority should follow blast radius, since services that affect the most users or revenue when they degrade are the ones where incomplete traces cost the most investigation time.<\/p>\n<p>Establish baseline MTTR now. The goal of the pipeline described below is to cut active triage time by 80%, turning a 45-minute investigation into a 5-minute review. That delta is only measurable if you record the starting point.<\/p>\n<p><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>Quantify MTTR improvements with Struct<\/strong><\/a> \u2014 connect your Zipkin traces to Struct and let AI complete the first-pass investigation before your engineer is fully awake.<\/p>\n<h2>2. Step-by-Step Implementation<\/h2>\n<p><strong>OTel SDK instrumentation.<\/strong> Instrument each service with the OpenTelemetry SDK for its language. The Go SDK&#8217;s TracerProvider uses AlwaysSample in development, which is appropriate for development but must be replaced in production. Set the sampler via environment variable to avoid code changes at deploy time:<\/p>\n<pre><code># service environment (all languages) OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.05 OTEL_EXPORTER_OTLP_ENDPOINT=http:\/\/otel-collector:4317 <\/code><\/pre>\n<p>ParentBased sampling propagates the root span&#8217;s sampling decision to all child spans, keeping full traces intact rather than producing orphaned child spans with no parent.<\/p>\n<p><strong>OTel Collector gateway configuration (<code>otel-collector-config.yaml<\/code>).<\/strong> Deploy the Collector as a Kubernetes Deployment in a gateway pattern so all nodes funnel spans through a single pipeline. <a href=\"https:\/\/opentelemetry.io\/docs\/collector\/architecture\" target=\"_blank\" rel=\"noindex nofollow\">The Collector receives telemetry via receivers, processes it through configurable pipelines, and exports to one or more backends.<\/a><\/p>\n<pre><code># otel-collector-config.yaml (2026 defaults) receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 zipkin: endpoint: 0.0.0.0:9411 # ingest legacy Zipkin-format spans processors: memory_limiter: check_interval: 1s limit_mib: 512 tail_sampling: decision_wait: 10s num_traces: 50000 policies: - name: errors-policy type: status_code status_code: {status_codes: [ERROR]} - name: slow-traces-policy type: latency latency: {threshold_ms: 1000} - name: probabilistic-policy type: probabilistic probabilistic: {sampling_percentage: 2} exporters: zipkin: endpoint: http:\/\/zipkin:9411\/api\/v2\/spans elasticsearch: endpoints: [https:\/\/es-host:9200] index: otel-traces tls: ca_file: \/certs\/ca.crt service: pipelines: traces: receivers: [otlp, zipkin] processors: [memory_limiter, tail_sampling] exporters: [zipkin, elasticsearch] <\/code><\/pre>\n<p><a href=\"https:\/\/grafana.com\/docs\/opentelemetry\/collector\/sampling\/tail\/\" target=\"_blank\" rel=\"noindex nofollow\">Tail sampling evaluates complete traces after all spans are collected and applies configurable policies to retain those with errors, high latency, or other criteria of interest<\/a>. This approach delivers higher-quality datasets than head-based sampling because decisions are made after the full trace is assembled. <a href=\"https:\/\/dash0.com\/knowledge\/opentelemetry-tracing\" target=\"_blank\" rel=\"noindex nofollow\">The probabilistic_sampler processor controls remaining volume<\/a>, so ingestion costs stay predictable.<\/p>\n<p><strong>Zipkin on Elasticsearch (<code>zipkin-deployment.yaml<\/code>).<\/strong><\/p>\n<pre><code># zipkin-deployment.yaml (2026 defaults) apiVersion: apps\/v1 kind: Deployment metadata: name: zipkin spec: replicas: 2 template: spec: containers: - name: zipkin image: openzipkin\/zipkin:3 env: - name: STORAGE_TYPE value: elasticsearch - name: ES_HOSTS value: https:\/\/es-host:9200 - name: ES_USERNAME valueFrom: secretKeyRef: {name: es-creds, key: username} - name: ES_PASSWORD valueFrom: secretKeyRef: {name: es-creds, key: password} ports: - containerPort: 9411 <\/code><\/pre>\n<p><strong>nginx reverse proxy with basic auth (<code>nginx-zipkin.conf<\/code>).<\/strong> <a href=\"https:\/\/api7.ai\/blog\/api-gateway-vs-reverse-proxy\" target=\"_blank\" rel=\"noindex nofollow\">API gateways, which function as specialized reverse proxies, act as a single control point providing centralized token validation, rate limiting, TLS termination, and centralized logging, whereas traditional reverse proxies primarily handle the core network functions such as TLS termination and routing.<\/a><\/p>\n<pre><code># nginx-zipkin.conf (2026 defaults) server { listen 443 ssl; server_name zipkin.internal.example.com; ssl_certificate \/etc\/ssl\/zipkin.crt; ssl_certificate_key \/etc\/ssl\/zipkin.key; ssl_protocols TLSv1.2 TLSv1.3; add_header Strict-Transport-Security \"max-age=63072000\" always; location \/ { auth_basic \"Zipkin Production\"; auth_basic_user_file \/etc\/nginx\/.htpasswd; proxy_pass http:\/\/zipkin:9411; proxy_set_header X-Request-ID $request_id; } } <\/code><\/pre>\n<p>Zero-trust principles recommend a default deny-all stance and explicitly allowing only necessary communication. Pair this nginx config with a Kubernetes NetworkPolicy that blocks direct pod-to-pod access to port 9411 from outside the observability namespace.<\/p>\n<p><strong>Log-trace correlation.<\/strong> Inject <code>trace_id<\/code> and <code>span_id<\/code> into every structured log line using the OTel SDK&#8217;s logging bridge. This single field is what allows Struct and on-call engineers to pivot from a log line directly to the full trace without manual searching.<\/p>\n<p><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>See trace correlation in action<\/strong><\/a> \u2014 once your pipeline is live, Struct ingests traces and correlated logs to generate a root-cause dashboard in under 5 minutes.<\/p>\n<h2>3. How This Pipeline Fits into Engineering Operations<\/h2>\n<p>The five-layer pipeline you just deployed, SDK, Collector, storage, proxy, and correlation, produces high-fidelity trace data. A trace pipeline that terminates at a Zipkin UI is only half the solution. The operational value appears when traces feed alerting channels and incident handoff workflows.<\/p>\n<p>Configure Zipkin or your Elasticsearch index as a data source in your alerting stack such as Grafana, Datadog, or Prometheus with the <a href=\"https:\/\/dash0.com\/knowledge\/opentelemetry-tracing\" target=\"_blank\" rel=\"noindex nofollow\">spanmetrics connector generating RED-style metrics from trace spans<\/a>. Set SLO-based alerts on p99 latency and error rate derived from span data. When those alerts fire into Slack or PagerDuty, the <code>trace_id<\/code> embedded in the alert payload gives Struct the anchor it needs to pull the full trace, correlated logs, and relevant code context automatically.<\/p>\n<p>For incident handoff, include the Struct dashboard URL in every post-incident review. The dynamically generated timeline, which merges trace spans, log events, and deployment markers, replaces the manual reconstruction that typically consumes the first 30 minutes of a post-mortem.<\/p>\n<p><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>Standardize incident handoffs with Struct<\/strong><\/a> \u2014 encode your team&#8217;s specific escalation paths and runbook steps directly into Struct so every alert follows the same investigation procedure, regardless of who is on call.<\/p>\n<h2>4. Measurement and Improvement of Your Trace Pipeline<\/h2>\n<p>A tracing pipeline that is not monitored will silently degrade. The six metrics below represent the minimum viable monitoring surface, since they catch three failure modes that cause silent data loss and three cost or effectiveness issues that surprise teams at month-end.<\/p>\n<p>The failure modes include dropped spans, backend failures, and sampling misconfiguration. The cost and effectiveness issues include unbounded ingestion, retention drift, and investigations that do not improve MTTR. Use this table as a checklist for day-one instrumentation.<\/p>\n<table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Source<\/th>\n<th>Alert Threshold<\/th>\n<th>What It Indicates<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>otelcol_processor_dropped_spans<\/code><\/td>\n<td>OTel Collector<\/td>\n<td>&gt; 0 sustained for 5 min<\/td>\n<td>Pipeline backpressure or memory limit hit<\/td>\n<\/tr>\n<tr>\n<td><code>otelcol_exporter_send_failed_spans<\/code><\/td>\n<td>OTel Collector<\/td>\n<td>&gt; 0<\/td>\n<td>Elasticsearch or Zipkin backend unreachable<\/td>\n<\/tr>\n<tr>\n<td><code>otelcol_receiver_accepted_spans<\/code><\/td>\n<td>OTel Collector<\/td>\n<td>Drop &gt; 20% week-over-week<\/td>\n<td>Services stopped emitting spans<\/td>\n<\/tr>\n<tr>\n<td>Tail sampling decision ratio (sampled \/ evaluated)<\/td>\n<td>OTel Collector tail_sampling processor<\/td>\n<td>Error-policy rate drops to 0<\/td>\n<td>Error spans not being retained<\/td>\n<\/tr>\n<tr>\n<td>Elasticsearch index size growth rate<\/td>\n<td>Elasticsearch cluster API<\/td>\n<td>&gt; 20 GB\/day (adjust per retention policy)<\/td>\n<td>Sampling rate too high or ILM policy misconfigured<\/td>\n<\/tr>\n<tr>\n<td>MTTR delta (pre\/post pipeline)<\/td>\n<td>Incident tracker<\/td>\n<td>No improvement after 30 days<\/td>\n<td>Traces not correlated to alerts or Struct not ingesting<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>Track your pipeline&#8217;s ROI<\/strong><\/a> \u2014 Struct surfaces MTTR trends and investigation quality scores so engineering leadership can quantify the pipeline&#8217;s impact on reliability.<\/p>\n<h2>5. Common Pitfalls and Storage Decisions<\/h2>\n<p><strong>Over-sampling.<\/strong> <a href=\"https:\/\/opentelemetry.io\/docs\/languages\/go\/sampling\" target=\"_blank\" rel=\"noindex nofollow\">AlwaysSample is recommended only for development or initial setup<\/a> because it retains 100% of spans regardless of their diagnostic value. Leaving it enabled in production generates storage costs that scale linearly with traffic, since every request becomes a stored trace, and it creates Elasticsearch index pressure that degrades query performance during incidents, exactly when fast trace lookup matters most.<\/p>\n<p><strong>Missing trace context in logs.<\/strong> If <code>trace_id<\/code> is absent from log lines, the correlation between a log-based alert and its originating trace is manual. Enforce <code>trace_id<\/code> injection at the logging framework level, such as Logback MDC or Python logging filters, as a non-negotiable instrumentation standard.<\/p>\n<p><strong>No pipeline monitoring.<\/strong> <a href=\"https:\/\/dash0.com\/knowledge\/opentelemetry-tracing\" target=\"_blank\" rel=\"noindex nofollow\">Tail sampling introduces memory and timing considerations<\/a> described in the implementation section above, so poorly chosen policies can drop valuable traces or overwhelm the pipeline. Treat the Collector as a production service, expose its Prometheus metrics endpoint, alert on dropped spans, and include it in your incident runbook.<\/p>\n<p><strong>Elasticsearch vs. Cassandra storage decision.<\/strong> <a href=\"https:\/\/newrelic.com\/blog\/apm\/distributed-tracing-tools\" target=\"_blank\" rel=\"noindex nofollow\">Zipkin supports storage backends including Cassandra and Elasticsearch<\/a>, each with distinct operational trade-offs. The table below maps five decision dimensions to concrete recommendations so your team can decide whether query patterns, write volume, and existing expertise justify Cassandra&#8217;s complexity or whether Elasticsearch&#8217;s query flexibility and ecosystem integration deliver better return.<\/p>\n<table>\n<thead>\n<tr>\n<th>Dimension<\/th>\n<th>Elasticsearch<\/th>\n<th>Cassandra<\/th>\n<th>Recommendation<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Query flexibility<\/td>\n<td>Full-text search, ES|QL aggregations, <a href=\"https:\/\/elastic.co\/docs\/reference\/query-languages\/esql\/functions-operators\/aggregation-functions\/percentile\" target=\"_blank\" rel=\"noindex nofollow\">approximate PERCENTILE via TDigest<\/a><\/td>\n<td>Primary-key lookups, limited ad-hoc queries<\/td>\n<td>Elasticsearch for teams needing ad-hoc trace search<\/td>\n<\/tr>\n<tr>\n<td>Write throughput<\/td>\n<td>High, scales horizontally with shards<\/td>\n<td>Very high, optimized for write-heavy workloads<\/td>\n<td>Cassandra for extreme write volume (&gt;100k spans\/sec)<\/td>\n<\/tr>\n<tr>\n<td>Operational complexity<\/td>\n<td>Moderate, ILM policies manage retention<\/td>\n<td>High, requires tuning compaction and TTLs<\/td>\n<td>Elasticsearch for teams without Cassandra expertise<\/td>\n<\/tr>\n<tr>\n<td>Cross-cluster resilience<\/td>\n<td><a href=\"https:\/\/metoro.io\/blog\/kubernetes-observability\" target=\"_blank\" rel=\"noindex nofollow\">External\/out-of-cluster storage survives cluster failures<\/a><\/td>\n<td>Same benefit when deployed externally<\/td>\n<td>Both, deploy outside the application cluster<\/td>\n<\/tr>\n<tr>\n<td>Ecosystem integration<\/td>\n<td>Native Kibana, Grafana, Struct ingestion<\/td>\n<td>Requires separate query layer for dashboards<\/td>\n<td>Elasticsearch for teams already using the ELK stack<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>6. Tie-in to Automated Investigation Tools<\/h2>\n<p>A hardened Zipkin pipeline, OTel SDK to Collector with tail sampling to Elasticsearch to nginx auth, produces high-fidelity, cost-controlled trace data. That data becomes operationally transformative only when it is consumed automatically at alert time rather than manually at investigation time.<\/p>\n<p>Struct integrates directly into Slack and PagerDuty alerting channels. When an alert fires, Struct queries the Elasticsearch-backed Zipkin index using the <code>trace_id<\/code> embedded in the alert, correlates the trace spans with CloudWatch or Datadog logs, maps the timeline against recent GitHub deployments, and delivers a root-cause dashboard to the Slack thread before the on-call engineer has finished reading the alert. The investigation that previously took 30\u201345 minutes of manual tool-hopping is completed in under 5 minutes, matching the 80% reduction established as the pipeline&#8217;s baseline goal.<\/p>\n<p>For teams with custom runbooks, Struct&#8217;s composable widget system encodes those procedures directly. Specific correlation ID formats, escalation paths, and service-specific diagnostic queries are all executed automatically on every alert, giving junior engineers the same starting context that previously required a senior engineer&#8217;s tribal knowledge.<\/p>\n<h2>Frequently Asked Questions<\/h2>\n<h3>What is the minimum observability maturity required before this pipeline adds value?<\/h3>\n<p>Teams need at least structured logging with a consistent request ID field, one or more services emitting OTel or Zipkin-format spans, and an alerting channel such as Slack or PagerDuty. The pipeline described here is designed to harden and extend an existing dev-grade setup, not to replace basic logging. If your services emit no spans at all, start with OTel auto-instrumentation for your primary language before deploying the Collector gateway.<\/p>\n<h3>How long does a production rollout realistically take?<\/h3>\n<p>For a team with existing Kubernetes infrastructure and a managed Elasticsearch cluster, the Collector deployment, Zipkin backend, and nginx proxy can be production-ready in one to two sprint cycles, roughly two to four weeks. OTel SDK instrumentation across all services typically takes longer and should be prioritized by blast radius. Struct&#8217;s integration with the resulting pipeline takes under 10 minutes once the Elasticsearch index is live and <code>trace_id<\/code> is present in log lines.<\/p>\n<h3>How does this architecture address security and compliance requirements?<\/h3>\n<p>The nginx reverse proxy enforces TLS 1.2 or higher and HTTP authentication, which prevents unauthenticated access to the Zipkin UI and its raw span data. Kubernetes NetworkPolicies block direct pod-to-pod access to the Zipkin port. Elasticsearch credentials are stored as Kubernetes Secrets and injected at runtime. For SOC 2 and HIPAA environments, Struct processes logs and traces ephemerally and holds SOC 2 and HIPAA certifications. Teams with strict requirements that prohibit any data leaving their VPC should evaluate on-premise deployment options before integrating any external tooling.<\/p>\n<h3>Can junior engineers safely use the output of this pipeline during an on-call incident?<\/h3>\n<p>Yes, and this is one of the primary operational benefits. When Struct consumes the trace pipeline and delivers a pre-built root-cause dashboard to the Slack alert thread, a junior engineer&#8217;s starting point matches a senior engineer&#8217;s starting point. They see a correlated timeline, blast radius assessment, and suggested fix. They do not need to know which Elasticsearch index to query or how to interpret raw Zipkin span JSON.<\/p>\n<p>The Slack-native conversational interface also allows them to ask follow-up questions such as \u201cpull logs from 5 minutes before the spike\u201d or \u201ccheck if this affects user segment X\u201d without leaving the incident thread or requiring escalation.<\/p>\n<h3>What is the right tail sampling rate for a high-traffic production service?<\/h3>\n<p>A practical starting point is a probabilistic fallback rate of 1\u20135% combined with policy-based retention for all error spans and all traces exceeding 1,000 ms latency. This combination ensures that the traces most relevant to incidents are always captured while keeping Elasticsearch index growth predictable. Monitor the Collector&#8217;s dropped-span and accepted-span metrics weekly for the first month and adjust the probabilistic rate based on actual storage consumption and the ratio of error traces to total traces. Teams with strict SLAs should bias toward retaining more error traces even at higher storage cost.<\/p>\n<h2>Conclusion<\/h2>\n<p>A production-grade Zipkin pipeline requires five layers working in concert. These layers are OTel SDK instrumentation with ParentBased sampling configured via environment variables, an OTel Collector gateway applying tail sampling policies that always retain errors and slow traces, a Zipkin backend persisted to Elasticsearch deployed outside the application cluster, an nginx reverse proxy enforcing TLS and authentication, and log-trace correlation via injected <code>trace_id<\/code> fields. Each layer addresses a specific failure mode that causes 3 a.m. incidents to drag on, including incomplete traces, unbounded storage costs, unauthenticated UI exposure, and the inability to pivot from a log line to its originating trace.<\/p>\n<p>The pipeline described here is the prerequisite, not the destination. Once high-fidelity, cost-controlled traces flow into Elasticsearch, the operational leverage comes from automating the investigation layer that consumes them. Struct ingests those traces alongside logs and code context, completes the root-cause analysis in under 5 minutes, and delivers the result directly to the Slack thread where the on-call engineer is already working.<\/p>\n<p><a href=\"https:\/\/cal.com\/deepanm\/struct-demo\" target=\"_blank\"><strong>Set up Struct and streamline on-call<\/strong><\/a> \u2014 configure Struct in under 10 minutes and let AI handle your next on-call investigation from first alert to suggested fix.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Set up a production-ready Zipkin tracing pipeline with OTel, Elasticsearch &amp; sampling. Struct cuts triage time by 80%. Start your free trial today.<\/p>\n","protected":false},"author":73,"featured_media":673,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-674","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/posts\/674","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/comments?post=674"}],"version-history":[{"count":0,"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/posts\/674\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/media\/673"}],"wp:attachment":[{"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/media?parent=674"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/categories?post=674"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/struct.ai\/articles\/wp-json\/wp\/v2\/tags?post=674"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}