OpenTelemetry
OpenTelemetry (OTel) is the vendor-neutral standard for tracing, metrics, and logs — replacing fragmented agents (Jaeger client, Prometheus client, custom MDC) with one SDK and…
Introduction
OpenTelemetry (OTel) is the vendor-neutral standard for tracing, metrics, and logs — replacing fragmented agents (Jaeger client, Prometheus client, custom MDC) with one SDK and OTLP export. Distributed tracing propagates trace context across Spring Boot services, Kafka consumers, and JDBC calls — one trace ID follows a payment request from gateway to database.
Java integration: OpenTelemetry Java agent (zero-code attach) or Spring Boot 3 Micrometer OTel bridge. Export to Grafana Tempo, Jaeger, Datadog, or Honeycomb via OTLP. Staff engineers mandate trace propagation before microservice count exceeds five — otherwise incidents are blind.
Business problem
Operating microservices without observability:
- Blind incidents: Payment timeout — which of 8 services slow? Hours of log grep.
- No correlation: Separate trace ID, request ID, log MDC — can't link metrics spike to failing traces.
- Vendor lock-in: Datadog agent + Jaeger + Prometheus — three SDKs, three configs.
- Missing SLO data: No histogram of checkout latency — guessing p99 from averages.
- Kafka black box: Consumer lag visible but which message processing fails? — need span per consume.
Why this topic exists
OpenTelemetry unifies the three pillars with one context propagation model:
- Traces: Spans with parent-child hierarchy — visualize request waterfall across services.
- Metrics: Counters, gauges, histograms — RED (Rate, Errors, Duration) per service.
- Logs: Structured logs with trace_id injected — click trace → see logs; click log → see trace.
- Context propagation: W3C traceparent header — gateway → order → payment → Kafka consumer.
- OTLP: Single export protocol to any backend — switch vendor without re-instrumenting app.
Core concepts
OpenTelemetry concepts for Java:
- Tracer: Creates spans —
span = tracer.spanBuilder("processPayment").startSpan(). - Span: Named operation with start/end time, attributes (http.status_code=200), events.
- Trace: Tree of spans sharing trace_id — root span from gateway, child spans per service call.
- Context propagation: Inject traceparent into HTTP headers and Kafka record headers on produce; extract on consume.
- Meter: Records metrics —
counter.add(1, attributes)for orders_processed_total. - Auto-instrumentation: Java agent instruments Spring MVC, JDBC, Kafka, HttpClient without code changes.
Internal architecture
Distributed trace across Java microservices:
Client POST /checkout│▼ trace_id=abc123 span_id=root┌─────────────┐│ API Gateway │ span: HTTP POST /checkout (120ms)└──────┬──────┘│ traceparent: 00-abc123-root-01▼┌─────────────┐│ Order Svc │ span: createOrder (80ms)│ │ ├─ span: JDBC insert (15ms)│ │ └─ span: kafka send order.events (5ms)└──────┬──────┘│ Kafka header: traceparent▼┌─────────────┐│ Inventory │ span: consume OrderCreated (40ms)└─────────────┘OTLP export → OpenTelemetry Collector → Tempo (traces)→ Prometheus (metrics)→ Loki (logs via trace_id)
Tracing, metrics, logs, and propagation:
Code walkthrough
Manual OpenTelemetry spans in Java + Spring Boot 3 Micrometer:
- try (Scope scope): Makes span current — child HTTP/JDBC auto-instrumentation nests under it.
- setAttribute: Searchable facets in trace UI — filter by payment.currency=USD.
- recordException: Stack trace attached to span — visible in Jaeger/Tempo.
- sampling.probability: 10% in prod reduces cost — always sample errors if tail-based sampling configured.
// build.gradle — Spring Boot 3.2+dependencies {implementation("io.opentelemetry:opentelemetry-api:1.36.0")implementation("org.springframework.boot:spring-boot-starter-actuator")implementation("io.micrometer:micrometer-tracing-bridge-otel")implementation("io.opentelemetry:opentelemetry-exporter-otlp")}// Manual span in payment logic@Servicepublic class PaymentService {private final Tracer tracer = GlobalOpenTelemetry.getTracer("payment-service");public PaymentResult charge(PaymentRequest req) {Span span = tracer.spanBuilder("charge").setSpanKind(SpanKind.SERVER).startSpan();try (Scope scope = span.makeCurrent()) {span.setAttribute("payment.amount", req.amount());span.setAttribute("payment.currency", req.currency());validate(req);PaymentResult result = gateway.charge(req);span.setStatus(StatusCode.OK);return result;} catch (Exception e) {span.recordException(e);span.setStatus(StatusCode.ERROR, e.getMessage());throw e;} finally {span.end();}}}// application.yml — OTLP exportmanagement:tracing:sampling:probability: 1.0 # 100% dev; 0.1 prodotlp:tracing:endpoint: http://otel-collector:4318/v1/tracesmetrics:export:otlp:enabled: true
Production example
Java agent auto-instrumentation + Kafka propagation:
- -javaagent: Zero-code instrumentation for Spring, JDBC, Kafka, RestTemplate — fastest adoption path.
- OTEL_SERVICE_NAME: Appears in every span — filter traces by service in UI.
- Kafka inject/extract: Consumer continues same trace — end-to-end checkout visibility.
- traceId in logs: MDC populated by Micrometer bridge — correlate logs to traces in Grafana.
# Dockerfile — attach OpenTelemetry Java agentENV OTEL_SERVICE_NAME=payment-serviceENV OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317ENV OTEL_TRACES_EXPORTER=otlpENV OTEL_METRICS_EXPORTER=otlpENV OTEL_INSTRUMENTATION_KAFKA_ENABLED=trueENTRYPOINT ["java", "-javaagent:/otel/opentelemetry-javaagent.jar", \"-jar", "/app.jar"]# Kafka producer — propagate trace context in headers (agent does auto)# Manual if needed:public ProducerRecord<String, OrderEvent> withTrace(OrderEvent event) {ProducerRecord<String, OrderEvent> record =new ProducerRecord<>("order.events", event.orderId(), event);GlobalOpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), record, (carrier, key, value) ->carrier.headers().add(key, value.getBytes(StandardCharsets.UTF_8)));return record;}# Grafana dashboard — RED metrics from Micrometer# rate(http_server_requests_seconds_count{service="payment"})# histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))# Structured logging with trace_id (Logback)# logging.pattern.level=%5p [traceId=%X{traceId} spanId=%X{spanId}]
Enterprise case study
Stripe — observability culture: Stripe invests heavily in distributed tracing for payment flows — every API request traceable end-to-end. Java teams adopting OTel mirror this: sampling strategy is critical — 100% traces at Stripe-scale is petabytes; use probabilistic sampling (1–10%) plus tail-based sampling (keep all errors and slow traces). Lesson: define SLO on trace-derived metrics — p99 checkout span duration < 500ms.
- Before: Logs only — MTTR 2 hours for cross-service timeout incidents.
- After: OTel traces — identify slow JDBC in inventory service in 5 minutes.
- Java agent rollout: Attach agent via ENV in Deployment — no code change first sprint.
- Cost control: 10% head sampling + collector tail sampling for status=ERROR.
Performance considerations
OTel performance impact on Java:
- Sampling: Head-based 10% sampling — 90% requests zero export overhead.
- Batch export: OTLP exporter batches spans — async export thread, minimal request path impact.
- Agent overhead: Typically <3% CPU — measure in load test before prod.
- Cardinality explosion: Don't put user_id on metric labels — unbounded series kills Prometheus.
- Span attributes: Limit high-cardinality attributes on spans — use logs for detail.
Security considerations
Observability security:
- PII in spans: Never attribute email, PAN, password — use opaque IDs only.
- OTLP TLS: Encrypt export to collector — mTLS in zero-trust environments.
- Trace data retention: Traces may contain URLs with tokens — scrub query params.
- RBAC on trace UI: Production traces visible to on-call only — not all engineers.
Scalability considerations
Scaling observability pipeline:
- Collector fan-out: App → regional collector → vendor backend — buffer and batch at scale.
- Tail sampling at collector: Keep errors and >1s latency — drop happy path bulk.
- Metrics aggregation: RED per service — don't export raw span as metric per request at scale.
- Log volume: Structured JSON logs with trace_id — ship to Loki/ELK with retention policy.
Production challenges
Real OTel adoption challenges:
- Broken propagation: Missing traceparent on Feign call — orphan spans; fix with agent or manual interceptor.
- Async gaps: @Async without context propagation — child work detached trace; use Context.taskWrapping.
- Dual instrumentation: Old Jaeger + new OTel — duplicate spans; migrate fully, one SDK.
- Sampling bugs: 0% sampling in prod — no traces; verify OTEL_TRACES_SAMPLER config.
- Clock skew: Cross-service span timestamps misordered — use NTP on nodes.
Common mistakes
- No context propagation across Kafka/async — traces fragment into useless single-service spans.
- 100% sampling in production — cost explosion and collector overload.
- High-cardinality metric labels (userId, orderId) — Prometheus OOM.
- PII in span attributes — compliance violation in trace storage.
- Ignoring logs correlation — traces without trace_id in logs — half the debugging value lost.
Debugging guide
Debug OpenTelemetry in Java services:
- Verify export:
curl http://otel-collector:4318/v1/traces— collector health; check app logs for OTel export errors. - Agent debug:
OTEL_JAVAAGENT_DEBUG=true— verbose agent startup (dev only). - Trace not appearing: Check sampling probability, firewall to collector, wrong endpoint port (4317 gRPC vs 4318 HTTP).
- Broken parent: Inspect incoming request for traceparent header — missing at gateway = root only.
# Enable OTel logging (dev)export OTEL_JAVAAGENT_DEBUG=trueexport OTEL_TRACES_EXPORTER=logging # console spans# Spring Boot — verify tracing activecurl -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \http://localhost:8080/actuator/health# Collector pipeline checkkubectl logs deployment/otel-collector -n observability
Best practices
- Adopt W3C tracecontext (traceparent) — standard propagation header.
- Start with Java agent auto-instrumentation — add manual spans for business operations only.
- Set OTEL_SERVICE_NAME per Deployment — clear service identity in traces.
- Use probabilistic sampling in prod (1–10%) + tail sampling for errors.
- Inject trace_id into structured logs — Micrometer MDC bridge.
- Propagate context through Kafka headers and @Async executors.
- Define SLOs from trace metrics — p99 latency, error rate per endpoint.
Anti-patterns
- Custom X-Request-Id only — no W3C propagation — incompatible with OTel ecosystem.
- Manual span for every method — noise; focus on boundaries and business ops.
- Exporting traces directly to vendor from every pod — bypass collector buffering.
- Storing full request body in span attributes — size limits and PII risk.
- Different trace IDs in logs and traces — failing to enable Micrometer-OTel MDC integration.
Staff engineer notes
- Staff engineers mandate trace propagation checklist in microservice PR template — HTTP, Kafka, async covered.
- Observability is not optional at 5+ services — OTel before optimizing performance you can't measure.
- Sampling is a product decision — 10% sample with error tail capture balances cost and debuggability.
- Metrics for alerting, traces for debugging, logs for detail — three pillars, one trace_id thread.
- OpenTelemetry Collector is the control point — switch Tempo to Jaeger without touching Java apps.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is OpenTelemetry?
BeginnerModel answer
- Vendor-neutral observability framework for traces, metrics, and logs. Unified SDK, OTLP export protocol, W3C context propagation. CNCF project
- successor to OpenTracing/OpenCensus merge.
Follow-up probe
vs proprietary agents?
2What is distributed tracing?
BeginnerModel answer
- Track request flow across multiple services as tree of spans sharing trace_id. Root span at entry (gateway), child spans for each downstream call. Waterfall shows latency breakdown
- find slow service.
Follow-up probe
Span vs trace?
3What is a span?
BeginnerModel answer
Single unit of work with operation name, start/end timestamp, attributes (key-value metadata), optional events.
Spans have parent reference forming tree.
Example: 'HTTP GET /orders' or 'JDBC SELECT'.
Follow-up probe
Span attributes?
4What is trace context propagation?
BeginnerModel answer
Pass trace_id and span_id across service boundaries via W3C traceparent HTTP header or message headers.
Downstream service extracts context, creates child span linked to parent.
Broken propagation orphans spans.
Follow-up probe
traceparent format?
5Traces vs metrics vs logs?
BeginnerModel answer
Traces: request path latency breakdown (debugging).
Metrics: aggregated counters/histograms over time (alerting).
Logs: discrete events with detail (audit).
OTel correlates via shared trace_id in logs.
Follow-up probe
RED method?
Intermediate
6How instrument Spring Boot with OTel?
IntermediateModel answer
Option 1: OpenTelemetry Java agent (-javaagent) auto-instruments Spring MVC, JDBC, Kafka.
Option 2: Micrometer tracing bridge-otel with Spring Boot 3 actuator.
Export OTLP to collector.
probability.
Follow-up probe
Agent vs SDK?
7What is OTLP?
IntermediateModel answer
- OpenTelemetry Protocol
- standard export format for traces, metrics, logs. gRPC (4317) or HTTP (4318) to OpenTelemetry Collector or vendor backend. One protocol replaces Jaeger thrift, Prometheus push, etc.
Follow-up probe
Collector role?
8Explain sampling strategies.
IntermediateModel answer
Head-based: decide at trace start (probabilistic 10%).
Tail-based: decide after complete (keep errors/slow).
ParentBased: respect upstream sampling decision.
Prod uses low head sampling + tail sampling at collector for errors.
Follow-up probe
100% sampling when?
9How propagate trace through Kafka?
IntermediateModel answer
Inject traceparent into ProducerRecord headers on send using TextMapPropagator.
Consumer extracts context, starts consumer span as child.
OTel Java agent auto-instruments Kafka client when enabled.
Follow-up probe
Async @Scheduled?
10What is OpenTelemetry Collector?
IntermediateModel answer
Vendor-agnostic pipeline: receive OTLP from apps, process (batch, filter, sample), export to Tempo/Jaeger/Prometheus/Datadog.
Decouples instrumentation from backend.
Scale collector independently.
Follow-up probe
Processor examples?
Advanced
11Design observability for 20 Java microservices.
AdvancedModel answer
OTel Java agent all services, OTEL_SERVICE_NAME per deployment, OTLP to regional collector, tail sample errors, Grafana Tempo traces + Prometheus RED metrics, Loki logs with trace_id, W3C propagation HTTP+Kafka, 10% head sampling, SLO dashboard p99 checkout < 500ms, alert on error rate.
Follow-up probe
Cost control?
12Debug 5s latency — tracing approach?
AdvancedModel answer
- Find trace for slow request in Tempo/Jaeger. Waterfall shows span breakdown
- e.g. payment HTTP 4.8s of 5s. Drill payment span attributes. Check JDBC child spans for slow query. Correlate trace_id logs in payment service at that timestamp.
Follow-up probe
No trace found?
13OTel vs Spring Cloud Sleuth?
AdvancedModel answer
- Sleuth deprecated
- Micrometer Tracing + OTel bridge is Spring Boot 3 path. OTel vendor-neutral, broader instrumentation, OTLP standard. Migrate Sleuth apps to micrometer-tracing-bridge-otel.
Follow-up probe
Brave vs OTel?
14Cardinality in metrics — pitfalls?
AdvancedModel answer
- Label values create time series
- userId label with millions values explodes Prometheus memory. Limit labels to service, endpoint, status. High-cardinality data in traces/logs with sampling, not metric labels.
Follow-up probe
Exemplars?
15Context propagation in virtual threads?
AdvancedModel answer
- OpenTelemetry Context attaches to thread
- virtual threads must propagate Context when task moves carriers. Java agent handles most cases. Manual: Context.current().wrap(runnable) for executor tasks. Test async paths explicitly.
Follow-up probe
ThreadLocal issue?
Hands-on exercise
Lab: OpenTelemetry concepts:
- Run playground — review trace hierarchy simulation.
- Write traceparent header format (version-traceId-spanId-flags).
- List 3 span attributes for payment charge operation.
- Explain head vs tail sampling trade-off for prod.
- Bonus: sketch OTLP flow app → collector → Tempo.
JavaOpenTelemetry for Java
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Java agent vs manual SDK: Agent wins speed; SDK wins control and smaller footprint.
- Head vs tail sampling: Head wins simplicity; tail wins keep-all-errors without 100% cost.
- OTLP to collector vs direct vendor: Collector wins flexibility; direct wins simplicity for small setups.
- 100% traces vs 10% sample: Full wins debuggability; sample wins cost at scale.
Summary
OpenTelemetry is the observability foundation for Java microservices — traces show where latency lives, metrics fire alerts, logs provide detail, all linked by trace_id. Deploy the Java agent, fix propagation across Kafka and async, and sample intelligently before production traffic grows. You now have the platform stack: microservices, Kafka, Redis, Docker, Kubernetes, and OTel.
Key takeaways
- OpenTelemetry — unified traces, metrics, logs with OTLP export.
- Distributed tracing — W3C traceparent propagation across HTTP and Kafka.
- Java agent — fastest Spring Boot instrumentation path.
- Sampling — probabilistic in prod; tail sample errors at collector.
- Correlate logs with trace_id — complete the debugging story.