StackWise
Spring Boot 4Production & Microservices

Actuator, Metrics & Tracing

Health checks and Kubernetes probes, Micrometer metrics, distributed tracing with OpenTelemetry, structured logging and graceful shutdown.

intermediate 3 min readLesson 18 of 23

Observability = logs + metrics + traces

Signal Answers Spring Boot tooling
Metrics How much / how fast? Request rate, latency percentiles, error rate, pool usage Micrometer → Prometheus, OTLP, Datadog…
Traces Where did the time go across services? Micrometer Tracing / OpenTelemetry
Logs What exactly happened? Logback/Log4j2, structured JSON, trace ids
Health Can this instance serve traffic? Actuator health groups, Kubernetes probes

Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: health, info, metrics, prometheus
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true                  # /actuator/health/liveness and /readiness
  server:
    port: 8081                         # separate port, not exposed publicly
Endpoint Purpose
/actuator/health Aggregated health of DB, disk, brokers, custom indicators
/actuator/health/liveness, /readiness Kubernetes probes
/actuator/metrics, /actuator/prometheus Metrics
/actuator/info Build, git and custom info
/actuator/env, /configprops, /beans, /conditions, /mappings Diagnostics — secure them!
/actuator/loggers View and change log levels at runtime
/actuator/threaddump, /heapdump JVM diagnostics

Liveness vs readiness

  • Liveness — is the process healthy, or should Kubernetes restart it? Don’t include external dependencies here: a database outage shouldn’t restart every pod.
  • Readiness — can it receive traffic right now? Fails during startup, shutdown or when a critical dependency is unavailable; Kubernetes stops routing to it.

Custom health indicator

@Component
class PaymentGatewayHealth implements HealthIndicator {
    private final PaymentClient client;
    PaymentGatewayHealth(PaymentClient client) { this.client = client; }

    @Override
    public Health health() {
        return client.ping()
            ? Health.up().withDetail("latencyMs", client.lastLatency().toMillis()).build()
            : Health.down().withDetail("reason", "ping failed").build();
    }
}

Metrics with Micrometer

Boot auto-instruments HTTP server and client requests, JVM memory and GC, thread pools, HikariCP, caches, Kafka, and more. Add business metrics yourself:

@Service
class CheckoutService {
    private final Counter ordersPlaced;
    private final Timer paymentTimer;

    CheckoutService(MeterRegistry registry) {
        this.ordersPlaced = Counter.builder("shop.orders.placed").description("Orders placed").register(registry);
        this.paymentTimer = Timer.builder("shop.payment.duration").publishPercentileHistogram().register(registry);
    }

    void checkout(Cart cart) {
        paymentTimer.record(() -> payments.charge(cart));
        ordersPlaced.increment();
    }
}

Or declaratively with the Observation API, which produces a metric and a trace span from one annotation:

@Observed(name = "shop.pricing.quote", contextualName = "price-quote")
public Quote quote(Cart cart) { ... }

Distributed tracing with OpenTelemetry

A trace follows one request across services; each hop is a span. Context propagates via the W3C traceparent header, which Boot adds to outgoing RestClient, WebClient and Kafka calls automatically.

gantt
  dateFormat X
  axisFormat %Lms
  title Trace 4bf92f35… POST /api/orders
  section gateway
  POST /api/orders         :0, 420
  section order-service
  OrderController.create   :20, 400
  SELECT customer          :30, 45
  INSERT orders            :60, 80
  section inventory-service
  POST /reservations       :90, 290
  UPDATE stock             :110, 260
  section payment-service
  POST /charges            :300, 395

Spring Boot 4 provides a dedicated OpenTelemetry starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
management:
  tracing:
    sampling:
      probability: 0.1            # sample 10% in production
  opentelemetry:
    tracing:
      export:
        otlp:
          endpoint: http://otel-collector:4318/v1/traces

Send traces to an OpenTelemetry Collector, then Grafana Tempo, Jaeger, Zipkin or a vendor.

Structured logging

Spring Boot can write JSON logs without extra libraries:

logging.structured.format.console=ecs        # or logstash, gelf

Each line then includes the timestamp, level, logger, message, traceId and spanId — so you can jump from a trace to its logs. Add context with MDC or the fluent SLF4J API: log.atInfo().addKeyValue("orderId", id).log("Order placed").

Graceful shutdown

server.shutdown=graceful                          # default since Boot 3.4
spring.lifecycle.timeout-per-shutdown-phase=30s

On SIGTERM the server stops accepting new requests, readiness flips to OUT_OF_SERVICE, in-flight requests finish, then the context closes. Pair it with a Kubernetes preStop sleep of a few seconds so load balancers deregister the pod first.

Finished this lesson?

Mark it complete to track your progress through Spring Boot 4.