diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java index eaab6482ed..f1b1df2c5c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java @@ -74,6 +74,10 @@ public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator return this; } + ReactiveHealthIndicatorRegistry getRegistry() { + return this.registry; + } + @Override public Mono health() { return Flux.fromIterable(this.registry.getAll().entrySet()) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index 4f53f49dde..87074cc9e6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; @@ -48,10 +49,46 @@ public class ReactiveHealthEndpointWebExtension { .map((health) -> this.responseMapper.map(health, securityContext)); } + @ReadOperation + public Mono> healthForComponent( + SecurityContext securityContext, @Selector String component) { + return responseFromIndicator(getNestedHealthIndicator(this.delegate, component), + securityContext); + } + + @ReadOperation + public Mono> healthForComponentInstance( + SecurityContext securityContext, @Selector String component, + @Selector String instance) { + ReactiveHealthIndicator indicator = getNestedHealthIndicator(this.delegate, + component); + if (indicator != null) { + indicator = getNestedHealthIndicator(indicator, instance); + } + return responseFromIndicator(indicator, securityContext); + } + public Mono> health(SecurityContext securityContext, ShowDetails showDetails) { return this.delegate.health().map((health) -> this.responseMapper.map(health, securityContext, showDetails)); } + private Mono> responseFromIndicator( + ReactiveHealthIndicator indicator, SecurityContext securityContext) { + return (indicator != null) + ? indicator.health() + .map((health) -> this.responseMapper.map(health, securityContext)) + : Mono.empty(); + } + + private ReactiveHealthIndicator getNestedHealthIndicator( + ReactiveHealthIndicator healthIndicator, String name) { + if (healthIndicator instanceof CompositeReactiveHealthIndicator) { + return ((CompositeReactiveHealthIndicator) healthIndicator).getRegistry() + .get(name); + } + return null; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java index 186c3af3b4..8bc03e6b2b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,20 @@ package org.springframework.boot.actuate.health; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; +import reactor.core.publisher.Mono; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointRunners; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,23 +58,87 @@ public class HealthEndpointWebIntegrationTests { } @Test - public void whenHealthIsDown503ResponseIsReturned() { + public void whenHealthIsDown503ResponseIsReturned() throws Exception { + withHealthIndicator("charlie", () -> Health.down().build(), + () -> Mono.just(Health.down().build()), () -> { + client.get().uri("/actuator/health").exchange().expectStatus() + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody() + .jsonPath("status").isEqualTo("DOWN") + .jsonPath("details.alpha.status").isEqualTo("UP") + .jsonPath("details.bravo.status").isEqualTo("UP") + .jsonPath("details.charlie.status").isEqualTo("DOWN"); + return null; + }); + } + + @Test + public void whenComponentHealthIsDown503ResponseIsReturned() throws Exception { + withHealthIndicator("charlie", () -> Health.down().build(), + () -> Mono.just(Health.down().build()), () -> { + client.get().uri("/actuator/health/charlie").exchange().expectStatus() + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody() + .jsonPath("status").isEqualTo("DOWN"); + return null; + }); + } + + @Test + public void whenComponentInstanceHealthIsDown503ResponseIsReturned() + throws Exception { + CompositeHealthIndicator composite = new CompositeHealthIndicator( + new OrderedHealthAggregator(), + Collections.singletonMap("one", () -> Health.down().build())); + CompositeReactiveHealthIndicator reactiveComposite = new CompositeReactiveHealthIndicator( + new OrderedHealthAggregator(), + new DefaultReactiveHealthIndicatorRegistry(Collections.singletonMap("one", + () -> Mono.just(Health.down().build())))); + withHealthIndicator("charlie", composite, reactiveComposite, () -> { + client.get().uri("/actuator/health/charlie/one").exchange().expectStatus() + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody() + .jsonPath("status").isEqualTo("DOWN"); + return null; + }); + } + + private void withHealthIndicator(String name, HealthIndicator healthIndicator, + ReactiveHealthIndicator reactiveHealthIndicator, Callable action) + throws Exception { + Consumer unregister; + Consumer reactiveUnregister; + try { + ReactiveHealthIndicatorRegistry registry = context + .getBean(ReactiveHealthIndicatorRegistry.class); + registry.register(name, reactiveHealthIndicator); + reactiveUnregister = registry::unregister; + } + catch (NoSuchBeanDefinitionException ex) { + reactiveUnregister = (indicatorName) -> { + }; + // Continue + } HealthIndicatorRegistry registry = context.getBean(HealthIndicatorRegistry.class); - registry.register("charlie", () -> Health.down().build()); + registry.register(name, healthIndicator); + unregister = reactiveUnregister.andThen(registry::unregister); try { - client.get().uri("/actuator/health").exchange().expectStatus() - .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody() - .jsonPath("status").isEqualTo("DOWN").jsonPath("details.alpha.status") - .isEqualTo("UP").jsonPath("details.bravo.status").isEqualTo("UP") - .jsonPath("details.charlie.status").isEqualTo("DOWN"); + action.call(); } finally { - registry.unregister("charlie"); + unregister.accept("charlie"); } } @Test public void whenHealthIndicatorIsRemovedResponseIsAltered() { + Consumer reactiveRegister = null; + try { + ReactiveHealthIndicatorRegistry registry = context + .getBean(ReactiveHealthIndicatorRegistry.class); + ReactiveHealthIndicator unregistered = registry.unregister("bravo"); + reactiveRegister = (name) -> registry.register(name, unregistered); + } + catch (NoSuchBeanDefinitionException ex) { + // Continue + } HealthIndicatorRegistry registry = context.getBean(HealthIndicatorRegistry.class); HealthIndicator bravo = registry.unregister("bravo"); try { @@ -78,6 +149,9 @@ public class HealthEndpointWebIntegrationTests { } finally { registry.register("bravo", bravo); + if (reactiveRegister != null) { + reactiveRegister.accept("bravo"); + } } } @@ -91,6 +165,16 @@ public class HealthEndpointWebIntegrationTests { .createHealthIndicatorRegistry(healthIndicators); } + @Bean + @ConditionalOnWebApplication(type = Type.REACTIVE) + public ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry( + Map reactiveHealthIndicators, + Map healthIndicators) { + return new ReactiveHealthIndicatorRegistryFactory() + .createReactiveHealthIndicatorRegistry(reactiveHealthIndicators, + healthIndicators); + } + @Bean public HealthEndpoint healthEndpoint(HealthIndicatorRegistry registry) { return new HealthEndpoint(new CompositeHealthIndicator( @@ -98,6 +182,7 @@ public class HealthEndpointWebIntegrationTests { } @Bean + @ConditionalOnWebApplication(type = Type.SERVLET) public HealthEndpointWebExtension healthWebEndpointExtension( HealthEndpoint healthEndpoint) { return new HealthEndpointWebExtension(healthEndpoint, @@ -106,6 +191,18 @@ public class HealthEndpointWebIntegrationTests { new HashSet<>(Arrays.asList("ACTUATOR")))); } + @Bean + @ConditionalOnWebApplication(type = Type.REACTIVE) + public ReactiveHealthEndpointWebExtension reactiveHealthWebEndpointExtension( + ReactiveHealthIndicatorRegistry registry, HealthEndpoint healthEndpoint) { + return new ReactiveHealthEndpointWebExtension( + new CompositeReactiveHealthIndicator(new OrderedHealthAggregator(), + registry), + new HealthWebEndpointResponseMapper(new HealthStatusHttpMapper(), + ShowDetails.ALWAYS, + new HashSet<>(Arrays.asList("ACTUATOR")))); + } + @Bean public HealthIndicator alphaHealthIndicator() { return () -> Health.up().build();