diff --git a/boot/actuator-webflux/README.adoc b/boot/actuator-webflux/README.adoc new file mode 100644 index 0000000..85b4abd --- /dev/null +++ b/boot/actuator-webflux/README.adoc @@ -0,0 +1 @@ +Tests the Spring Boot Actuator on WebFlux. diff --git a/boot/actuator-webflux/build.gradle b/boot/actuator-webflux/build.gradle new file mode 100644 index 0000000..826f5ab --- /dev/null +++ b/boot/actuator-webflux/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "java" + id "org.springframework.boot" + id "org.springframework.cr.smoke-test" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.crac:crac:$cracVersion") + implementation(project(":cr-listener")) + + runtimeOnly("io.micrometer:micrometer-registry-prometheus") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":cr-smoke-test-support")) +} + +crSmokeTest { + webApplication = true +} diff --git a/boot/actuator-webflux/src/appTest/java/com/example/actuator/webmvc/ActuatorWebFluxApplicationAotTests.java b/boot/actuator-webflux/src/appTest/java/com/example/actuator/webmvc/ActuatorWebFluxApplicationAotTests.java new file mode 100644 index 0000000..7fb9549 --- /dev/null +++ b/boot/actuator-webflux/src/appTest/java/com/example/actuator/webmvc/ActuatorWebFluxApplicationAotTests.java @@ -0,0 +1,187 @@ +package com.example.actuator.webmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.cr.smoketest.support.junit.ApplicationTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +class ActuatorWebFluxApplicationAotTests { + + @Test + void shouldContainLinks(WebTestClient client) { + client.get() + .uri("/actuator") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$._links.self.templated") + .isEqualTo(false) + .jsonPath("$._links.env-toMatch.templated") + .isEqualTo(true); + } + + @Test + void shouldHaveCustomHealthIndicator(WebTestClient client) { + client.get() + .uri("/actuator/health") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.status") + .isEqualTo("UP") + .jsonPath("$.components.custom.status") + .isEqualTo("UP") + .jsonPath("$.components.custom.details.hello") + .isEqualTo("world"); + } + + @Test + void shouldHaveAnotherCustomHealthIndicator(WebTestClient client) { + client.get() + .uri("/actuator/health") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.components.anotherCustom.status") + .isEqualTo("UP"); + } + + @Test + void shouldHaveCompositeHealth(WebTestClient client) { + client.get() + .uri("/actuator/health") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.components.composite.status") + .isEqualTo("UP") + .jsonPath("$.components.composite.components.another-custom.status") + .isEqualTo("UP") + .jsonPath("$.components.composite.components.custom.status") + .isEqualTo("UP"); + } + + @Test + void shouldHaveCustomEndpoint(WebTestClient client) { + client.get() + .uri("/actuator/custom") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("custom-read")); + } + + @Test + void shouldHaveCustomWebEndpoint(WebTestClient client) { + client.get() + .uri("/actuator/customWeb") + .exchange() + .expectStatus() + .isEqualTo(299) + .expectBody() + .consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("customWeb-read")); + } + + @Test + void shouldHaveReadiness(WebTestClient client) { + client.get() + .uri("/actuator/health/readiness") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.status") + .isEqualTo("UP"); + } + + @Test + void shouldHaveEnvInfoProperties(WebTestClient client) { + client.get() + .uri("/actuator/info") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.app.hello") + .isEqualTo("world"); + } + + @Test + void shouldHaveJavaInfoProperties(WebTestClient client) { + client.get() + .uri("/actuator/info") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.java.version") + .isNotEmpty(); + } + + @Test + void shouldHaveOsInfoProperties(WebTestClient client) { + client.get() + .uri("/actuator/info") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.os.name") + .isNotEmpty(); + } + + @Test + void shouldHaveMetrics(WebTestClient client) { + client.get() + .uri("/actuator/metrics/jvm.classes.loaded") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.measurements.[0].value") + .isNotEmpty(); + } + + @Test + void prometheusWorks(WebTestClient client) { + client.get() + .uri("/actuator/prometheus") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith((result) -> assertThat(new String(result.getResponseBodyContent())) + // Check custom timer + .contains("custom_timer_seconds_max 5.0") + .contains("custom_timer_seconds_count 1.0") + .contains("custom_timer_seconds_sum 5.0") + // Check JVM metric + .contains("# TYPE jvm_threads_peak_threads gauge")); + } + + @Test + void shouldHaveLoggers(WebTestClient client) { + client.get() + .uri("/actuator/loggers") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.levels") + .isNotEmpty() + .jsonPath("$.loggers.['ROOT']") + .isNotEmpty() + .jsonPath("$.loggers.['_org.springframework']") + .isNotEmpty(); + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/ActuatorWebfluxApplication.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/ActuatorWebfluxApplication.java new file mode 100644 index 0000000..8939b7b --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/ActuatorWebfluxApplication.java @@ -0,0 +1,28 @@ +package com.example.actuator.webmvc; + +import java.util.Map; + +import com.example.actuator.webmvc.health.AnotherCustomHealthIndicator; +import com.example.actuator.webmvc.health.CustomHealthIndicator; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.health.CompositeHealthContributor; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class ActuatorWebfluxApplication { + + public static void main(String[] args) throws InterruptedException { + SpringApplication.run(ActuatorWebfluxApplication.class, args); + Thread.currentThread().join(); // To be able to measure memory consumption + } + + @Bean("composite") + CompositeHealthContributor compositeHealthContributor(CustomHealthIndicator customHealthIndicator, + AnotherCustomHealthIndicator anotherCustomHealthIndicator) { + return CompositeHealthContributor + .fromMap(Map.of("custom", customHealthIndicator, "another-custom", anotherCustomHealthIndicator)); + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomEndpoint.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomEndpoint.java new file mode 100644 index 0000000..eaf776e --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomEndpoint.java @@ -0,0 +1,16 @@ +package com.example.actuator.webmvc; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.stereotype.Component; + +@Component +@Endpoint(id = "custom") +class CustomEndpoint { + + @ReadOperation + String read() { + return "custom-read"; + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomWebEndpoint.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomWebEndpoint.java new file mode 100644 index 0000000..e61fe33 --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/CustomWebEndpoint.java @@ -0,0 +1,17 @@ +package com.example.actuator.webmvc; + +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.stereotype.Component; + +@Component +@WebEndpoint(id = "customWeb") +class CustomWebEndpoint { + + @ReadOperation + WebEndpointResponse read() { + return new WebEndpointResponse<>("customWeb-read", 299); + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/RegisterCustomTimer.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/RegisterCustomTimer.java new file mode 100644 index 0000000..c6c90b8 --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/RegisterCustomTimer.java @@ -0,0 +1,24 @@ +package com.example.actuator.webmvc; + +import java.time.Duration; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +class RegisterCustomTimer implements CommandLineRunner { + + private final MeterRegistry meterRegistry; + + RegisterCustomTimer(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public void run(String... args) { + this.meterRegistry.timer("custom.timer").record(Duration.ofSeconds(5)); + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/AnotherCustomHealthIndicator.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/AnotherCustomHealthIndicator.java new file mode 100644 index 0000000..7b34e45 --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/AnotherCustomHealthIndicator.java @@ -0,0 +1,15 @@ +package com.example.actuator.webmvc.health; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class AnotherCustomHealthIndicator implements HealthIndicator { + + @Override + public Health health() { + return Health.up().build(); + } + +} diff --git a/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/CustomHealthIndicator.java b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/CustomHealthIndicator.java new file mode 100644 index 0000000..73e1898 --- /dev/null +++ b/boot/actuator-webflux/src/main/java/com/example/actuator/webmvc/health/CustomHealthIndicator.java @@ -0,0 +1,15 @@ +package com.example.actuator.webmvc.health; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class CustomHealthIndicator implements HealthIndicator { + + @Override + public Health health() { + return Health.up().withDetail("hello", "world").build(); + } + +} diff --git a/boot/actuator-webflux/src/main/resources/application.properties b/boot/actuator-webflux/src/main/resources/application.properties new file mode 100644 index 0000000..54cc6c4 --- /dev/null +++ b/boot/actuator-webflux/src/main/resources/application.properties @@ -0,0 +1,8 @@ +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +management.endpoint.health.show-components=always +management.endpoint.health.probes.enabled=true +management.info.env.enabled=true +management.info.java.enabled=true +management.info.os.enabled=true +info.app.hello=world