Actuator, health checks and metrics
The production endpoints Spring Boot gives you, which of them are exposed by default, how health is aggregated into one status, and what Micrometer records.
After this lesson you can
- Expose exactly the Actuator endpoints you intend, and know the default
- Predict the overall health status and HTTP code from its components
- Explain liveness versus readiness, and why metric tags must stay low-cardinality
- Change a log level at runtime and predict a logger's effective level
spring-boot-starter-actuator adds operational endpoints under
/actuator: health, info, metrics, env, beans, conditions,
configprops, loggers (read and change log levels at runtime),
mappings, threaddump, heapdump, and prometheus when the
Prometheus registry is on the classpath.
Exposure is opt-in
Most endpoints are enabled but not exposed over HTTP. Out of the
box, Spring Boot 3 exposes only health. You choose the rest:
management.endpoints.web.exposure.include=health,info,metrics,prometheus
# or everything except the sensitive ones:
# management.endpoints.web.exposure.include=*
# management.endpoints.web.exposure.exclude=env,beans,heapdump
exclude always beats include. Be deliberate: env and
configprops reveal configuration (Boot masks values by default, but
the structure is still informative), heapdump hands out the
application's memory. Put Actuator behind Spring Security, or on a
separate port with management.server.port that is not reachable from
the internet.
Health
/actuator/health combines health indicators — Boot contributes
them for the database, disk space, Redis, and so on, and you can add
your own HealthIndicator bean. The overall status is the most severe
component status, in the order DOWN, OUT_OF_SERVICE, UP,
UNKNOWN. DOWN and OUT_OF_SERVICE map to 503, UP and
UNKNOWN to 200, which is what a load balancer actually reads.
Component details are hidden unless you set
management.endpoint.health.show-details.
On Kubernetes, Boot also provides two health groups:
/actuator/health/liveness ("is the process broken — restart it?") and
/actuator/health/readiness ("can it take traffic right now?"). A group
is aggregated by the same rule, over only the indicators it includes.
By default liveness holds livenessState and readiness holds
readinessState; you can add more, for example
management.endpoint.health.group.readiness.include=readinessState,db.
Keep external dependencies such as the database out of liveness: a
database outage should take instances out of rotation, not restart all
of them in a loop. The model below configures readiness that way and
probes all three URLs during a database outage, and again while the
application is shutting down (readiness reports OUT_OF_SERVICE once
it refuses traffic):
import java.util.*; public class Solution { // Which indicators each URL aggregates; readiness also includes db. static final Map<String, List<String>> GROUPS = new LinkedHashMap<>(); static { GROUPS.put("/actuator/health", List.of("db", "diskSpace", "livenessState", "readinessState")); GROUPS.put("/actuator/health/liveness", List.of("livenessState")); GROUPS.put("/actuator/health/readiness", List.of("readinessState", "db")); } static final List<String> SEVERITY = List.of("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN"); static List<String> probe(String moment, Map<String, String> indicators) { List<String> lines = new ArrayList<>(); GROUPS.forEach((url, members) -> { String status = members.stream() .map(indicators::get) .min(Comparator.comparingInt(SEVERITY::indexOf)) .orElseThrow(); int http = status.equals("DOWN") || status.equals("OUT_OF_SERVICE") ? 503 : 200; lines.add(moment + ": " + url + " " + status + " " + http); }); return lines; } public static List<String> run() { List<String> out = new ArrayList<>(); out.addAll(probe("db outage", Map.of( "db", "DOWN", "diskSpace", "UP", "livenessState", "UP", "readinessState", "UP"))); out.addAll(probe("shutting down", Map.of( "db", "UP", "diskSpace", "UP", "livenessState", "UP", "readinessState", "OUT_OF_SERVICE"))); return out; }}Metrics and tracing
Boot instruments the application with Micrometer, a facade over
monitoring systems (Prometheus, Datadog, OTLP, …). Every Spring MVC
request is timed as http.server.requests, tagged with method,
status, outcome, exception and uri — and uri is the route
template, /orders/{id}, not /orders/42. That keeps the number
of tag values bounded: each distinct combination of tags is a separate
time series, so a tag holding a user id or a raw path creates one series
per value and can take down the monitoring system. Your own metrics
follow the same rule.
For distributed tracing, Spring Boot 3 uses Micrometer Tracing (the successor of Spring Cloud Sleuth) with a Brave or OpenTelemetry bridge; trace and span ids propagate across HTTP calls and can be written into every log line, so one request can be followed through several services.
Log levels at runtime
During an incident you often want more logging from one package
without a redeploy. GET /actuator/loggers/com.acme.orders shows the
logger's configuredLevel (its own, possibly null) and its
effectiveLevel; POST to the same URL with
{"configuredLevel": "DEBUG"} changes it, and posting null removes
the override again. A logger without its own level inherits the level
of its nearest configured ancestor in the dot-separated name
(, then ), and finally 's. The change lives in
memory only, so a restart reverts it.
Exercise · Pro
Change a log level at runtime
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade