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
This commit is contained in:
parent
9f6d3bb21d
commit
b51b997b70
|
@ -1,13 +1,11 @@
|
||||||
[[health]]
|
[[health]]
|
||||||
= Health (`health`)
|
= Health (`health`)
|
||||||
|
|
||||||
The `health` endpoint provides detailed information about the health of the application.
|
The `health` endpoint provides detailed information about the health of the application.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[health-retrieving]]
|
[[health-retrieving]]
|
||||||
== Retrieving the Health
|
== Retrieving the Health of the application
|
||||||
|
|
||||||
To retrieve the health of the application, make a `GET` request to `/actuator/health`,
|
To retrieve the health of the application, make a `GET` request to `/actuator/health`,
|
||||||
as shown in the following curl-based example:
|
as shown in the following curl-based example:
|
||||||
|
|
||||||
|
@ -21,9 +19,56 @@ include::{snippets}health/http-response.adoc[]
|
||||||
|
|
||||||
[[health-retrieving-response-structure]]
|
[[health-retrieving-response-structure]]
|
||||||
=== Response Structure
|
=== Response Structure
|
||||||
|
|
||||||
The response contains details of the health of the application. The following table
|
The response contains details of the health of the application. The following table
|
||||||
describes the structure of the response:
|
describes the structure of the response:
|
||||||
|
|
||||||
[cols="2,1,3"]
|
[cols="2,1,3"]
|
||||||
include::{snippets}health/response-fields.adoc[]
|
include::{snippets}health/response-fields.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[health-retrieving-component]]
|
||||||
|
== Retrieving the Health of a component
|
||||||
|
To retrieve the health of a particular component of the application, make a `GET` request
|
||||||
|
to `/actuator/health/{component}`, as shown in the following curl-based example:
|
||||||
|
|
||||||
|
include::{snippets}health/component/curl-request.adoc[]
|
||||||
|
|
||||||
|
The resulting response is similar to the following:
|
||||||
|
|
||||||
|
include::{snippets}health/component/http-response.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[health-retrieving-component-response-structure]]
|
||||||
|
=== Response Structure
|
||||||
|
The response contains details of the health of a particular component of the application.
|
||||||
|
The following table describes the structure of the response:
|
||||||
|
|
||||||
|
[cols="2,1,3"]
|
||||||
|
include::{snippets}health/component/response-fields.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[health-retrieving-component-instance]]
|
||||||
|
== Retrieving the Health of a component instance
|
||||||
|
If a particular component consists of multiple instances (as the `broker` indicator in
|
||||||
|
the example above), the health of a particular instance of that component can be retrieved
|
||||||
|
by issuing a `GET` request to `/actuator/health/{component}/{instance}`, as shown in the
|
||||||
|
following curl-based example:
|
||||||
|
|
||||||
|
include::{snippets}health/instance/curl-request.adoc[]
|
||||||
|
|
||||||
|
The resulting response is similar to the following:
|
||||||
|
|
||||||
|
include::{snippets}health/instance/http-response.adoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[health-retrieving-component-instance-response-structure]]
|
||||||
|
=== Response Structure
|
||||||
|
The response contains details of the health of an instance of a particular component of
|
||||||
|
the application. The following table describes the structure of the response:
|
||||||
|
|
||||||
|
[cols="2,1,3"]
|
||||||
|
include::{snippets}health/instance/response-fields.adoc[]
|
||||||
|
|
|
@ -58,7 +58,7 @@ public class CloudFoundryWebEndpointDiscovererTests {
|
||||||
assertThat(endpoints.size()).isEqualTo(2);
|
assertThat(endpoints.size()).isEqualTo(2);
|
||||||
for (ExposableWebEndpoint endpoint : endpoints) {
|
for (ExposableWebEndpoint endpoint : endpoints) {
|
||||||
if (endpoint.getId().equals("health")) {
|
if (endpoint.getId().equals("health")) {
|
||||||
WebOperation operation = endpoint.getOperations().iterator().next();
|
WebOperation operation = findMainReadOperation(endpoint);
|
||||||
assertThat(operation.invoke(new InvocationContext(
|
assertThat(operation.invoke(new InvocationContext(
|
||||||
mock(SecurityContext.class), Collections.emptyMap())))
|
mock(SecurityContext.class), Collections.emptyMap())))
|
||||||
.isEqualTo("cf");
|
.isEqualTo("cf");
|
||||||
|
@ -67,6 +67,16 @@ public class CloudFoundryWebEndpointDiscovererTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WebOperation findMainReadOperation(ExposableWebEndpoint endpoint) {
|
||||||
|
for (WebOperation operation : endpoint.getOperations()) {
|
||||||
|
if (operation.getRequestPredicate().getPath().equals("health")) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No main read operation found from "
|
||||||
|
+ endpoint.getOperations());
|
||||||
|
}
|
||||||
|
|
||||||
private void load(Class<?> configuration,
|
private void load(Class<?> configuration,
|
||||||
Consumer<CloudFoundryWebEndpointDiscoverer> consumer) {
|
Consumer<CloudFoundryWebEndpointDiscoverer> consumer) {
|
||||||
this.load((id) -> null, (id) -> id, configuration, consumer);
|
this.load((id) -> null, (id) -> id, configuration, consumer);
|
||||||
|
|
|
@ -284,8 +284,9 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
||||||
Collection<ExposableWebEndpoint> endpoints = getHandlerMapping(
|
Collection<ExposableWebEndpoint> endpoints = getHandlerMapping(
|
||||||
context).getEndpoints();
|
context).getEndpoints();
|
||||||
ExposableWebEndpoint endpoint = endpoints.iterator().next();
|
ExposableWebEndpoint endpoint = endpoints.iterator().next();
|
||||||
WebOperation webOperation = endpoint.getOperations().iterator()
|
assertThat(endpoint.getOperations()).hasSize(3);
|
||||||
.next();
|
WebOperation webOperation = findOperationWithRequestPath(endpoint,
|
||||||
|
"health");
|
||||||
Object invoker = ReflectionTestUtils.getField(webOperation,
|
Object invoker = ReflectionTestUtils.getField(webOperation,
|
||||||
"invoker");
|
"invoker");
|
||||||
assertThat(ReflectionTestUtils.getField(invoker, "target"))
|
assertThat(ReflectionTestUtils.getField(invoker, "target"))
|
||||||
|
@ -346,6 +347,17 @@ public class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
||||||
CloudFoundryWebFluxEndpointHandlerMapping.class);
|
CloudFoundryWebFluxEndpointHandlerMapping.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint,
|
||||||
|
String requestPath) {
|
||||||
|
for (WebOperation operation : endpoint.getOperations()) {
|
||||||
|
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No operation found with request path "
|
||||||
|
+ requestPath + " from " + endpoint.getOperations());
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
static class TestConfiguration {
|
static class TestConfiguration {
|
||||||
|
|
||||||
|
|
|
@ -279,8 +279,9 @@ public class CloudFoundryActuatorAutoConfigurationTests {
|
||||||
CloudFoundryWebEndpointServletHandlerMapping.class)
|
CloudFoundryWebEndpointServletHandlerMapping.class)
|
||||||
.getEndpoints();
|
.getEndpoints();
|
||||||
ExposableWebEndpoint endpoint = endpoints.iterator().next();
|
ExposableWebEndpoint endpoint = endpoints.iterator().next();
|
||||||
WebOperation webOperation = endpoint.getOperations().iterator()
|
assertThat(endpoint.getOperations()).hasSize(3);
|
||||||
.next();
|
WebOperation webOperation = findOperationWithRequestPath(endpoint,
|
||||||
|
"health");
|
||||||
Object invoker = ReflectionTestUtils.getField(webOperation,
|
Object invoker = ReflectionTestUtils.getField(webOperation,
|
||||||
"invoker");
|
"invoker");
|
||||||
assertThat(ReflectionTestUtils.getField(invoker, "target"))
|
assertThat(ReflectionTestUtils.getField(invoker, "target"))
|
||||||
|
@ -294,6 +295,17 @@ public class CloudFoundryActuatorAutoConfigurationTests {
|
||||||
CloudFoundryWebEndpointServletHandlerMapping.class);
|
CloudFoundryWebEndpointServletHandlerMapping.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint,
|
||||||
|
String requestPath) {
|
||||||
|
for (WebOperation operation : endpoint.getOperations()) {
|
||||||
|
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("No operation found with request path "
|
||||||
|
+ requestPath + " from " + endpoint.getOperations());
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
static class TestConfiguration {
|
static class TestConfiguration {
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
|
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
@ -24,6 +27,8 @@ import javax.sql.DataSource;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
|
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
|
||||||
|
import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry;
|
||||||
|
import org.springframework.boot.actuate.health.Health;
|
||||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||||
import org.springframework.boot.actuate.health.HealthIndicator;
|
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||||
import org.springframework.boot.actuate.health.HealthIndicatorRegistryFactory;
|
import org.springframework.boot.actuate.health.HealthIndicatorRegistryFactory;
|
||||||
|
@ -35,6 +40,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||||
|
|
||||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||||
|
@ -47,9 +53,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||||
* Tests for generating documentation describing the {@link HealthEndpoint}.
|
* Tests for generating documentation describing the {@link HealthEndpoint}.
|
||||||
*
|
*
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Stephane Nicoll
|
||||||
*/
|
*/
|
||||||
public class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
public class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
|
||||||
|
|
||||||
|
private static final List<FieldDescriptor> componentFields = Arrays.asList(
|
||||||
|
fieldWithPath("status")
|
||||||
|
.description("Status of a specific part of the application"),
|
||||||
|
subsectionWithPath("details").description(
|
||||||
|
"Details of the health of a specific part of the"
|
||||||
|
+ " application."));
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void health() throws Exception {
|
public void health() throws Exception {
|
||||||
this.mockMvc.perform(get("/actuator/health")).andExpect(status().isOk())
|
this.mockMvc.perform(get("/actuator/health")).andExpect(status().isOk())
|
||||||
|
@ -66,6 +80,19 @@ public class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentati
|
||||||
+ " application."))));
|
+ " application."))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void healthComponent() throws Exception {
|
||||||
|
this.mockMvc.perform(get("/actuator/health/db")).andExpect(status().isOk())
|
||||||
|
.andDo(document("health/component", responseFields(componentFields)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void healthComponentInstance() throws Exception {
|
||||||
|
this.mockMvc.perform(get("/actuator/health/broker/us1"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andDo(document("health/instance", responseFields(componentFields)));
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Import(BaseDocumentationConfiguration.class)
|
@Import(BaseDocumentationConfiguration.class)
|
||||||
@ImportAutoConfiguration(DataSourceAutoConfiguration.class)
|
@ImportAutoConfiguration(DataSourceAutoConfiguration.class)
|
||||||
|
@ -84,11 +111,22 @@ public class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentati
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DataSourceHealthIndicator dataSourceHealthIndicator(
|
public DataSourceHealthIndicator dbHealthIndicator(
|
||||||
DataSource dataSource) {
|
DataSource dataSource) {
|
||||||
return new DataSourceHealthIndicator(dataSource);
|
return new DataSourceHealthIndicator(dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CompositeHealthIndicator brokerHealthIndicator() {
|
||||||
|
Map<String, HealthIndicator> indicators = new LinkedHashMap<>();
|
||||||
|
indicators.put("us1", () -> Health.up().withDetail("version", "1.0.2")
|
||||||
|
.build());
|
||||||
|
indicators.put("us2", () -> Health.up().withDetail("version", "1.0.4")
|
||||||
|
.build());
|
||||||
|
return new CompositeHealthIndicator(new OrderedHealthAggregator(),
|
||||||
|
new DefaultHealthIndicatorRegistry(indicators));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,29 @@
|
||||||
package org.springframework.boot.actuate.autoconfigure.health;
|
package org.springframework.boot.actuate.autoconfigure.health;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
|
import org.springframework.boot.actuate.health.CompositeHealthIndicator;
|
||||||
|
import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry;
|
||||||
import org.springframework.boot.actuate.health.Health;
|
import org.springframework.boot.actuate.health.Health;
|
||||||
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
|
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
|
||||||
|
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||||
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
|
import org.springframework.boot.actuate.health.HealthWebEndpointResponseMapper;
|
||||||
|
import org.springframework.boot.actuate.health.OrderedHealthAggregator;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.entry;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
@ -42,6 +53,7 @@ import static org.mockito.Mockito.mock;
|
||||||
public class HealthEndpointWebExtensionTests {
|
public class HealthEndpointWebExtensionTests {
|
||||||
|
|
||||||
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||||
|
.withUserConfiguration(HealthIndicatorsConfiguration.class)
|
||||||
.withConfiguration(
|
.withConfiguration(
|
||||||
AutoConfigurations.of(HealthIndicatorAutoConfiguration.class,
|
AutoConfigurations.of(HealthIndicatorAutoConfiguration.class,
|
||||||
HealthEndpointAutoConfiguration.class));
|
HealthEndpointAutoConfiguration.class));
|
||||||
|
@ -84,7 +96,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
this.contextRunner.run((context) -> {
|
this.contextRunner.run((context) -> {
|
||||||
HealthEndpointWebExtension extension = context
|
HealthEndpointWebExtension extension = context
|
||||||
.getBean(HealthEndpointWebExtension.class);
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
assertThat(extension.getHealth(mock(SecurityContext.class)).getBody()
|
assertThat(extension.health(mock(SecurityContext.class)).getBody()
|
||||||
.getDetails()).isEmpty();
|
.getDetails()).isEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -96,7 +108,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.getBean(HealthEndpointWebExtension.class);
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
SecurityContext securityContext = mock(SecurityContext.class);
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
|
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
|
||||||
assertThat(extension.getHealth(securityContext).getBody().getDetails())
|
assertThat(extension.health(securityContext).getBody().getDetails())
|
||||||
.isEmpty();
|
.isEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -113,7 +125,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
given(securityContext.getPrincipal())
|
given(securityContext.getPrincipal())
|
||||||
.willReturn(mock(Principal.class));
|
.willReturn(mock(Principal.class));
|
||||||
assertThat(
|
assertThat(
|
||||||
extension.getHealth(securityContext).getBody().getDetails())
|
extension.health(securityContext).getBody().getDetails())
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -125,7 +137,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.run((context) -> {
|
.run((context) -> {
|
||||||
HealthEndpointWebExtension extension = context
|
HealthEndpointWebExtension extension = context
|
||||||
.getBean(HealthEndpointWebExtension.class);
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
assertThat(extension.getHealth(null).getBody().getDetails())
|
assertThat(extension.health(null).getBody().getDetails())
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -137,7 +149,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.run((context) -> {
|
.run((context) -> {
|
||||||
HealthEndpointWebExtension extension = context
|
HealthEndpointWebExtension extension = context
|
||||||
.getBean(HealthEndpointWebExtension.class);
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
assertThat(extension.getHealth(mock(SecurityContext.class)).getBody()
|
assertThat(extension.health(mock(SecurityContext.class)).getBody()
|
||||||
.getDetails()).isEmpty();
|
.getDetails()).isEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -154,7 +166,7 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.willReturn(mock(Principal.class));
|
.willReturn(mock(Principal.class));
|
||||||
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
|
||||||
assertThat(
|
assertThat(
|
||||||
extension.getHealth(securityContext).getBody().getDetails())
|
extension.health(securityContext).getBody().getDetails())
|
||||||
.isEmpty();
|
.isEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -171,11 +183,233 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.willReturn(mock(Principal.class));
|
.willReturn(mock(Principal.class));
|
||||||
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
|
||||||
assertThat(
|
assertThat(
|
||||||
extension.getHealth(securityContext).getBody().getDetails())
|
extension.health(securityContext).getBody().getDetails())
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthenticatedUsersAreNotShownComponentByDefault() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponent(
|
||||||
|
mock(SecurityContext.class), "simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticatedUsersAreNotShownComponentByDefault() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
|
||||||
|
assertDetailsNotFound(extension.healthForComponent(securityContext,
|
||||||
|
"simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticatedUsersWhenAuthorizedCanBeShownComponent() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
assertSimpleComponent(extension.healthForComponent(
|
||||||
|
securityContext, "simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthenticatedUsersCanBeShownComponent() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=always")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertSimpleComponent(extension.healthForComponent(null, "simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentCanBeHiddenFromAuthenticatedUsers() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=never")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponent(
|
||||||
|
mock(SecurityContext.class), "simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentCanBeHiddenFromUnauthorizedUsers() {
|
||||||
|
this.contextRunner.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized",
|
||||||
|
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
|
||||||
|
assertDetailsNotFound(extension.healthForComponent(securityContext,
|
||||||
|
"simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentCanBeShownToAuthorizedUsers() {
|
||||||
|
this.contextRunner.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized",
|
||||||
|
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
|
||||||
|
assertSimpleComponent(extension.healthForComponent(securityContext,
|
||||||
|
"simple"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentThatDoesNotExistMapTo404() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=always")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponent(null,
|
||||||
|
"does-not-exist"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthenticatedUsersAreNotShownComponentInstanceByDefault() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponentInstance(
|
||||||
|
mock(SecurityContext.class), "composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticatedUsersAreNotShownComponentInstanceByDefault() {
|
||||||
|
this.contextRunner.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal()).willReturn(mock(Principal.class));
|
||||||
|
assertDetailsNotFound(extension.healthForComponentInstance(securityContext,
|
||||||
|
"composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticatedUsersWhenAuthorizedCanBeShownComponentInstance() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
assertSimpleComponent(extension.healthForComponentInstance(
|
||||||
|
securityContext, "composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unauthenticatedUsersCanBeShownComponentInstance() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=always")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertSimpleComponent(extension.healthForComponentInstance(null,
|
||||||
|
"composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentInstanceCanBeHiddenFromAuthenticatedUsers() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=never")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponentInstance(
|
||||||
|
mock(SecurityContext.class), "composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentInstanceCanBeHiddenFromUnauthorizedUsers() {
|
||||||
|
this.contextRunner.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized",
|
||||||
|
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(false);
|
||||||
|
assertDetailsNotFound(extension.healthForComponentInstance(securityContext,
|
||||||
|
"composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentInstanceCanBeShownToAuthorizedUsers() {
|
||||||
|
this.contextRunner.withPropertyValues(
|
||||||
|
"management.endpoint.health.show-details=when-authorized",
|
||||||
|
"management.endpoint.health.roles=ACTUATOR").run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
given(securityContext.isUserInRole("ACTUATOR")).willReturn(true);
|
||||||
|
assertSimpleComponent(extension.healthForComponentInstance(securityContext,
|
||||||
|
"composite", "one"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void componentInstanceThatDoesNotExistMapTo404() {
|
||||||
|
this.contextRunner
|
||||||
|
.withPropertyValues("management.endpoint.health.show-details=always")
|
||||||
|
.run((context) -> {
|
||||||
|
HealthEndpointWebExtension extension = context
|
||||||
|
.getBean(HealthEndpointWebExtension.class);
|
||||||
|
assertDetailsNotFound(extension.healthForComponentInstance(null,
|
||||||
|
"composite", "does-not-exist"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertDetailsNotFound(WebEndpointResponse<?> response) {
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
|
||||||
|
assertThat(response.getBody()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSimpleComponent(WebEndpointResponse<Health> response) {
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||||
|
assertThat(response.getBody().getDetails()).containsOnly(
|
||||||
|
entry("counter", 42));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void roleCanBeCustomized() {
|
public void roleCanBeCustomized() {
|
||||||
this.contextRunner.withPropertyValues(
|
this.contextRunner.withPropertyValues(
|
||||||
|
@ -188,9 +422,28 @@ public class HealthEndpointWebExtensionTests {
|
||||||
.willReturn(mock(Principal.class));
|
.willReturn(mock(Principal.class));
|
||||||
given(securityContext.isUserInRole("ADMIN")).willReturn(true);
|
given(securityContext.isUserInRole("ADMIN")).willReturn(true);
|
||||||
assertThat(
|
assertThat(
|
||||||
extension.getHealth(securityContext).getBody().getDetails())
|
extension.health(securityContext).getBody().getDetails())
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class HealthIndicatorsConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HealthIndicator simpleHealthIndicator() {
|
||||||
|
return () -> Health.up().withDetail("counter", 42).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public HealthIndicator compositeHealthIndicator() {
|
||||||
|
Map<String, HealthIndicator> nestedIndicators = new HashMap<>();
|
||||||
|
nestedIndicators.put("one", simpleHealthIndicator());
|
||||||
|
nestedIndicators.put("two", () -> Health.up().build());
|
||||||
|
return new CompositeHealthIndicator(new OrderedHealthAggregator(),
|
||||||
|
new DefaultHealthIndicatorRegistry(nestedIndicators));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,15 @@ public class CompositeHealthIndicator implements HealthIndicator {
|
||||||
this.registry.register(name, indicator);
|
this.registry.register(name, indicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link HealthIndicatorRegistry} of this instance.
|
||||||
|
* @return the registry of nested {@link HealthIndicator health indicators}
|
||||||
|
* @since 2.1.0
|
||||||
|
*/
|
||||||
|
public HealthIndicatorRegistry getRegistry() {
|
||||||
|
return this.registry;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Health health() {
|
public Health health() {
|
||||||
Map<String, Health> healths = new LinkedHashMap<>();
|
Map<String, Health> healths = new LinkedHashMap<>();
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.boot.actuate.health;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||||
|
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +27,7 @@ import org.springframework.util.Assert;
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
* @author Christian Dupuis
|
* @author Christian Dupuis
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Stephane Nicoll
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Endpoint(id = "health")
|
@Endpoint(id = "health")
|
||||||
|
@ -48,4 +50,42 @@ public class HealthEndpoint {
|
||||||
return this.healthIndicator.health();
|
return this.healthIndicator.health();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link Health} of a particular component or {@code null} if such
|
||||||
|
* component does not exist.
|
||||||
|
* @param component the name of a particular {@link HealthIndicator}
|
||||||
|
* @return the {@link Health} for the component or {@code null}
|
||||||
|
*/
|
||||||
|
@ReadOperation
|
||||||
|
public Health healthForComponent(@Selector String component) {
|
||||||
|
HealthIndicator indicator = getNestedHealthIndicator(this.healthIndicator,
|
||||||
|
component);
|
||||||
|
return (indicator != null ? indicator.health() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link Health} of a particular {@code instance} managed by the specified
|
||||||
|
* {@code component} or {@code null} if that particular component is not a
|
||||||
|
* {@link CompositeHealthIndicator} or if such instance does not exist.
|
||||||
|
* @param component the name of a particular {@link CompositeHealthIndicator}
|
||||||
|
* @param instance the name of an instance managed by that component
|
||||||
|
* @return the {@link Health} for the component instance of {@code null}
|
||||||
|
*/
|
||||||
|
@ReadOperation
|
||||||
|
public Health healthForComponentInstance(@Selector String component,
|
||||||
|
@Selector String instance) {
|
||||||
|
HealthIndicator indicator = getNestedHealthIndicator(this.healthIndicator,
|
||||||
|
component);
|
||||||
|
HealthIndicator nestedIndicator = getNestedHealthIndicator(indicator, instance);
|
||||||
|
return (nestedIndicator != null ? nestedIndicator.health() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HealthIndicator getNestedHealthIndicator(HealthIndicator healthIndicator,
|
||||||
|
String name) {
|
||||||
|
if (healthIndicator instanceof CompositeHealthIndicator) {
|
||||||
|
return ((CompositeHealthIndicator) healthIndicator).getRegistry().get(name);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,11 @@
|
||||||
|
|
||||||
package org.springframework.boot.actuate.health;
|
package org.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
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.WebEndpointResponse;
|
||||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||||
|
|
||||||
|
@ -30,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExten
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
|
* @author Stephane Nicoll
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@EndpointWebExtension(endpoint = HealthEndpoint.class)
|
@EndpointWebExtension(endpoint = HealthEndpoint.class)
|
||||||
|
@ -46,10 +50,26 @@ public class HealthEndpointWebExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReadOperation
|
@ReadOperation
|
||||||
public WebEndpointResponse<Health> getHealth(SecurityContext securityContext) {
|
public WebEndpointResponse<Health> health(SecurityContext securityContext) {
|
||||||
return this.responseMapper.map(this.delegate.health(), securityContext);
|
return this.responseMapper.map(this.delegate.health(), securityContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ReadOperation
|
||||||
|
public WebEndpointResponse<Health> healthForComponent(SecurityContext securityContext,
|
||||||
|
@Selector String component) {
|
||||||
|
Supplier<Health> health = () -> this.delegate.healthForComponent(component);
|
||||||
|
return this.responseMapper.mapDetails(health, securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReadOperation
|
||||||
|
public WebEndpointResponse<Health> healthForComponentInstance(
|
||||||
|
SecurityContext securityContext, @Selector String component,
|
||||||
|
@Selector String instance) {
|
||||||
|
Supplier<Health> health = () -> this.delegate.healthForComponentInstance(
|
||||||
|
component, instance);
|
||||||
|
return this.responseMapper.mapDetails(health, securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
public WebEndpointResponse<Health> getHealth(SecurityContext securityContext,
|
public WebEndpointResponse<Health> getHealth(SecurityContext securityContext,
|
||||||
ShowDetails showDetails) {
|
ShowDetails showDetails) {
|
||||||
return this.responseMapper.map(this.delegate.health(), securityContext,
|
return this.responseMapper.map(this.delegate.health(), securityContext,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.actuate.health;
|
package org.springframework.boot.actuate.health;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
|
@ -43,6 +44,28 @@ public class HealthWebEndpointResponseMapper {
|
||||||
this.authorizedRoles = authorizedRoles;
|
this.authorizedRoles = authorizedRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given {@code health} details to a {@link WebEndpointResponse}, honouring
|
||||||
|
* the mapper's default {@link ShowDetails} using the given {@code securityContext}.
|
||||||
|
* <p>
|
||||||
|
* If the current user does not have the right to see the details, the
|
||||||
|
* {@link Supplier} is not invoked and a 404 response is returned instead.
|
||||||
|
* @param health the provider of health details, invoked if the current user has the
|
||||||
|
* right to see them
|
||||||
|
* @param securityContext the security context
|
||||||
|
* @return the mapped response
|
||||||
|
*/
|
||||||
|
public WebEndpointResponse<Health> mapDetails(Supplier<Health> health,
|
||||||
|
SecurityContext securityContext) {
|
||||||
|
if (canSeeDetails(securityContext, this.showDetails)) {
|
||||||
|
Health healthDetails = health.get();
|
||||||
|
if (healthDetails != null) {
|
||||||
|
return createWebEndpointResponse(healthDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the
|
* Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the
|
||||||
* mapper's default {@link ShowDetails} using the given {@code securityContext}.
|
* mapper's default {@link ShowDetails} using the given {@code securityContext}.
|
||||||
|
@ -71,10 +94,25 @@ public class HealthWebEndpointResponseMapper {
|
||||||
|| !isUserInRole(securityContext)))) {
|
|| !isUserInRole(securityContext)))) {
|
||||||
health = Health.status(health.getStatus()).build();
|
health = Health.status(health.getStatus()).build();
|
||||||
}
|
}
|
||||||
|
return createWebEndpointResponse(health);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebEndpointResponse<Health> createWebEndpointResponse(Health health) {
|
||||||
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
|
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
|
||||||
return new WebEndpointResponse<>(health, status);
|
return new WebEndpointResponse<>(health, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canSeeDetails(SecurityContext securityContext,
|
||||||
|
ShowDetails showDetails) {
|
||||||
|
if (showDetails == ShowDetails.NEVER
|
||||||
|
|| (showDetails == ShowDetails.WHEN_AUTHORIZED
|
||||||
|
&& (securityContext.getPrincipal() == null
|
||||||
|
|| !isUserInRole(securityContext)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isUserInRole(SecurityContext securityContext) {
|
private boolean isUserInRole(SecurityContext securityContext) {
|
||||||
if (CollectionUtils.isEmpty(this.authorizedRoles)) {
|
if (CollectionUtils.isEmpty(this.authorizedRoles)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.actuate.health;
|
package org.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -30,16 +31,21 @@ import static org.assertj.core.api.Assertions.entry;
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Christian Dupuis
|
* @author Christian Dupuis
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
|
* @author Stephane Nicoll
|
||||||
*/
|
*/
|
||||||
public class HealthEndpointTests {
|
public class HealthEndpointTests {
|
||||||
|
|
||||||
|
private static final HealthIndicator one = () -> new Health.Builder()
|
||||||
|
.status(Status.UP).withDetail("first", "1").build();
|
||||||
|
|
||||||
|
private static final HealthIndicator two = () -> new Health.Builder()
|
||||||
|
.status(Status.UP).withDetail("second", "2").build();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void statusAndFullDetailsAreExposed() {
|
public void statusAndFullDetailsAreExposed() {
|
||||||
Map<String, HealthIndicator> healthIndicators = new HashMap<>();
|
Map<String, HealthIndicator> healthIndicators = new HashMap<>();
|
||||||
healthIndicators.put("up", () -> new Health.Builder().status(Status.UP)
|
healthIndicators.put("up", one);
|
||||||
.withDetail("first", "1").build());
|
healthIndicators.put("upAgain", two);
|
||||||
healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP)
|
|
||||||
.withDetail("second", "2").build());
|
|
||||||
HealthEndpoint endpoint = new HealthEndpoint(
|
HealthEndpoint endpoint = new HealthEndpoint(
|
||||||
createHealthIndicator(healthIndicators));
|
createHealthIndicator(healthIndicators));
|
||||||
Health health = endpoint.health();
|
Health health = endpoint.health();
|
||||||
|
@ -51,6 +57,56 @@ public class HealthEndpointTests {
|
||||||
assertThat(upAgainHealth.getDetails()).containsOnly(entry("second", "2"));
|
assertThat(upAgainHealth.getDetails()).containsOnly(entry("second", "2"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusForComponentIsExposed() {
|
||||||
|
HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator(
|
||||||
|
Collections.singletonMap("test", one)));
|
||||||
|
Health health = endpoint.healthForComponent("test");
|
||||||
|
assertThat(health).isNotNull();
|
||||||
|
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||||
|
assertThat(health.getDetails()).containsOnly(entry("first", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusForUnknownComponentReturnNull() {
|
||||||
|
HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator(
|
||||||
|
Collections.emptyMap()));
|
||||||
|
Health health = endpoint.healthForComponent("does-not-exist");
|
||||||
|
assertThat(health).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusForComponentInstanceIsExposed() {
|
||||||
|
CompositeHealthIndicator compositeIndicator = new CompositeHealthIndicator(
|
||||||
|
new OrderedHealthAggregator(), new DefaultHealthIndicatorRegistry(
|
||||||
|
Collections.singletonMap("sub", () -> Health.down().build())));
|
||||||
|
HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator(
|
||||||
|
Collections.singletonMap("test", compositeIndicator)));
|
||||||
|
Health health = endpoint.healthForComponentInstance("test", "sub");
|
||||||
|
assertThat(health).isNotNull();
|
||||||
|
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
|
||||||
|
assertThat(health.getDetails()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusForUnknownComponentInstanceReturnNull() {
|
||||||
|
CompositeHealthIndicator compositeIndicator = new CompositeHealthIndicator(
|
||||||
|
new OrderedHealthAggregator(), new DefaultHealthIndicatorRegistry(
|
||||||
|
Collections.singletonMap("sub", () -> Health.down().build())));
|
||||||
|
HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator(
|
||||||
|
Collections.singletonMap("test", compositeIndicator)));
|
||||||
|
Health health = endpoint.healthForComponentInstance("test", "does-not-exist");
|
||||||
|
assertThat(health).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void statusForComponentInstanceThatIsNotACompositeReturnNull() {
|
||||||
|
HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator(
|
||||||
|
Collections.singletonMap("test", () -> Health.up().build())));
|
||||||
|
Health health = endpoint.healthForComponentInstance("test", "does-not-exist");
|
||||||
|
assertThat(health).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
private HealthIndicator createHealthIndicator(
|
private HealthIndicator createHealthIndicator(
|
||||||
Map<String, HealthIndicator> healthIndicators) {
|
Map<String, HealthIndicator> healthIndicators) {
|
||||||
return new CompositeHealthIndicator(new OrderedHealthAggregator(),
|
return new CompositeHealthIndicator(new OrderedHealthAggregator(),
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2018 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.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.actuate.health;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link HealthWebEndpointResponseMapper}.
|
||||||
|
*
|
||||||
|
* @author Stephane Nicoll
|
||||||
|
*/
|
||||||
|
public class HealthWebEndpointResponseMapperTests {
|
||||||
|
|
||||||
|
private final HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper();
|
||||||
|
|
||||||
|
private Set<String> autorizedRoles = Collections.singleton("ACTUATOR");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapDetailsWithDisableDetailsDoesNotInvokeSupplier() {
|
||||||
|
HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.NEVER);
|
||||||
|
Supplier<Health> supplier = mockSupplier();
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
WebEndpointResponse<Health> response = mapper.mapDetails(supplier,
|
||||||
|
securityContext);
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
|
||||||
|
verifyZeroInteractions(supplier);
|
||||||
|
verifyZeroInteractions(securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapDetailsWithUnauthorizedUserDoesNotInvokeSupplier() {
|
||||||
|
HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED);
|
||||||
|
Supplier<Health> supplier = mockSupplier();
|
||||||
|
SecurityContext securityContext = mockSecurityContext("USER");
|
||||||
|
WebEndpointResponse<Health> response = mapper.mapDetails(supplier,
|
||||||
|
securityContext);
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
|
||||||
|
assertThat(response.getBody()).isNull();
|
||||||
|
verifyZeroInteractions(supplier);
|
||||||
|
verify(securityContext).isUserInRole("ACTUATOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapDetailsWithAuthorizedUserInvokeSupplier() {
|
||||||
|
HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED);
|
||||||
|
Supplier<Health> supplier = mockSupplier();
|
||||||
|
given(supplier.get()).willReturn(Health.down().build());
|
||||||
|
SecurityContext securityContext = mockSecurityContext("ACTUATOR");
|
||||||
|
WebEndpointResponse<Health> response = mapper.mapDetails(supplier,
|
||||||
|
securityContext);
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value());
|
||||||
|
assertThat(response.getBody().getStatus()).isEqualTo(Status.DOWN);
|
||||||
|
verify(supplier).get();
|
||||||
|
verify(securityContext).isUserInRole("ACTUATOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mapDetailsWithUnavailableHealth() {
|
||||||
|
HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.ALWAYS);
|
||||||
|
Supplier<Health> supplier = mockSupplier();
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
WebEndpointResponse<Health> response = mapper.mapDetails(supplier,
|
||||||
|
securityContext);
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
|
||||||
|
assertThat(response.getBody()).isNull();
|
||||||
|
verify(supplier).get();
|
||||||
|
verifyZeroInteractions(securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Supplier<Health> mockSupplier() {
|
||||||
|
return mock(Supplier.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecurityContext mockSecurityContext(String... roles) {
|
||||||
|
List<String> associatedRoles = Arrays.asList(roles);
|
||||||
|
SecurityContext securityContext = mock(SecurityContext.class);
|
||||||
|
given(securityContext.getPrincipal())
|
||||||
|
.willReturn(mock(Principal.class));
|
||||||
|
given(securityContext.isUserInRole(anyString())).will((Answer<Boolean>) invocation -> {
|
||||||
|
String expectedRole = invocation.getArgument(0);
|
||||||
|
return associatedRoles.contains(expectedRole);
|
||||||
|
});
|
||||||
|
return securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HealthWebEndpointResponseMapper createMapper(ShowDetails showDetails) {
|
||||||
|
return new HealthWebEndpointResponseMapper(this.statusHttpMapper, showDetails,
|
||||||
|
this.autorizedRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue