Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup and FlightRecorderApplicationStartup behind a system property, and a /diag/startup endpoint that computes step self time -- the number /actuator/startup does not give you and the one that names the actual culprits. Captured under docs/output/: the step tree sorted both ways, the same startup as JFR events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache timings (6.93 s to 4.82 s). Post body and metadata live in post/. Moves the existing Actuator project into actuator-in-production/ so the repository holds one directory per article; the root README is now an index.
4.5 KiB
← 04 Securing Actuator · 05 · Custom health indicators · 06 Failure modes →
05 — Custom health indicators
Three indicators, three different jobs.
| Bean | Class | Checks |
|---|---|---|
ordersDatabase |
OrdersDatabaseHealthIndicator |
the query the application depends on |
kafka |
KafkaHealthIndicator |
AdminClient.describeCluster |
externalApi |
ExternalApiHealthIndicator |
an HTTP dependency, with a budget |
The shape
import org.springframework.boot.health.contributor.AbstractHealthIndicator;
import org.springframework.boot.health.contributor.Health;
@Component("ordersDatabase")
public class OrdersDatabaseHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// throw, or call builder.up() / down() / status("DEGRADED")
}
}
Note the package — org.springframework.boot.health.contributor, new in Boot 4. See
chapter 02.
AbstractHealthIndicator over the bare interface, because it catches your exceptions and turns
them into DOWN with the error recorded, rather than letting one throwing indicator take down
the whole /actuator/health response.
The bean name is the JSON key
@Component("ordersDatabase") produces "ordersDatabase" in the response. Boot strips a
trailing HealthIndicator from the bean name, so ordersDatabaseHealthIndicator gives the same
key. Rename the bean and every dashboard, alert and health-group include: that referenced the
old key silently stops matching — which is why
HealthGroupTests asserts on the
names.
Check what the application needs, not what the connection can do
Boot's built-in db indicator runs a validation query and answers "can I reach the database".
That is rarely the question. ordersDatabase runs the query the service actually depends on and
adds a latency judgement:
{"details":{"orders":3,"queryMs":0,"slowThresholdMs":250},"status":"UP"}
It returns a custom DEGRADED status above the threshold. Custom statuses are legal;
management.endpoint.health.status.order and .http-mapping control how they aggregate and
what HTTP code they produce. An unmapped custom status aggregates as UNKNOWN and maps to 200,
which is usually not what you meant.
Every indicator needs a budget
All three carry an explicit timeout, and the reason is chapter 06.
ordersDatabase—jdbc.setQueryTimeout(2)externalApi— connect and read timeouts of 750 ms on theRestClientrequest factorykafka— four separate Kafka bounds plus an outerKafkaFuture.get(timeout)
Credentials
externalApi sends HTTP Basic. The first captured run of this repository did not, and reported:
{"error":"HttpClientErrorException$Unauthorized: 401 : [no body]","status":"DOWN"}
which looks exactly like an upstream outage and was a missing header. If your indicator authenticates, make the failure message distinguish "they are down" from "we are unauthorised" — future you will be reading it at 3am.
Reuse the client
KafkaHealthIndicator creates one AdminClient in its constructor and closes it in
close(). Creating one per probe opens a fresh set of broker connections on every probe
interval; across a fleet of any size that is a denial-of-service against your own brokers.
What it all looks like
output/07-custom-health-indicators.txt has the full
nine-component response. Flipping the stub upstream to DOWN and changing nothing else:
{
"status": "DOWN",
"externalApi": {
"details": {
"error": "HttpServerErrorException$ServiceUnavailable: 503 : \"upstream unavailable\"",
"url": "http://localhost:8080/stub/upstream/ping",
"latencyMs": 101,
"timeoutMs": 750
},
"status": "DOWN"
}
}
/actuator/health now answers 503. If that URL is your readiness probe, every pod in the
deployment just left the load balancer because a third party had a bad minute.
Chapter 07 fixes that.
← 04 · 05 · 06 Failure modes →