Saltar al contenido
Mis Guidelines
Guías
En esta guía (12 elementos)

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-service placeholders with your project’s actual package and service name when applying patterns.

Companion files:


6. OpenTelemetry (OTEL) Tracing

How Micrometer Tracing Bridge Works

The project uses Micrometer Tracing Bridge to OpenTelemetry (micrometer-tracing-bridge-otel). This means:

  1. Micrometer’s ObservationRegistry creates observations (spans).
  2. The bridge translates Micrometer observations into OTEL spans.
  3. The OTEL exporter (opentelemetry-exporter-otlp) sends spans to an OTEL Collector.
  4. 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 @Scheduled instrumentation)

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

EnvironmentProbabilityRationale
local1.0Trace everything during development
staging1.0Full visibility for QA validation
production0.110% 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-01

Regex 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=otherValue

Propagation matrix

Micrometer Tracing automatically handles propagation when io.micrometer:micrometer-tracing-bridge-otel is on the classpath.

EdgeDirectionAuto-handled?Carrier
API Gateway → service (HTTP)inboundyes (extract); see api-gateway.md §13 for gateway-side behaviorTraceparent HTTP header
Service A → Service B (RestClient/WebClient)outboundyesTraceparent HTTP header
Service → Kafkaoutboundyes (Spring Kafka instruments KafkaTemplate.send)traceparent Kafka record header
Kafka → service consumerinboundyes (@KafkaListener extracts)traceparent Kafka record header
Service → RabbitMQoutboundyes when MessagePostProcessor configuredtraceparent in MessageProperties.headers
RabbitMQ → service consumerinboundyestraceparent in MessageProperties.headers
Service → SQSoutboundyes when AWS SDK instrumentedtraceparent in MessageAttributeValue
Async (@Async, CompletableFuture)in-processyes via Micrometer ContextSnapshotReactor / TaskDecorator
Manual handoff (scheduled job, batch, CLI)noneNO — 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: B3

B3 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:

EnvironmentProbabilityWhy
dev / local1.0full visibility
staging1.0full QA validation
production0.110% 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 Traceparent on 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 traceId into the EventMessage.Header.traceId field — survives DLQ redrive and broker-header loss.
  • Never log full Traceparent body — log only the traceId portion (indices 3..34 inclusive of the string).

MDC Correlation (trace-id, span-id in logs)

Micrometer Tracing automatically populates SLF4J MDC with:

MDC KeyDescription
traceIdW3C Trace ID (32 hex chars)
spanIdSpan 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):

// Kotlin
import io.micrometer.observation.Observation
import 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()
}
}
}
// Java
import 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

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:

Terminal window
export NEW_RELIC_LICENSE_KEY=your-license-key-here
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318
export 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:

RegionEndpoint
UShttps://otlp.nr-data.net:4318
EUhttps://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:

  1. Download the NewRelic Java Agent.
  2. Add to JVM startup flags:
Terminal window
java -javaagent:/path/to/newrelic.jar \
-Dnewrelic.config.app_name=platform-service \
-Dnewrelic.config.license_key=${NEW_RELIC_LICENSE_KEY} \
-jar platform-service.jar
  1. 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: true

When 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 Metric
WHERE metricName = 'usecase.operation'
AND entity = 'user'
AND operation = 'create_user'
SINCE 1 hour ago
TIMESERIES
-- Error rate by use case
SELECT count(*)
FROM Metric
WHERE metricName = 'usecase.operation.errors'
FACET entity, operation
SINCE 1 hour ago
TIMESERIES
-- Throughput by entity
SELECT rate(count(*), 1 minute)
FROM Metric
WHERE metricName = 'usecase.operation.calls'
FACET entity
SINCE 1 hour ago
TIMESERIES
-- Distributed trace search by trace-id
SELECT *
FROM Span
WHERE trace.id = 'your-trace-id-here'
SINCE 1 day ago
-- Slowest operations
SELECT max(duration)
FROM Span
WHERE service.name = 'platform-service'
FACET name
SINCE 1 hour ago
LIMIT 20