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 |
|---|---|
|
Monotonically increasing count (requests served, orders created). |
|
Count + total duration + distribution of an operation’s latency. |
|
A value that can go up or down (queue size, active connections), sampled on read. |
|
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.
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-actuatorplusmicrometer-registry-prometheusgives every Spring Boot app a/actuator/prometheusendpoint with no code beyond declaring domain-specificCounter/Timer/Gaugemeters. -
Micrometer Tracing plus the OpenTelemetry bridge auto-configures the
OpenTelemetryandSdkTracerProviderbeans, 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.