Metrics and Observability: Micrometer, Actuator, and Distributed Tracing

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

Observability covers three related signals: metrics (numeric time series), traces (the causal path of a request across services), and logs. Spring Boot’s Actuator exposes operational endpoints backed by Micrometer, the vendor-neutral metrics facade, and pairs with Micrometer Tracing for distributed tracing via OpenTelemetry. This page walks through instrumenting a service with both signals and wiring them into Prometheus and Grafana.

Micrometer as the metrics facade

Micrometer is to metrics what SLF4J is to logging: application code depends only on Micrometer’s API (MeterRegistry, Counter, Timer, Gauge), and a registry implementation forwards those measurements to a concrete backend (Prometheus, Datadog, CloudWatch, and others) without the instrumented code knowing which one is active. Adding spring-boot-starter-actuator together with the micrometer-registry-prometheus dependency auto-configures a PrometheusMeterRegistry bean.

@Service
public class OrderService {

    private final Counter ordersCreated;
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry registry) {
        this.ordersCreated = Counter.builder("orders.created")
                .description("Number of orders created")
                .tag("channel", "web")
                .register(registry);
        this.orderProcessingTimer = Timer.builder("orders.processing.time")
                .description("Time spent processing an order")
                .register(registry);
    }

    public Order create(OrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = new Order(request);
            ordersCreated.increment();
            return order;
        });
    }
}

@Timed on a method (with @EnablePrometheusMetrics-style AOP support enabled) achieves the same result declaratively for simple cases. Every meter carries a name plus tags (dimensions), which Prometheus surfaces as labels — keep tag cardinality bounded (never tag with a raw user ID or request ID) or the resulting metric series will explode. See the Micrometer reference documentation.

Common meter types

Meter Use

Counter

Monotonically increasing count (requests served, orders created).

Timer

Count + total duration + distribution of an operation’s latency.

Gauge

A value that can go up or down (queue size, active connections), sampled on read.

DistributionSummary

Distribution of a non-time value (payload size in bytes).

Spring Boot Actuator endpoints

Actuator exposes a family of HTTP endpoints under /actuator for health, metrics, environment, and configuration introspection once spring-boot-starter-actuator is on the classpath. Endpoints are enabled by default but, apart from health, not exposed over HTTP until explicitly listed:

management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    tags:
      application: order-service

With that configuration, /actuator/health reports liveness/readiness (aggregating each HealthIndicator, such as a database or message-broker connection check), /actuator/metrics/{name} inspects one Micrometer meter interactively (e.g. /actuator/metrics/orders.created), and /actuator/info surfaces build metadata. Never expose the full endpoint set (include: "*") on a public network without also locking Actuator behind authentication — endpoints like env and beans reveal configuration and, potentially, secrets. See the Spring Boot Actuator observability reference.

Exposing /actuator/prometheus

The prometheus endpoint renders every registered Micrometer meter in the Prometheus text exposition format, ready to be scraped:

management:
  endpoints:
    web:
      exposure:
        include: health, prometheus
  prometheus:
    metrics:
      export:
        enabled: true

A GET /actuator/prometheus then returns plain text such as:

# HELP orders_created_total Number of orders created
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web",} 42.0
# HELP orders_processing_time_seconds Time spent processing an order
# TYPE orders_processing_time_seconds summary
orders_processing_time_seconds_count{application="order-service",} 42.0
orders_processing_time_seconds_sum{application="order-service",} 3.87

Note the _total and _seconds suffixes and the base-unit conversion (Micrometer records in seconds regardless of the unit used in code) — both are conventions the Prometheus registry applies automatically so dashboards and alerting rules stay portable across languages and frameworks.

Micrometer Tracing and OpenTelemetry

Micrometer Tracing is the tracing counterpart of the metrics facade: application and library code calls a small, stable API (Tracer, Span, ObservationRegistry), and a bridge forwards spans to a concrete tracer implementation. The OpenTelemetry bridge (micrometer-tracing-bridge-otel) wires that API to the OpenTelemetry Java SDK, so Spring Boot auto-configures the underlying OpenTelemetry and SdkTracerProvider beans once the bridge and an OTLP exporter dependency are present.

management:
  tracing:
    sampling:
      probability: 1.0        # trace every request; lower this in production
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
spring:
  application:
    name: order-service

spring.application.name becomes the service.name resource attribute that identifies this process’s spans in the tracing backend. With micrometer-tracing-bridge-otel and opentelemetry-exporter-otlp on the classpath, Spring Boot exports the OpenTelemetry bean (the SDK entry point) and its SdkTracerProvider (which batches and exports finished spans) without any Java configuration; a custom SdkTracerProvider can still be supplied to add extra span processors or resource attributes:

@Configuration
public class TracingConfig {

    @Bean
    public SdkTracerProviderBuilderCustomizer otelResourceCustomizer() {
        return builder -> builder.setResource(
                Resource.getDefault().merge(Resource.create(
                        Attributes.of(
                                AttributeKey.stringKey("deployment.environment"), "production"))));
    }
}

Application code rarely creates spans directly — Spring MVC, WebClient, RestTemplate, and JDBC are auto-instrumented — but a manual span is created through the Micrometer Tracer when a custom unit of work needs its own span:

@Service
public class PaymentService {

    private final Tracer tracer;

    public PaymentService(Tracer tracer) {
        this.tracer = tracer;
    }

    public void charge(Order order) {
        Span span = tracer.nextSpan().name("payment.charge").start();
        try (Tracer.SpanInScope ignored = tracer.withSpan(span)) {
            span.tag("order.id", order.getId());
            // charge logic
        } finally {
            span.end();
        }
    }
}

See Spring Boot’s observability reference for the Micrometer Observation/Tracing integration, and the OpenTelemetry Java documentation for the SDK, exporters, and the OTLP protocol itself.

OTLP export configuration

The OTLP (OpenTelemetry Protocol) exporter ships spans (and, separately, metrics) to a collector over HTTP or gRPC. A minimal collector-bound configuration:

management:
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
      timeout: 10s
      compression: gzip
    metrics:
      export:
        url: http://otel-collector:4318/v1/metrics
        step: 30s

In development, exporting straight to a local Jaeger or Zipkin instance that accepts OTLP avoids running a full collector; in production, routing through an OpenTelemetry Collector lets sampling, batching, and fan-out to multiple backends be tuned without redeploying every service.

Wiring Prometheus and Grafana end to end

With /actuator/prometheus exposed, Prometheus itself needs a scrape config naming the target, and Grafana needs a data source pointing at that Prometheus server.

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'order-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['order-service:8080']

Prometheus polls http://order-service:8080/actuator/prometheus every 15 seconds and stores each sample as a time series labelled by the metric’s tags plus the job and instance labels it adds itself. See the Prometheus documentation for scrape_configs, relabeling, and service discovery beyond the static list shown here.

Grafana then queries that Prometheus server as a data source, either through its UI or via a provisioning file checked into version control:

# grafana/provisioning/datasources/prometheus.yml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    jsonData:
      timeInterval: 15s

From there, a dashboard panel built on a PromQL query such as rate(orders_created_total[5m]) or histogram_quantile(0.95, rate(orders_processing_time_seconds_bucket[5m])) visualizes throughput and latency percentiles for the service. See the Grafana documentation for dashboard JSON models, panel types, and alerting rules built on top of a Prometheus data source.

Distributed tracing across services

A trace is a single logical request represented as a tree of spans; when service A calls service B over HTTP, the trace context (a trace ID and the calling span’s ID) travels in request headers (traceparent under the W3C Trace Context standard) so service B’s spans attach to the same trace instead of starting a new one. Spring Boot’s auto-instrumented RestTemplate/WebClient and inbound MVC filters handle this propagation automatically once Micrometer Tracing is on the classpath in both services.

sequenceDiagram participant C as Client participant A as Service A participant OA as OTel SDK (A) participant B as Service B participant OB as OTel SDK (B) participant Col as OTel Collector C->>A: POST /orders A->>OA: start span "POST /orders" (new trace ID) A->>B: POST /payments (header: traceparent=-) B->>OB: start span "POST /payments" (same trace ID, parent = A's span) OB-->>B: span context active B-->>A: 200 OK OB->>Col: export span (async, batched) A-->>C: 201 Created OA->>Col: export span (async, batched)

Because both spans share the same trace ID, the tracing backend (Jaeger, Tempo, or any OTLP-compatible store queried from Grafana) can render the full request path across service A and service B as one waterfall, even though each service exported its span independently and asynchronously. Combining this with the metrics from /actuator/prometheus — for example correlating a latency spike in orders.processing.time with the traces recorded during the same window — is the core workflow that makes Micrometer plus OpenTelemetry more useful together than either signal alone.

Summary

  • Micrometer is the metrics facade; spring-boot-starter-actuator plus micrometer-registry-prometheus gives every Spring Boot app a /actuator/prometheus endpoint with no code beyond declaring domain-specific Counter/Timer/Gauge meters.

  • Micrometer Tracing plus the OpenTelemetry bridge auto-configures the OpenTelemetry and SdkTracerProvider beans, exporting spans over OTLP to a collector or tracing backend.

  • Trace context propagates across service boundaries in request headers, letting spans from independently deployed services join into one distributed trace.

  • Prometheus scrapes the exposed endpoint on an interval; Grafana queries Prometheus as a data source to build dashboards and alerts on top of the same metrics.