Spring Boot 4 Actuator in production: endpoints, security, custom health indicators
Companion repository for the ankurm.com article. Every transcript in docs/output/ was produced by running this project; scripts/run-all.sh regenerates all of them. Verified against Spring Boot 4.1.1 / Framework 7.0.9 / Security 7.1.1 / Micrometer 1.17.1 / kafka-clients 4.2.1 on Temurin JDK 25.0.4.1+1.
This commit is contained in:
81
src/test/java/com/ankurm/actuator/HealthGroupTests.java
Normal file
81
src/test/java/com/ankurm/actuator/HealthGroupTests.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.ankurm.actuator;
|
||||
|
||||
import com.ankurm.actuator.health.UpstreamState;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.health.contributor.Status;
|
||||
import org.springframework.boot.health.registry.HealthContributorRegistry;
|
||||
import org.springframework.boot.health.contributor.HealthIndicator;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The contract that matters operationally: an upstream outage must move readiness and must NOT
|
||||
* move liveness.
|
||||
*
|
||||
* <p>See docs/07-groups-and-probes.md.
|
||||
*/
|
||||
// DEFINED_PORT, not the default MOCK environment. ExternalApiHealthIndicator makes a real
|
||||
// HTTP call to this application's own stub controller, so a servlet container has to be
|
||||
// listening on the port the indicator was configured with. With the mock environment the
|
||||
// indicator reports DOWN with "Connection refused" and the test proves nothing.
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
@ActiveProfiles("groups")
|
||||
class HealthGroupTests {
|
||||
|
||||
@Autowired
|
||||
private HealthContributorRegistry registry;
|
||||
|
||||
@Autowired
|
||||
private UpstreamState upstream;
|
||||
|
||||
@AfterEach
|
||||
void reset() {
|
||||
this.upstream.set(UpstreamState.Mode.UP);
|
||||
}
|
||||
|
||||
private Status statusOf(String name) {
|
||||
var contributor = this.registry.getContributor(name);
|
||||
assertThat(contributor).as("contributor '%s' is registered", name).isNotNull();
|
||||
return ((HealthIndicator) contributor).health().getStatus();
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyCustomIndicatorIsRegisteredUnderTheExpectedName() {
|
||||
// The bean name, minus a trailing "HealthIndicator", is the key in the JSON response.
|
||||
// Rename the bean and you silently break every dashboard that reads it.
|
||||
assertThat(statusOf("ordersDatabase")).isNotNull();
|
||||
assertThat(statusOf("externalApi")).isNotNull();
|
||||
assertThat(statusOf("kafka")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void upstreamOutageMovesTheExternalApiIndicatorButNotLiveness() {
|
||||
assertThat(statusOf("externalApi")).isEqualTo(Status.UP);
|
||||
assertThat(statusOf("livenessState")).isEqualTo(Status.UP);
|
||||
|
||||
this.upstream.set(UpstreamState.Mode.DOWN);
|
||||
|
||||
assertThat(statusOf("externalApi")).isEqualTo(Status.DOWN);
|
||||
assertThat(statusOf("livenessState"))
|
||||
.as("a third-party outage must never make this process look unrecoverable")
|
||||
.isEqualTo(Status.UP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void theExternalApiIndicatorRespectsItsTimeoutBudget() {
|
||||
this.upstream.set(UpstreamState.Mode.SLOW);
|
||||
long start = System.nanoTime();
|
||||
Status status = statusOf("externalApi");
|
||||
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
|
||||
|
||||
assertThat(status).isEqualTo(Status.DOWN);
|
||||
// The stub sleeps 30s. demo.upstream.timeout-ms is 750. If this assertion ever fails,
|
||||
// someone removed the read timeout and the health endpoint can now block a worker.
|
||||
assertThat(elapsedMs).isLessThan(5_000L);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user