In this guide (12 items)
Observability — Tracing
Observability — Tracing
Project-agnostic. Part of the Observability suite. Covers: OpenTelemetry tracing, Micrometer-OTEL bridge, automatic + custom spans, propagation across services, MDC correlation, NewRelic OTEL exporter, NewRelic Java agent.
Replace
com.example.platform/platform-serviceplaceholders with your project’s actual package and service name when applying patterns.
Companion files:
- observability.md — index + three-pillars overview.
- obs-metrics.md — Micrometer + Prometheus + decorator metrics.
- obs-logging.md — structured logging with trace correlation.
- obs-recipes.md — step-by-step + dashboards + alerting + checklist.
6. OpenTelemetry (OTEL) Tracing
How Micrometer Tracing Bridge Works
The project uses Micrometer Tracing Bridge to OpenTelemetry (micrometer-tracing-bridge-otel). This means:
- Micrometer’s
ObservationRegistrycreates observations (spans). - The bridge translates Micrometer observations into OTEL spans.
- The OTEL exporter (
opentelemetry-exporter-otlp) sends spans to an OTEL Collector. - Spring Boot auto-configures everything — no manual span creation needed for HTTP, JDBC, Kafka.
What Is Automatically Traced
Spring Boot with Micrometer Tracing auto-instruments:
- HTTP requests (inbound via Spring MVC, outbound via RestClient/WebClient)
- JDBC queries (via DataSource proxy)
- Kafka produce/consume (via Spring Kafka instrumentation)
- Redis operations (via Spring Data Redis)
- Scheduled tasks (via
@Scheduledinstrumentation)
No additional code is needed for these. The tracing bridge handles it transparently.
OTEL Collector Configuration
Add this to application.yml (bootstrap module):
management: tracing: enabled: true sampling: probability: ${MANAGEMENT_TRACING_SAMPLING_PROBABILITY:1.0} otlp: tracing: endpoint: ${MANAGEMENT_OTLP_TRACING_ENDPOINT:http://localhost:4318/v1/traces}management.otlp.tracing.endpoint is the canonical Spring Boot 3.x property bound by the
micrometer-tracing-bridge-otel autoconfiguration (default http://localhost:4318/v1/traces).
spring.application.name is exported as the service.name resource attribute automatically — do
NOT use the OTEL-SDK otel.exporter.otlp.* / otel.resource.attributes properties here, as they
are read by the OTEL SDK autoconfigure path, not by the Spring Boot bridge.
Sampling Configuration
| Environment | Probability | Rationale |
|---|---|---|
| local | 1.0 | Trace everything during development |
| staging | 1.0 | Full visibility for QA validation |
| production | 0.1 | 10% sampling to reduce overhead and cost |
Override via environment variable: MANAGEMENT_TRACING_SAMPLING_PROBABILITY=0.1
W3C Trace Context — Canonical Reference
The W3C Trace Context spec (w3.org/TR/trace-context/) defines two HTTP headers — traceparent and tracestate — that propagate a distributed trace across services, brokers, and gateways. Spring Boot 3+ (Micrometer Tracing) defaults to W3C; B3 (Zipkin legacy) remains available for backward compatibility.
traceparent format
traceparent: 00-<trace-id>-<parent-id>-<flags> ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | 32 hex chars (16 bytes) — trace-id, identifies the whole trace | 16 hex chars (8 bytes) — parent-id (span-id of the caller) | 02 hex chars (1 byte) — flags, e.g. "01" = sampled 02 hex chars version, currently always "00"Example:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01Regex for validation (use exactly this — KrakenD reference plugin uses the same):
^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$tracestate
Vendor-specific extensions, opaque to most services. Propagate as-is; never parse or reorder. Example:
tracestate: vendorA=opaqueValue,vendorB=otherValuePropagation matrix
Micrometer Tracing automatically handles propagation when io.micrometer:micrometer-tracing-bridge-otel is on the classpath.
| Edge | Direction | Auto-handled? | Carrier |
|---|---|---|---|
| API Gateway → service (HTTP) | inbound | yes (extract); see api-gateway.md §13 for gateway-side behavior | Traceparent HTTP header |
| Service A → Service B (RestClient/WebClient) | outbound | yes | Traceparent HTTP header |
| Service → Kafka | outbound | yes (Spring Kafka instruments KafkaTemplate.send) | traceparent Kafka record header |
| Kafka → service consumer | inbound | yes (@KafkaListener extracts) | traceparent Kafka record header |
| Service → RabbitMQ | outbound | yes when MessagePostProcessor configured | traceparent in MessageProperties.headers |
| RabbitMQ → service consumer | inbound | yes | traceparent in MessageProperties.headers |
| Service → SQS | outbound | yes when AWS SDK instrumented | traceparent in MessageAttributeValue |
Async (@Async, CompletableFuture) | in-process | yes via Micrometer ContextSnapshot | Reactor / TaskDecorator |
| Manual handoff (scheduled job, batch, CLI) | none | NO — propagate manually (see below) | — |
management: tracing: propagation: type: W3C # default in Spring Boot 3+; alternative: B3 (Zipkin legacy)Manual propagation (when auto-handling is unavailable)
For scheduled jobs, batch entry points, CLI tools, or when starting a span from an external trigger:
@Autowired Tracer tracer;@Autowired Propagator propagator;
void runJob(String externalTraceparent) { Propagator.Getter<String> getter = (carrier, key) -> "traceparent".equalsIgnoreCase(key) ? carrier : null; Span span = propagator.extract(externalTraceparent, getter).start(); try (Tracer.SpanInScope ws = tracer.withSpan(span)) { // work — outbound calls within this scope are auto-propagated } finally { span.end(); }}MDC integration
Micrometer auto-populates MDC with traceId and spanId once the W3C context is extracted:
[%X{traceId:-},%X{spanId:-}]See the MDC Correlation section below for the full Logback pattern.
B3 fallback (Zipkin legacy)
When integrating with a Zipkin ecosystem:
management: tracing: propagation: type: B3B3 uses different headers (X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled). Coexistence with W3C is configurable but adds complexity — prefer W3C-only unless integrating with legacy Zipkin services.
Sampling head vs tail
- Head-based (default): the entry point (gateway or service) decides at trace start; the flag in
traceparent(01= sampled,00= not) carries the decision to all downstream services. - Tail-based: the OTEL collector samples after collecting the full trace, based on duration / error / attributes. Requires more storage; useful for catching error traces while keeping volume down.
Configure sampling probability per env:
| Environment | Probability | Why |
|---|---|---|
| dev / local | 1.0 | full visibility |
| staging | 1.0 | full QA validation |
| production | 0.1 | 10% sampling to cap volume + cost |
Override via env var: MANAGEMENT_TRACING_SAMPLING_PROBABILITY=0.1.
Gateway propagation contract
The API gateway is the first hop where W3C context is created or accepted. See api-gateway.md §13 for the canonical gateway-side behavior — including the legacy Trace-Id fallback that bridges older clients to W3C.
Hard rules
- Services MUST extract
Traceparenton every inbound request. If absent — log a warning and continue (do not fail). Generate a new trace. - Outbound calls MUST carry
Traceparent. Auto-propagation is non-negotiable; if it breaks, treat as a P2 bug. - Logs MUST include
traceId(via MDC) on every log line — see obs-logging.md. - Business-level correlation: services that publish events via the transactional outbox ALSO stamp
traceIdinto theEventMessage.Header.traceIdfield — survives DLQ redrive and broker-header loss. - Never log full
Traceparentbody — log only thetraceIdportion (indices 3..34 inclusive of the string).
MDC Correlation (trace-id, span-id in logs)
Micrometer Tracing automatically populates SLF4J MDC with:
| MDC Key | Description |
|---|---|
traceId | W3C Trace ID (32 hex chars) |
spanId | Span ID (16 hex chars) |
This project already uses these in the logging pattern (see application.yml):
logging: pattern: console: "%d{HH:mm:ss.SSS} [%thread] [trace-id=%X{traceId}] %-5level %logger{5} - %msg%n" file: "%d{HH:mm:ss.SSS} [%thread] [trace-id=%X{traceId}] %-5level %logger{5} - %msg%n"This means every log line includes the trace-id, allowing you to correlate logs with traces in your observability backend.
Custom Spans (when needed)
For rare cases where you need a custom span (avoid unless necessary):
// Kotlinimport io.micrometer.observation.Observationimport io.micrometer.observation.ObservationRegistry
class SomeAdapter(private val observationRegistry: ObservationRegistry) { fun someOperation() { val observation = Observation.createNotStarted("custom.operation", observationRegistry) .lowCardinalityKeyValue("entity", "user") .start() try { // ... operation logic } catch (ex: Exception) { observation.error(ex) throw ex } finally { observation.stop() } }}// Javaimport io.micrometer.observation.Observation;import io.micrometer.observation.ObservationRegistry;
public class SomeAdapter { private final ObservationRegistry observationRegistry;
public SomeAdapter(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; }
public void someOperation() { Observation observation = Observation.createNotStarted("custom.operation", observationRegistry) .lowCardinalityKeyValue("entity", "user") .start(); try { // ... operation logic } catch (Exception ex) { observation.error(ex); throw ex; } finally { observation.stop(); } }}7. NewRelic with OTEL
Option A: NewRelic OTEL Exporter (Recommended)
NewRelic natively supports OTLP ingestion. This is the simplest approach — just point the OTEL exporter to NewRelic’s OTLP endpoint.
management: otlp: tracing: endpoint: ${MANAGEMENT_OTLP_TRACING_ENDPOINT:https://otlp.nr-data.net:4318/v1/traces} headers: api-key: ${NEW_RELIC_LICENSE_KEY}management.otlp.tracing.endpoint and management.otlp.tracing.headers.* are the Spring Boot 3.x
properties bound by the micrometer-tracing-bridge-otel bridge; spring.application.name is
exported as service.name automatically. The OTEL-SDK otel.exporter.otlp.* properties are NOT
read on this path.
Environment variables for production:
export NEW_RELIC_LICENSE_KEY=your-license-key-hereexport OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318export OTEL_EXPORTER_OTLP_HEADERS="api-key=${NEW_RELIC_LICENSE_KEY}"Advantages:
- No additional agent or sidecar needed.
- Uses the same OTEL exporter already in the project.
- Traces, metrics, and logs all flow through a single pipeline.
- Full compatibility with NewRelic APM, distributed tracing, and alerting.
NewRelic Endpoints by Region:
| Region | Endpoint |
|---|---|
| US | https://otlp.nr-data.net:4318 |
| EU | https://otlp.eu01.nr-data.net:4318 |
Option B: NewRelic Java Agent + OTEL (Alternative)
If you need full APM features (thread profiling, JVM metrics, code-level visibility), use the NewRelic Java Agent alongside OTEL:
- Download the NewRelic Java Agent.
- Add to JVM startup flags:
java -javaagent:/path/to/newrelic.jar \ -Dnewrelic.config.app_name=platform-service \ -Dnewrelic.config.license_key=${NEW_RELIC_LICENSE_KEY} \ -jar platform-service.jar- Configure
newrelic.yml:
common: &default_settings app_name: platform-service license_key: ${NEW_RELIC_LICENSE_KEY} distributed_tracing: enabled: true application_logging: enabled: true forwarding: enabled: true max_samples_stored: 10000 metrics: enabled: true local_decorating: enabled: trueWhen to choose Option B:
- You need thread-level profiling and JVM internals.
- You want NewRelic’s auto-instrumentation for libraries not covered by Micrometer.
- Your organization already uses the NewRelic agent across other services.
NRQL Query Examples
Query custom metrics from this project in NewRelic:
-- Use case latency percentiles (p50, p95, p99)SELECT percentile(duration, 50, 95, 99)FROM MetricWHERE metricName = 'usecase.operation' AND entity = 'user' AND operation = 'create_user'SINCE 1 hour agoTIMESERIES
-- Error rate by use caseSELECT count(*)FROM MetricWHERE metricName = 'usecase.operation.errors'FACET entity, operationSINCE 1 hour agoTIMESERIES
-- Throughput by entitySELECT rate(count(*), 1 minute)FROM MetricWHERE metricName = 'usecase.operation.calls'FACET entitySINCE 1 hour agoTIMESERIES
-- Distributed trace search by trace-idSELECT *FROM SpanWHERE trace.id = 'your-trace-id-here'SINCE 1 day ago
-- Slowest operationsSELECT max(duration)FROM SpanWHERE service.name = 'platform-service'FACET nameSINCE 1 hour agoLIMIT 20