Intermediate · Observe
Correlate Logs, Metrics & Traces
Instrument one request so an alert can lead to a trace and then to structured diagnostic logs.
Primary source: Spring Boot Actuator: Production-ready Features
Lesson 2 of 3
1d streakThe Architectural Question
Alerts page someone with a metric; without correlated traces and structured logs, every diagnosis restarts from zero.
What this guide proves
- Use stable low-cardinality metric tags
- Propagate observation context across async boundaries
- Log trace identifiers without logging credentials or payload secrets
The Three Pillars: Correlating Logs, Metrics, and Distributed Traces
Observability is not just having monitoring tools; it is the ability to understand the internal state of a complex system based on its external outputs. In distributed microservices, effective diagnosis requires unifying all three pillars.
| Observability Pillar | Core Question Answered | Typical Storage Engine | Example Artifact |
|---|---|---|---|
| Metrics | Is the system healthy? Where is the aggregate bottleneck? | Prometheus, InfluxDB, CloudWatch | p95 latency graph, HTTP error rate (req/sec), heap usage |
| Traces | Which specific hop or database query caused latency in this request? | Tempo, Jaeger, Zipkin | Waterfall timeline showing controller -> auth -> SQL -> Redis |
| Logs | What exact contextual error occurred at the point of failure? | Elasticsearch, Loki, OpenSearch | JSON record with stack trace, customer ID, and input parameters |
The W3C TraceContext Bridge
traceparent HTTP header: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01. The traceId (4bf9...) stays constant across every microservice hop, allowing you to click a point on a Grafana metrics graph, jump to the exact trace in Tempo, and filter logs in Loki with that single trace ID.Active Recall Checkpoint
What links an aggregate Prometheus metric spike to an individual log error in an integrated observability pipeline?
Hint: What common identifier is attached to both span exemplars and log MDC?
Show answer & explanation
The traceId. Prometheus Exemplars attach trace IDs directly to histogram metric samples, linking high-latency metrics directly to distributed trace spans and correlated log lines.
Micrometer Instrumentation: Counters, Timers & Gauges
Micrometer is the dimensional metrics instrumentation facade included in Spring Boot (analogous to SLF4J for logging). It supports Prometheus, Datadog, New Relic, and CloudWatch with a vendor-neutral API.
package com.example.order;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class OrderMetrics {
private final Counter orderSuccessCounter;
private final Counter orderFailureCounter;
private final Timer orderProcessingTimer;
public OrderMetrics(MeterRegistry registry) {
this.orderSuccessCounter = Counter.builder("app.orders.completed")
.description("Total number of successfully placed orders")
.tag("status", "success")
.register(registry);
this.orderFailureCounter = Counter.builder("app.orders.completed")
.description("Total number of failed orders")
.tag("status", "failure")
.register(registry);
this.orderProcessingTimer = Timer.builder("app.orders.processing.time")
.description("Latency of the complete order processing pipeline")
.publishPercentileHistogram() // Enables Prometheus histogram_quantile calculations!
.sla(Duration.ofMillis(200), Duration.ofMillis(500), Duration.ofSeconds(1))
.register(registry);
}
public void recordSuccess(Duration duration) {
orderSuccessCounter.increment();
orderProcessingTimer.record(duration);
}
public void recordFailure() {
orderFailureCounter.increment();
}
}Why publishPercentileHistogram() is Mandatory for Accurate p99s
publishPercentiles(0.95, 0.99)), because percentiles cannot be aggregated across multiple horizontal pods. publishPercentileHistogram() emits raw bucket counts, allowing Prometheus to calculate true cluster-wide percentiles with histogram_quantile(0.95, sum(rate(...))).Active Recall Checkpoint
Why is a Gauge NOT suitable for counting incoming HTTP requests or completed orders?
Hint: What does a Gauge measure compared to a Counter?
Show answer & explanation
A Gauge measures a momentary value that can go up and down (like active database connections or queue size). For events that continuously accumulate (requests, completed orders), a Counter must be used so Prometheus can calculate rates over time using rate().
Distributed Tracing with Micrometer Tracing & OpenTelemetry
In Spring Boot 3+, the legacy Spring Cloud Sleuth project was retired and completely replaced by Micrometer Tracing. Adding micrometer-tracing-bridge-otel or micrometer-tracing-bridge-brave enables automatic trace propagation for Spring MVC controllers, RestClient, WebClient, and Spring Data queries.
<!-- Micrometer Tracing Bridge with OpenTelemetry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- Export traces to OpenTelemetry Collector / Tempo / Jaeger -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>management:
tracing:
sampling:
probability: 1.0 # Sample 100% in dev/staging; use 0.05 (5%) or 0.1 (10%) in high-traffic prod
enabled: true
otlp:
tracing:
endpoint: "http://otel-collector:4318/v1/traces"Active Recall Checkpoint
What sampling probability should you configure in a production system handling 20,000 requests per second?
Hint: Would recording full traces for all 20,000 requests saturate network bandwidth and storage?
Show answer & explanation
Use probabilistic or adaptive sampling (e.g. probability: 0.01 to 0.05 for 1%–5% sampling), or configure tail-based sampling in the OpenTelemetry Collector to capture 100% of errors and slow requests while sampling successful fast requests.
Deliberate Failure Lab: Trace Context Loss in Asynchronous Threads
A frequent operational headache occurs when a request delegates work to a @Async method, a CompletableFuture.supplyAsync(), or an event listener: the subsequent log lines suddenly have blank traceId and spanId fields.
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(25);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-worker-");
// CRITICAL: Propagate MDC and Tracing Context across thread boundaries:
executor.setTaskDecorator(runnable -> {
var contextMap = org.slf4j.MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
org.slf4j.MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
org.slf4j.MDC.clear();
}
};
});
executor.initialize();
return executor;
}
}The TaskDecorator Solution
ThreadLocal variables are bound to the calling thread, pooled worker threads do not inherit them. Configuring ThreadPoolTaskExecutor.setTaskDecorator(...) captures the caller's MDC context and installs it onto the worker thread for the duration of the execution.Active Recall Checkpoint
Why does @Async without a TaskDecorator break trace correlation in logs?
Hint: Where is the traceId stored for SLF4J logging?
Show answer & explanation
SLF4J stores the traceId in MDC, which relies on ThreadLocal variables. When an asynchronous thread pool executes the task, it runs on a separate worker thread that lacks the caller's ThreadLocal context unless explicitly copied.
Runtime Architecture & Flow
Observability · runtime visualization
Correlate Logs, Metrics & Traces
Request
Use stable low-cardinality metric tags
Logs
Propagate observation context across async boundaries
Metrics
Log trace identifiers without logging credentials or payload secrets
Traces
Measure checkout latency and follow one slow trace to its downstream span.
Working Implementation
Code Walkthrough
Measure checkout latency and follow one slow trace to its downstream span.
package com.example.observability;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
public class DistributedTracingMetricsLab {
private static final Logger log = LoggerFactory.getLogger(DistributedTracingMetricsLab.class);
private final Timer checkoutTimer;
public DistributedTracingMetricsLab(MeterRegistry registry) {
this.checkoutTimer = Timer.builder("app.checkout.duration")
.description("Latency distribution of checkout operations")
.publishPercentileHistogram()
.sla(Duration.ofMillis(100), Duration.ofMillis(300), Duration.ofMillis(500))
.register(registry);
}
public String executeCheckout(String cartId) {
return checkoutTimer.record(() -> {
log.info("Processing checkout for cartId={}", cartId);
// Simulated processing
return "ORDER-" + System.currentTimeMillis();
});
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Record a timer with low-cardinality tags only
- Propagate observation context across one async boundary
- Emit a log line carrying trace identifiers without credentials or payloads
Key Takeaway
Correlating the three pillars of observability via unified W3C traceparent headers connects aggregate Prometheus metric spikes directly to Tempo trace spans and Loki log records.
Architectural & Technical Review
Core Interview Questions
Q01How do metrics, traces, and logs complement each other in an observability pipeline?Show answer
Metrics detect that a problem exists in aggregate; traces isolate which service or query caused the latency; logs provide detailed error messages and context at the point of failure.
Q02Why is publishPercentileHistogram() preferred over client-side percentiles in Micrometer?Show answer
Client-side percentiles cannot be aggregated across multiple horizontal pods. Publishing bucket histograms allows Prometheus to compute true cluster-wide percentiles via histogram_quantile.
Q03How does Micrometer Tracing bridge trace context into SLF4J logging?Show answer
It extracts the W3C traceparent header and injects traceId and spanId directly into the SLF4J MDC, ensuring every log line includes trace coordinates.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Calculating client-side percentiles instead of publishing histograms — Precomputed percentiles on individual JVMs cannot be mathematically aggregated across horizontally scaled instances.
- Pitfall: Losing trace context across @Async thread boundaries without TaskDecorator — ThreadLocal MDC values do not transfer to asynchronous worker pools automatically without configuring a TaskDecorator.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources