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-actuatoradds/actuator/health,/actuator/info, metrics, and more- Custom
HealthIndicatorbeans add domain-specific health checks - Kubernetes liveness/readiness probes use dedicated
/actuator/health/livenessand/actuator/health/readiness - Micrometer collects metrics;
micrometer-registry-prometheusexposes them for scraping - Custom metrics:
Counterfor events,Gaugefor current values,Timerfor 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.