Spring Boot Actuator: Health, Metrics, and Management Endpoints

A running application is not enough — you need to know if it’s healthy, how it’s performing, and what it’s doing. Spring Boot Actuator exposes that information through HTTP endpoints and metrics.

Setup

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

By default, only /actuator/health and /actuator/info are exposed over HTTP. Everything else is available via JMX. Enable what you need:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,conditions,beans,env,loggers,threaddump,heapdump
      base-path: /actuator
  endpoint:
    health:
      show-details: when-authorized   # or 'always' (dev), 'never' (public)
      show-components: when-authorized
    metrics:
      enabled: true
  server:
    port: 8081   # expose actuator on a separate port (not public-facing)

Health Endpoint

GET /actuator/health — used by Kubernetes liveness/readiness probes and load balancers:

{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": { "database": "PostgreSQL", "validationQuery": "isValid()" }
    },
    "diskSpace": {
      "status": "UP",
      "details": { "total": 499963174912, "free": 382847729664, "threshold": 10485760 }
    },
    "redis": {
      "status": "UP"
    }
  }
}

Status values: UP, DOWN, OUT_OF_SERVICE, UNKNOWN.

Custom Health Indicators

@Component
public class ExternalPaymentServiceHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    @Override
    public Health health() {
        try {
            boolean reachable = client.ping();
            if (reachable) {
                return Health.up()
                    .withDetail("latency", client.getLastPingLatencyMs() + "ms")
                    .build();
            }
            return Health.down()
                .withDetail("reason", "Payment gateway did not respond")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .build();
        }
    }
}

Kubernetes Probes

Spring Boot provides dedicated liveness and readiness probes:

management:
  endpoint:
    health:
      probes:
        enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true
  • GET /actuator/health/liveness — is the app alive? (restart if DOWN)
  • GET /actuator/health/readiness — is the app ready to handle traffic? (remove from load balancer if DOWN)
# kubernetes deployment.yaml
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8081
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8081
  initialDelaySeconds: 10
  periodSeconds: 5

Mark the app as not ready during startup or maintenance:

@Service
public class StartupInitializationService {

    private final ApplicationEventPublisher eventPublisher;

    @EventListener(ApplicationStartedEvent.class)
    public void onStartup() {
        // App is alive but not ready yet
        eventPublisher.publishEvent(new AvailabilityChangeEvent<>(
            this, ReadinessState.REFUSING_TRAFFIC));

        loadLargeDataset();  // takes time

        // Now ready
        eventPublisher.publishEvent(new AvailabilityChangeEvent<>(
            this, ReadinessState.ACCEPTING_TRAFFIC));
    }
}

Info Endpoint

GET /actuator/info — returns build information, git commit, custom properties:

management:
  info:
    env:
      enabled: true
    git:
      enabled: true
      mode: full    # includes branch, commit hash, message
    build:
      enabled: true
    java:
      enabled: true

info:
  app:
    name: order-service
    description: Order management for DevOpsMonk platform
    version: "@project.version@"   # from pom.xml
  contact:
    team: platform
    slack: "#order-service"

Add git info (generate git.properties during build):

<!-- pom.xml -->
<plugin>
    <groupId>io.github.git-commit-id</groupId>
    <artifactId>git-commit-id-maven-plugin</artifactId>
    <version>9.0.1</version>
    <executions>
        <execution>
            <goals><goal>revision</goal></goals>
        </execution>
    </executions>
</plugin>

Now /actuator/info shows:

{
  "app": { "name": "order-service", "version": "1.2.3" },
  "git": {
    "branch": "main",
    "commit": { "id": "a3f92b1", "message": "feat: add order cancellation", "time": "2026-05-01T10:30:00Z" }
  },
  "build": { "artifact": "order-service", "version": "1.2.3", "time": "2026-05-03T08:00:00Z" }
}

Metrics with Micrometer

Micrometer is the metrics facade — it collects metrics and ships them to any backend (Prometheus, Datadog, CloudWatch, etc.).

Spring Boot auto-instruments:

  • JVM (heap, GC, threads)
  • HTTP requests (latency, error rates)
  • DataSource (connection pool usage)
  • Logback (log count by level)
  • Spring MVC (request counts and latencies)

Prometheus Endpoint

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

GET /actuator/prometheus returns all metrics in Prometheus text format:

# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 2.4117248E7

# HELP http_server_requests_seconds Duration of HTTP server request handling
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/orders"} 1543
http_server_requests_seconds_sum{...} 12.456

Custom Metrics

@Service
@RequiredArgsConstructor
public class OrderService {

    private final MeterRegistry registry;
    private final OrderRepository orderRepository;

    // Counter: track events
    private final Counter ordersCreated;
    private final Counter ordersFailed;

    public OrderService(MeterRegistry registry, OrderRepository repository) {
        this.registry = registry;
        this.orderRepository = repository;

        this.ordersCreated = Counter.builder("orders.created")
            .description("Total orders created")
            .tag("service", "order-service")
            .register(registry);

        this.ordersFailed = Counter.builder("orders.failed")
            .description("Total order creation failures")
            .register(registry);
    }

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        try {
            Order order = processOrder(request);
            ordersCreated.increment();
            return order;
        } catch (Exception e) {
            ordersFailed.increment(Tags.of("reason", e.getClass().getSimpleName()));
            throw e;
        }
    }
}

Types of Metrics

@Component
public class OrderMetrics {

    public OrderMetrics(MeterRegistry registry, OrderRepository repository) {

        // Counter — monotonically increasing (events)
        Counter.builder("orders.created").register(registry);

        // Gauge — current value (queue size, cache size, active connections)
        Gauge.builder("orders.pending.count",
                repository, repo -> repo.countByStatus(OrderStatus.PENDING))
            .description("Number of pending orders")
            .register(registry);

        // Timer — measure duration
        Timer timer = Timer.builder("orders.processing.time")
            .description("Time to process an order")
            .publishPercentiles(0.5, 0.95, 0.99)  // p50, p95, p99
            .register(registry);

        // DistributionSummary — measure value distribution (order amounts)
        DistributionSummary.builder("orders.amount")
            .description("Distribution of order amounts")
            .baseUnit("USD")
            .publishPercentiles(0.5, 0.95)
            .register(registry);
    }
}

@Timed — Method-Level Timing

@Service
public class OrderService {

    @Timed(value = "orders.create", description = "Time to create an order",
           percentiles = {0.5, 0.95, 0.99})
    public Order createOrder(CreateOrderRequest request) { ... }

    @Timed("inventory.check")
    public boolean checkInventory(UUID productId, int quantity) { ... }
}

Enable it:

@Configuration
public class MetricsConfig {
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

Loggers Endpoint

Change log levels at runtime without restarting:

# Get current log level for a package
GET /actuator/loggers/com.devopsmonk.order

# Response
{ "configuredLevel": "INFO", "effectiveLevel": "INFO" }

# Change to DEBUG temporarily
POST /actuator/loggers/com.devopsmonk.order
Content-Type: application/json
{ "configuredLevel": "DEBUG" }

# Reset to default
POST /actuator/loggers/com.devopsmonk.order
{ "configuredLevel": null }

Invaluable for diagnosing production issues without redeployment.

Env Endpoint

See all resolved configuration properties:

GET /actuator/env
GET /actuator/env/spring.datasource.url   # specific property

Security: Sensitive values like passwords are masked automatically (****). Still restrict access to this endpoint.

Securing Actuator

Never expose management endpoints publicly on the main port:

# Separate port for internal/ops access only
management:
  server:
    port: 8081

Or secure with Spring Security:

@Bean
@Order(1)
public SecurityFilterChain actuatorSecurityChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/actuator/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health/liveness",
                             "/actuator/health/readiness").permitAll()
            .requestMatchers("/actuator/prometheus").hasIpAddress("10.0.0.0/8")
            .anyRequest().hasRole("OPS")
        )
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

Or restrict by IP in your reverse proxy / Kubernetes NetworkPolicy.

What You’ve Learned

  • spring-boot-starter-actuator adds /actuator/health, /actuator/info, metrics, and more
  • Custom HealthIndicator beans add domain-specific health checks
  • Kubernetes liveness/readiness probes use dedicated /actuator/health/liveness and /actuator/health/readiness
  • Micrometer collects metrics; micrometer-registry-prometheus exposes them for scraping
  • Custom metrics: Counter for events, Gauge for current values, Timer for durations
  • Change log levels at runtime with POST /actuator/loggers/{package}
  • Always expose actuator on a separate port or restrict access — never public

Next: Article 35 — Logging: SLF4J, Logback, and Structured Logging — configure logging for local dev and production.