Skip to content

Commit 31ed042

Browse files
committed
Return 503 when component or instance is down with WebFlux
Closes gh-16109
1 parent 8d033e7 commit 31ed042

File tree

3 files changed

+149
-11
lines changed

3 files changed

+149
-11
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -120,6 +120,10 @@ public CompositeReactiveHealthIndicator timeoutStrategy(long timeout,
120120
return this;
121121
}
122122

123+
ReactiveHealthIndicatorRegistry getRegistry() {
124+
return this.registry;
125+
}
126+
123127
@Override
124128
public Mono<Health> health() {
125129
return Flux.fromIterable(this.registry.getAll().entrySet())

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.boot.actuate.endpoint.SecurityContext;
2222
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
23+
import org.springframework.boot.actuate.endpoint.annotation.Selector;
2324
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
2425
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
2526

@@ -48,10 +49,46 @@ public Mono<WebEndpointResponse<Health>> health(SecurityContext securityContext)
4849
.map((health) -> this.responseMapper.map(health, securityContext));
4950
}
5051

52+
@ReadOperation
53+
public Mono<WebEndpointResponse<Health>> healthForComponent(
54+
SecurityContext securityContext, @Selector String component) {
55+
return responseFromIndicator(getNestedHealthIndicator(this.delegate, component),
56+
securityContext);
57+
}
58+
59+
@ReadOperation
60+
public Mono<WebEndpointResponse<Health>> healthForComponentInstance(
61+
SecurityContext securityContext, @Selector String component,
62+
@Selector String instance) {
63+
ReactiveHealthIndicator indicator = getNestedHealthIndicator(this.delegate,
64+
component);
65+
if (indicator != null) {
66+
indicator = getNestedHealthIndicator(indicator, instance);
67+
}
68+
return responseFromIndicator(indicator, securityContext);
69+
}
70+
5171
public Mono<WebEndpointResponse<Health>> health(SecurityContext securityContext,
5272
ShowDetails showDetails) {
5373
return this.delegate.health().map((health) -> this.responseMapper.map(health,
5474
securityContext, showDetails));
5575
}
5676

77+
private Mono<WebEndpointResponse<Health>> responseFromIndicator(
78+
ReactiveHealthIndicator indicator, SecurityContext securityContext) {
79+
return (indicator != null)
80+
? indicator.health()
81+
.map((health) -> this.responseMapper.map(health, securityContext))
82+
: Mono.empty();
83+
}
84+
85+
private ReactiveHealthIndicator getNestedHealthIndicator(
86+
ReactiveHealthIndicator healthIndicator, String name) {
87+
if (healthIndicator instanceof CompositeReactiveHealthIndicator) {
88+
return ((CompositeReactiveHealthIndicator) healthIndicator).getRegistry()
89+
.get(name);
90+
}
91+
return null;
92+
}
93+
5794
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2018 the original author or authors.
2+
* Copyright 2012-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,13 +17,20 @@
1717
package org.springframework.boot.actuate.health;
1818

1919
import java.util.Arrays;
20+
import java.util.Collections;
2021
import java.util.HashSet;
2122
import java.util.Map;
23+
import java.util.concurrent.Callable;
24+
import java.util.function.Consumer;
2225

2326
import org.junit.Test;
2427
import org.junit.runner.RunWith;
28+
import reactor.core.publisher.Mono;
2529

30+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
2631
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointRunners;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
33+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
2734
import org.springframework.context.ConfigurableApplicationContext;
2835
import org.springframework.context.annotation.Bean;
2936
import org.springframework.context.annotation.Configuration;
@@ -51,23 +58,87 @@ public void whenHealthIsUp200ResponseIsReturned() {
5158
}
5259

5360
@Test
54-
public void whenHealthIsDown503ResponseIsReturned() {
61+
public void whenHealthIsDown503ResponseIsReturned() throws Exception {
62+
withHealthIndicator("charlie", () -> Health.down().build(),
63+
() -> Mono.just(Health.down().build()), () -> {
64+
client.get().uri("/actuator/health").exchange().expectStatus()
65+
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody()
66+
.jsonPath("status").isEqualTo("DOWN")
67+
.jsonPath("details.alpha.status").isEqualTo("UP")
68+
.jsonPath("details.bravo.status").isEqualTo("UP")
69+
.jsonPath("details.charlie.status").isEqualTo("DOWN");
70+
return null;
71+
});
72+
}
73+
74+
@Test
75+
public void whenComponentHealthIsDown503ResponseIsReturned() throws Exception {
76+
withHealthIndicator("charlie", () -> Health.down().build(),
77+
() -> Mono.just(Health.down().build()), () -> {
78+
client.get().uri("/actuator/health/charlie").exchange().expectStatus()
79+
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody()
80+
.jsonPath("status").isEqualTo("DOWN");
81+
return null;
82+
});
83+
}
84+
85+
@Test
86+
public void whenComponentInstanceHealthIsDown503ResponseIsReturned()
87+
throws Exception {
88+
CompositeHealthIndicator composite = new CompositeHealthIndicator(
89+
new OrderedHealthAggregator(),
90+
Collections.singletonMap("one", () -> Health.down().build()));
91+
CompositeReactiveHealthIndicator reactiveComposite = new CompositeReactiveHealthIndicator(
92+
new OrderedHealthAggregator(),
93+
new DefaultReactiveHealthIndicatorRegistry(Collections.singletonMap("one",
94+
() -> Mono.just(Health.down().build()))));
95+
withHealthIndicator("charlie", composite, reactiveComposite, () -> {
96+
client.get().uri("/actuator/health/charlie/one").exchange().expectStatus()
97+
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody()
98+
.jsonPath("status").isEqualTo("DOWN");
99+
return null;
100+
});
101+
}
102+
103+
private void withHealthIndicator(String name, HealthIndicator healthIndicator,
104+
ReactiveHealthIndicator reactiveHealthIndicator, Callable<Void> action)
105+
throws Exception {
106+
Consumer<String> unregister;
107+
Consumer<String> reactiveUnregister;
108+
try {
109+
ReactiveHealthIndicatorRegistry registry = context
110+
.getBean(ReactiveHealthIndicatorRegistry.class);
111+
registry.register(name, reactiveHealthIndicator);
112+
reactiveUnregister = registry::unregister;
113+
}
114+
catch (NoSuchBeanDefinitionException ex) {
115+
reactiveUnregister = (indicatorName) -> {
116+
};
117+
// Continue
118+
}
55119
HealthIndicatorRegistry registry = context.getBean(HealthIndicatorRegistry.class);
56-
registry.register("charlie", () -> Health.down().build());
120+
registry.register(name, healthIndicator);
121+
unregister = reactiveUnregister.andThen(registry::unregister);
57122
try {
58-
client.get().uri("/actuator/health").exchange().expectStatus()
59-
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody()
60-
.jsonPath("status").isEqualTo("DOWN").jsonPath("details.alpha.status")
61-
.isEqualTo("UP").jsonPath("details.bravo.status").isEqualTo("UP")
62-
.jsonPath("details.charlie.status").isEqualTo("DOWN");
123+
action.call();
63124
}
64125
finally {
65-
registry.unregister("charlie");
126+
unregister.accept("charlie");
66127
}
67128
}
68129

69130
@Test
70131
public void whenHealthIndicatorIsRemovedResponseIsAltered() {
132+
Consumer<String> reactiveRegister = null;
133+
try {
134+
ReactiveHealthIndicatorRegistry registry = context
135+
.getBean(ReactiveHealthIndicatorRegistry.class);
136+
ReactiveHealthIndicator unregistered = registry.unregister("bravo");
137+
reactiveRegister = (name) -> registry.register(name, unregistered);
138+
}
139+
catch (NoSuchBeanDefinitionException ex) {
140+
// Continue
141+
}
71142
HealthIndicatorRegistry registry = context.getBean(HealthIndicatorRegistry.class);
72143
HealthIndicator bravo = registry.unregister("bravo");
73144
try {
@@ -78,6 +149,9 @@ public void whenHealthIndicatorIsRemovedResponseIsAltered() {
78149
}
79150
finally {
80151
registry.register("bravo", bravo);
152+
if (reactiveRegister != null) {
153+
reactiveRegister.accept("bravo");
154+
}
81155
}
82156
}
83157

@@ -91,13 +165,24 @@ public HealthIndicatorRegistry healthIndicatorFactory(
91165
.createHealthIndicatorRegistry(healthIndicators);
92166
}
93167

168+
@Bean
169+
@ConditionalOnWebApplication(type = Type.REACTIVE)
170+
public ReactiveHealthIndicatorRegistry reactiveHealthIndicatorRegistry(
171+
Map<String, ReactiveHealthIndicator> reactiveHealthIndicators,
172+
Map<String, HealthIndicator> healthIndicators) {
173+
return new ReactiveHealthIndicatorRegistryFactory()
174+
.createReactiveHealthIndicatorRegistry(reactiveHealthIndicators,
175+
healthIndicators);
176+
}
177+
94178
@Bean
95179
public HealthEndpoint healthEndpoint(HealthIndicatorRegistry registry) {
96180
return new HealthEndpoint(new CompositeHealthIndicator(
97181
new OrderedHealthAggregator(), registry));
98182
}
99183

100184
@Bean
185+
@ConditionalOnWebApplication(type = Type.SERVLET)
101186
public HealthEndpointWebExtension healthWebEndpointExtension(
102187
HealthEndpoint healthEndpoint) {
103188
return new HealthEndpointWebExtension(healthEndpoint,
@@ -106,6 +191,18 @@ public HealthEndpointWebExtension healthWebEndpointExtension(
106191
new HashSet<>(Arrays.asList("ACTUATOR"))));
107192
}
108193

194+
@Bean
195+
@ConditionalOnWebApplication(type = Type.REACTIVE)
196+
public ReactiveHealthEndpointWebExtension reactiveHealthWebEndpointExtension(
197+
ReactiveHealthIndicatorRegistry registry, HealthEndpoint healthEndpoint) {
198+
return new ReactiveHealthEndpointWebExtension(
199+
new CompositeReactiveHealthIndicator(new OrderedHealthAggregator(),
200+
registry),
201+
new HealthWebEndpointResponseMapper(new HealthStatusHttpMapper(),
202+
ShowDetails.ALWAYS,
203+
new HashSet<>(Arrays.asList("ACTUATOR"))));
204+
}
205+
109206
@Bean
110207
public HealthIndicator alphaHealthIndicator() {
111208
return () -> Health.up().build();

0 commit comments

Comments
 (0)