Actuator & Observability Endpoints
Every Actuator endpoint worth exposing, writing custom health indicators for Kubernetes probes, adding Micrometer metrics that answer real questions, and securing it all.
On this page
Actuator turns a Spring Boot application from a black box into something you can interrogate. Out of the box it answers "is it up", "what is it configured with" and "what is it doing right now" — and with a little work it answers questions specific to your domain, which is where the real value is.
Key Takeaways
- Only
healthandinfoare exposed over HTTP by default; everything else is opt-in. - Liveness and readiness are different questions — conflating them causes restart storms during dependency outages.
- A custom
HealthIndicatoris about ten lines and is what makes readiness meaningful. - Micrometer meters are dimensional: tag them, but never with unbounded values like a user ID.
- Run management endpoints on a separate port in production.
The endpoints worth knowing
| Endpoint | Answers | Expose publicly? |
|---|---|---|
/actuator/health | Is the app up, and are its dependencies reachable? | Yes (without details) |
/actuator/info | Which build and commit is this? | Yes |
/actuator/metrics | What are the numbers? | No |
/actuator/prometheus | Same, in scrape format | Internal only |
/actuator/loggers | Current log levels — and changes them at runtime | No |
/actuator/env | Effective configuration and its source | No |
/actuator/configprops | Bound @ConfigurationProperties values | No |
/actuator/beans | Every bean in the context | No |
/actuator/conditions | The auto-configuration report | No |
/actuator/mappings | Every request mapping | No |
/actuator/threaddump | Live thread states | No |
/actuator/heapdump | A full heap dump file | Never |
/actuator/loggers deserves special mention: it lets you raise a package to DEBUG on a running
instance and drop it back, with no redeploy.
curl -X POST localhost:8080/actuator/loggers/com.acme.payments \
-H 'Content-Type: application/json' \
-d '{"configuredLevel":"DEBUG"}'Health, liveness and readiness
Enable the probe groups explicitly:
management:
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
group:
readiness:
include: 'readinessState,db,redis'
liveness:
include: 'livenessState'
endpoints:
web:
exposure:
include: 'health,info,prometheus'
server:
port: 9090 # separate management portThat gives you /actuator/health/liveness and /actuator/health/readiness as distinct endpoints
with distinct contributor sets.
A custom health indicator
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final RestClient client;
public PaymentGatewayHealthIndicator(RestClient.Builder builder,
@Value("${app.gateway.base-url}") String baseUrl) {
this.client = builder.baseUrl(baseUrl).build();
}
@Override
public Health health() {
long start = System.nanoTime();
try {
var status = client.get().uri("/ping").retrieve().toBodilessEntity();
long millis = (System.nanoTime() - start) / 1_000_000;
// Slow but reachable is still degraded — surface it rather than
// reporting a flat UP and waiting for user complaints.
return (millis > 500 ? Health.status("DEGRADED") : Health.up())
.withDetail("latencyMillis", millis)
.withDetail("httpStatus", status.getStatusCode().value())
.build();
} catch (RestClientException ex) {
return Health.down(ex).withDetail("baseUrl", client.toString()).build();
}
}
}The bean name determines the key in the JSON: PaymentGatewayHealthIndicator becomes
paymentGateway. Keep the check cheap and give it a timeout — a health endpoint that blocks for
thirty seconds turns a partial outage into a total one, because the orchestrator's probe times out
too.
Metrics that answer questions
Micrometer is a facade over the metrics backend, in the same way SLF4J is over logging. Four instrument types cover almost everything:
- Counter — a monotonically increasing count. Orders placed, retries attempted.
- Gauge — a value sampled on scrape. Queue depth, cache size.
- Timer — duration plus count, with percentiles. Any operation you care about the latency of.
- DistributionSummary — like a timer but for non-time values. Payload sizes, basket totals.
@Service
public class OrderService {
private final Counter placed;
private final Timer settlement;
private final MeterRegistry registry;
public OrderService(MeterRegistry registry, OrderQueue queue) {
this.registry = registry;
this.placed = Counter.builder("orders.placed")
.description("Orders accepted for processing")
.register(registry);
this.settlement = Timer.builder("orders.settlement")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// A gauge holds a weak reference to the object it samples. Keep the
// referent alive, or the gauge quietly starts reporting NaN.
Gauge.builder("orders.queue.depth", queue, OrderQueue::size).register(registry);
}
public Receipt place(Order order) {
return settlement.record(() -> {
Receipt receipt = doPlace(order);
// Tag with a bounded dimension: a handful of channels, not a user id.
registry.counter("orders.placed", "channel", order.channel()).increment();
placed.increment();
return receipt;
});
}
}The cardinality rule is not a style preference. Every distinct combination of tag values creates a
separate time series in Prometheus. Tagging by userId on a site with a million users creates a
million series and will take the monitoring stack down before it takes your application down.
For simple cases the annotations are enough — @Timed and @Counted on a method, provided you have
registered a TimedAspect bean.
Common tags and build info
@Configuration
public class MetricsConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> commonTags(
@Value("${spring.application.name}") String app,
@Value("${app.environment}") String env) {
return registry -> registry.config().commonTags("application", app, "env", env);
}
}Common tags let one Grafana dashboard serve every service. Pair it with build information in
/actuator/info so a dashboard can show which commit is running:
management:
info:
env:
enabled: true
git:
mode: full
info:
app:
name: '@project.artifactId@'
version: '@project.version@'Securing the endpoints
Two layers, both worth having:
@Bean
SecurityFilterChain actuator(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}The second layer is the network: management.server.port moves everything except your application
traffic onto a port your ingress does not route. Even if the security configuration is wrong, the
endpoints are unreachable from outside the cluster.
What to take away
Expose health and info publicly, everything else on an internal port behind authentication.
Write health indicators for the dependencies whose failure should stop traffic, and put them in the
readiness group only. Add timers and counters for the operations you would want a graph of during an
incident — and keep the tags bounded.
Frequently Asked Questions
What is the difference between liveness and readiness?
Is it safe to expose /actuator publicly?
Why are my custom metrics missing from Prometheus?
Related tutorials
- Spring AOP & Aspect-Oriented ProgrammingSpring AOP from pointcut syntax to proxy mechanics: the five advice types, writing annotation-driven aspects, aspect ordering, and why self-invocation silently does nothing.
- Spring Boot Testing MasterclassA test strategy that stays fast: when to use @SpringBootTest versus a slice, real databases with Testcontainers and @ServiceConnection, stubbing HTTP with WireMock, and context caching.
- Configuration & Profiles MasteryType-safe configuration with @ConfigurationProperties, the full property precedence order, relaxed binding rules, profile groups, and keeping secrets out of your YAML.
- Exception Handling & Error Response DesignA consistent error contract for a Spring Boot API: an exception hierarchy worth having, @ControllerAdvice done properly, RFC 7807 ProblemDetail, and validation errors clients can act on.