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

50 min

Lesson 2 of 3

1d streak

The 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 PillarCore Question AnsweredTypical Storage EngineExample Artifact
MetricsIs the system healthy? Where is the aggregate bottleneck?Prometheus, InfluxDB, CloudWatchp95 latency graph, HTTP error rate (req/sec), heap usage
TracesWhich specific hop or database query caused latency in this request?Tempo, Jaeger, ZipkinWaterfall timeline showing controller -> auth -> SQL -> Redis
LogsWhat exact contextual error occurred at the point of failure?Elasticsearch, Loki, OpenSearchJSON record with stack trace, customer ID, and input parameters

The W3C TraceContext Bridge

The W3C TraceContext standard defines the 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.

OrderMetrics.java
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

Never rely on client-side percentiles calculated by the JVM (e.g. 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.

pom.xml
<!-- 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>
application.yml
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.

AsyncConfig.java
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

Because 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.

DistributedTracingMetricsLab.java
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();
        });
    }
}
Timer.builder with publishPercentileHistogram() emits Prometheus buckets for histogram_quantile PromQL queries.
Micrometer Tracing automatically injects W3C traceId into SLF4J MDC, correlating the timer with log entries.
Avoid high-cardinality tags (like cartId or userId); keep metric tags bounded to low-cardinality values.

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

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

Supplementary Module Resources