Skip to content

Commit b51b997

Browse files
committed
Run specific health check
This commit improves the `health` endpoint to run health check for a particular component or, if that component is itself a composite, an instance of that component. Concretely, it is now possible to issue a `GET` on `/actuator/health/{component}` and `/actuator/health/{component}/instance` to retrieve the health of a component or an instance of a composite component, respectively. If details cannot be showed for the current user, any request leads to a 404 and does not invoke the health check at all. Closes gh-8865
1 parent 9f6d3bb commit b51b997

File tree

12 files changed

+681
-23
lines changed

12 files changed

+681
-23
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
[[health]]
22
= Health (`health`)
3-
43
The `health` endpoint provides detailed information about the health of the application.
54

65

76

87
[[health-retrieving]]
9-
== Retrieving the Health
10-
8+
== Retrieving the Health of the application
119
To retrieve the health of the application, make a `GET` request to `/actuator/health`,
1210
as shown in the following curl-based example:
1311

@@ -21,9 +19,56 @@ include::{snippets}health/http-response.adoc[]
2119

2220
[[health-retrieving-response-structure]]
2321
=== Response Structure
24-
2522
The response contains details of the health of the application. The following table
2623
describes the structure of the response:
2724

2825
[cols="2,1,3"]
2926
include::{snippets}health/response-fields.adoc[]
27+
28+
29+
30+
[[health-retrieving-component]]
31+
== Retrieving the Health of a component
32+
To retrieve the health of a particular component of the application, make a `GET` request
33+
to `/actuator/health/{component}`, as shown in the following curl-based example:
34+
35+
include::{snippets}health/component/curl-request.adoc[]
36+
37+
The resulting response is similar to the following:
38+
39+
include::{snippets}health/component/http-response.adoc[]
40+
41+
42+
43+
[[health-retrieving-component-response-structure]]
44+
=== Response Structure
45+
The response contains details of the health of a particular component of the application.
46+
The following table describes the structure of the response:
47+
48+
[cols="2,1,3"]
49+
include::{snippets}health/component/response-fields.adoc[]
50+
51+
52+
53+
[[health-retrieving-component-instance]]
54+
== Retrieving the Health of a component instance
55+
If a particular component consists of multiple instances (as the `broker` indicator in
56+
the example above), the health of a particular instance of that component can be retrieved
57+
by issuing a `GET` request to `/actuator/health/{component}/{instance}`, as shown in the
58+
following curl-based example:
59+
60+
include::{snippets}health/instance/curl-request.adoc[]
61+
62+
The resulting response is similar to the following:
63+
64+
include::{snippets}health/instance/http-response.adoc[]
65+
66+
67+
68+
[[health-retrieving-component-instance-response-structure]]
69+
=== Response Structure
70+
The response contains details of the health of an instance of a particular component of
71+
the application. The following table describes the structure of the response:
72+
73+
[cols="2,1,3"]
74+
include::{snippets}health/instance/response-fields.adoc[]

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void getEndpointsShouldAddCloudFoundryHealthExtension() {
5858
assertThat(endpoints.size()).isEqualTo(2);
5959
for (ExposableWebEndpoint endpoint : endpoints) {
6060
if (endpoint.getId().equals("health")) {
61-
WebOperation operation = endpoint.getOperations().iterator().next();
61+
WebOperation operation = findMainReadOperation(endpoint);
6262
assertThat(operation.invoke(new InvocationContext(
6363
mock(SecurityContext.class), Collections.emptyMap())))
6464
.isEqualTo("cf");
@@ -67,6 +67,16 @@ public void getEndpointsShouldAddCloudFoundryHealthExtension() {
6767
});
6868
}
6969

70+
private WebOperation findMainReadOperation(ExposableWebEndpoint endpoint) {
71+
for (WebOperation operation : endpoint.getOperations()) {
72+
if (operation.getRequestPredicate().getPath().equals("health")) {
73+
return operation;
74+
}
75+
}
76+
throw new IllegalStateException("No main read operation found from "
77+
+ endpoint.getOperations());
78+
}
79+
7080
private void load(Class<?> configuration,
7181
Consumer<CloudFoundryWebEndpointDiscoverer> consumer) {
7282
this.load((id) -> null, (id) -> id, configuration, consumer);

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,9 @@ public void healthEndpointInvokerShouldBeCloudFoundryWebExtension() {
284284
Collection<ExposableWebEndpoint> endpoints = getHandlerMapping(
285285
context).getEndpoints();
286286
ExposableWebEndpoint endpoint = endpoints.iterator().next();
287-
WebOperation webOperation = endpoint.getOperations().iterator()
288-
.next();
287+
assertThat(endpoint.getOperations()).hasSize(3);
288+
WebOperation webOperation = findOperationWithRequestPath(endpoint,
289+
"health");
289290
Object invoker = ReflectionTestUtils.getField(webOperation,
290291
"invoker");
291292
assertThat(ReflectionTestUtils.getField(invoker, "target"))
@@ -346,6 +347,17 @@ private CloudFoundryWebFluxEndpointHandlerMapping getHandlerMapping(
346347
CloudFoundryWebFluxEndpointHandlerMapping.class);
347348
}
348349

350+
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint,
351+
String requestPath) {
352+
for (WebOperation operation : endpoint.getOperations()) {
353+
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
354+
return operation;
355+
}
356+
}
357+
throw new IllegalStateException("No operation found with request path "
358+
+ requestPath + " from " + endpoint.getOperations());
359+
}
360+
349361
@Configuration
350362
static class TestConfiguration {
351363

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,9 @@ public void healthEndpointInvokerShouldBeCloudFoundryWebExtension() {
279279
CloudFoundryWebEndpointServletHandlerMapping.class)
280280
.getEndpoints();
281281
ExposableWebEndpoint endpoint = endpoints.iterator().next();
282-
WebOperation webOperation = endpoint.getOperations().iterator()
283-
.next();
282+
assertThat(endpoint.getOperations()).hasSize(3);
283+
WebOperation webOperation = findOperationWithRequestPath(endpoint,
284+
"health");
284285
Object invoker = ReflectionTestUtils.getField(webOperation,
285286
"invoker");
286287
assertThat(ReflectionTestUtils.getField(invoker, "target"))
@@ -294,6 +295,17 @@ private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping(
294295
CloudFoundryWebEndpointServletHandlerMapping.class);
295296
}
296297

298+
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint,
299+
String requestPath) {
300+
for (WebOperation operation : endpoint.getOperations()) {
301+
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
302+
return operation;
303+
}
304+
}
305+
throw new IllegalStateException("No operation found with request path "
306+
+ requestPath + " from " + endpoint.getOperations());
307+
}
308+
297309
@Configuration
298310
static class TestConfiguration {
299311

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
1818

1919
import java.io.File;
20+
import java.util.Arrays;
21+
import java.util.LinkedHashMap;
22+
import java.util.List;
2023
import java.util.Map;
2124

2225
import javax.sql.DataSource;
2326

2427
import org.junit.Test;
2528

2629
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
30+
import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry;
31+
import org.springframework.boot.actuate.health.Health;
2732
import org.springframework.boot.actuate.health.HealthEndpoint;
2833
import org.springframework.boot.actuate.health.HealthIndicator;
2934
import org.springframework.boot.actuate.health.HealthIndicatorRegistryFactory;
@@ -35,6 +40,7 @@
3540
import org.springframework.context.annotation.Bean;
3641
import org.springframework.context.annotation.Configuration;
3742
import org.springframework.context.annotation.Import;
43+
import org.springframework.restdocs.payload.FieldDescriptor;
3844

3945
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
4046
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
@@ -47,9 +53,17 @@
4753
* Tests for generating documentation describing the {@link HealthEndpoint}.
4854
*
4955
* @author Andy Wilkinson
56+
* @author Stephane Nicoll
5057
*/
5158
public class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
5259

60+
private static final List<FieldDescriptor> componentFields = Arrays.asList(
61+
fieldWithPath("status")
62+
.description("Status of a specific part of the application"),
63+
subsectionWithPath("details").description(
64+
"Details of the health of a specific part of the"
65+
+ " application."));
66+
5367
@Test
5468
public void health() throws Exception {
5569
this.mockMvc.perform(get("/actuator/health")).andExpect(status().isOk())
@@ -66,6 +80,19 @@ public void health() throws Exception {
6680
+ " application."))));
6781
}
6882

83+
@Test
84+
public void healthComponent() throws Exception {
85+
this.mockMvc.perform(get("/actuator/health/db")).andExpect(status().isOk())
86+
.andDo(document("health/component", responseFields(componentFields)));
87+
}
88+
89+
@Test
90+
public void healthComponentInstance() throws Exception {
91+
this.mockMvc.perform(get("/actuator/health/broker/us1"))
92+
.andExpect(status().isOk())
93+
.andDo(document("health/instance", responseFields(componentFields)));
94+
}
95+
6996
@Configuration
7097
@Import(BaseDocumentationConfiguration.class)
7198
@ImportAutoConfiguration(DataSourceAutoConfiguration.class)
@@ -84,11 +111,22 @@ public DiskSpaceHealthIndicator diskSpaceHealthIndicator() {
84111
}
85112

86113
@Bean
87-
public DataSourceHealthIndicator dataSourceHealthIndicator(
114+
public DataSourceHealthIndicator dbHealthIndicator(
88115
DataSource dataSource) {
89116
return new DataSourceHealthIndicator(dataSource);
90117
}
91118

119+
@Bean
120+
public CompositeHealthIndicator brokerHealthIndicator() {
121+
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
122+
indicators.put("us1", () -> Health.up().withDetail("version", "1.0.2")
123+
.build());
124+
indicators.put("us2", () -> Health.up().withDetail("version", "1.0.4")
125+
.build());
126+
return new CompositeHealthIndicator(new OrderedHealthAggregator(),
127+
new DefaultHealthIndicatorRegistry(indicators));
128+
}
129+
92130
}
93131

94132
}

0 commit comments

Comments
 (0)