Rename health JSON 'details' to 'components' in v3
Update the health endpoint so the nested components are now exposed under `components` rather than `details` when v3 of the actuator REST API is being used. This distinction helps to clarify the difference between composite health (health composed of other health components) and health details (technology specific information gathered by the indicator). Since this is a breaking change for the REST API, it is only returned for v3 payloads. Requests made accepting only a v2 response will have JSON provided in the original form. Closes gh-17929
This commit is contained in:
@@ -24,6 +24,7 @@ import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.health.HealthComponent;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
@@ -46,14 +47,14 @@ public class CloudFoundryReactiveHealthEndpointWebExtension {
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health() {
|
||||
return this.delegate.health(SecurityContext.NONE, true);
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion) {
|
||||
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
|
||||
@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return this.delegate.health(SecurityContext.NONE, true, path);
|
||||
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.health.HealthComponent;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
@@ -44,13 +45,14 @@ public class CloudFoundryHealthEndpointWebExtension {
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<HealthComponent> health() {
|
||||
return this.delegate.health(SecurityContext.NONE, true);
|
||||
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion) {
|
||||
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<HealthComponent> health(@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return this.delegate.health(SecurityContext.NONE, true, path);
|
||||
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion,
|
||||
@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.health.CompositeHealth;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthComponent;
|
||||
@@ -62,12 +63,12 @@ class CloudFoundryReactiveHealthEndpointWebExtensionTests {
|
||||
.withUserConfiguration(TestHealthIndicator.class);
|
||||
|
||||
@Test
|
||||
void healthDetailsAlwaysPresent() {
|
||||
void healthComponentsAlwaysPresent() {
|
||||
this.contextRunner.run((context) -> {
|
||||
CloudFoundryReactiveHealthEndpointWebExtension extension = context
|
||||
.getBean(CloudFoundryReactiveHealthEndpointWebExtension.class);
|
||||
HealthComponent body = extension.health().block(Duration.ofSeconds(30)).getBody();
|
||||
HealthComponent health = ((CompositeHealth) body).getDetails().entrySet().iterator().next().getValue();
|
||||
HealthComponent body = extension.health(ApiVersion.V3).block(Duration.ofSeconds(30)).getBody();
|
||||
HealthComponent health = ((CompositeHealth) body).getComponents().entrySet().iterator().next().getValue();
|
||||
assertThat(((Health) health).getDetails()).containsEntry("spring", "boot");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
@@ -299,7 +300,9 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests {
|
||||
|
||||
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint, String requestPath) {
|
||||
for (WebOperation operation : endpoint.getOperations()) {
|
||||
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
if (predicate.getPath().equals(requestPath)
|
||||
&& predicate.getProduces().contains(ActuatorMediaType.V3_JSON)) {
|
||||
return operation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
@@ -243,7 +244,9 @@ class CloudFoundryActuatorAutoConfigurationTests {
|
||||
|
||||
private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint, String requestPath) {
|
||||
for (WebOperation operation : endpoint.getOperations()) {
|
||||
if (operation.getRequestPredicate().getPath().equals(requestPath)) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
if (predicate.getPath().equals(requestPath)
|
||||
&& predicate.getProduces().contains(ActuatorMediaType.V3_JSON)) {
|
||||
return operation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAu
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.health.CompositeHealth;
|
||||
import org.springframework.boot.actuate.health.Health;
|
||||
import org.springframework.boot.actuate.health.HealthComponent;
|
||||
@@ -59,12 +60,12 @@ class CloudFoundryHealthEndpointWebExtensionTests {
|
||||
.withUserConfiguration(TestHealthIndicator.class);
|
||||
|
||||
@Test
|
||||
void healthDetailsAlwaysPresent() {
|
||||
void healthComponentsAlwaysPresent() {
|
||||
this.contextRunner.run((context) -> {
|
||||
CloudFoundryHealthEndpointWebExtension extension = context
|
||||
.getBean(CloudFoundryHealthEndpointWebExtension.class);
|
||||
HealthComponent body = extension.health().getBody();
|
||||
HealthComponent health = ((CompositeHealth) body).getDetails().entrySet().iterator().next().getValue();
|
||||
HealthComponent body = extension.health(ApiVersion.V3).getBody();
|
||||
HealthComponent health = ((CompositeHealth) body).getComponents().entrySet().iterator().next().getValue();
|
||||
assertThat(((Health) health).getDetails()).containsEntry("spring", "boot");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
@@ -73,28 +74,31 @@ class HealthEndpointDocumentationTests extends MockMvcEndpointDocumentationTests
|
||||
@Test
|
||||
void health() throws Exception {
|
||||
FieldDescriptor status = fieldWithPath("status").description("Overall status of the application.");
|
||||
FieldDescriptor components = fieldWithPath("details").description("The components that make up the health.");
|
||||
FieldDescriptor componentStatus = fieldWithPath("details.*.status")
|
||||
FieldDescriptor components = fieldWithPath("components").description("The components that make up the health.");
|
||||
FieldDescriptor componentStatus = fieldWithPath("components.*.status")
|
||||
.description("Status of a specific part of the application.");
|
||||
FieldDescriptor componentDetails = subsectionWithPath("details.*.details")
|
||||
FieldDescriptor nestedComponents = subsectionWithPath("components.*.components")
|
||||
.description("The nested components that make up the health.").optional();
|
||||
FieldDescriptor componentDetails = subsectionWithPath("components.*.details")
|
||||
.description("Details of the health of a specific part of the application. "
|
||||
+ "Presence is controlled by `management.endpoint.health.show-details`. May contain nested "
|
||||
+ "components that make up the health.")
|
||||
.optional();
|
||||
this.mockMvc.perform(get("/actuator/health")).andExpect(status().isOk())
|
||||
.andDo(document("health", responseFields(status, components, componentStatus, componentDetails)));
|
||||
this.mockMvc.perform(get("/actuator/health").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
|
||||
.andDo(document("health",
|
||||
responseFields(status, components, componentStatus, nestedComponents, componentDetails)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthComponent() throws Exception {
|
||||
this.mockMvc.perform(get("/actuator/health/db")).andExpect(status().isOk())
|
||||
this.mockMvc.perform(get("/actuator/health/db").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())
|
||||
.andDo(document("health/component", responseFields(componentFields)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthComponentInstance() throws Exception {
|
||||
this.mockMvc.perform(get("/actuator/health/broker/us1")).andExpect(status().isOk())
|
||||
.andDo(document("health/instance", responseFields(componentFields)));
|
||||
this.mockMvc.perform(get("/actuator/health/broker/us1").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andDo(document("health/instance", responseFields(componentFields)));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.health.AbstractHealthAggregator;
|
||||
import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry;
|
||||
@@ -229,7 +230,8 @@ class HealthEndpointAutoConfigurationTests {
|
||||
void runCreatesHealthEndpointWebExtension() {
|
||||
this.contextRunner.run((context) -> {
|
||||
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
|
||||
WebEndpointResponse<HealthComponent> response = webExtension.health(SecurityContext.NONE, true, "simple");
|
||||
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
|
||||
true, "simple");
|
||||
Health health = (Health) response.getBody();
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(health.getDetails()).containsEntry("counter", 42);
|
||||
@@ -240,7 +242,8 @@ class HealthEndpointAutoConfigurationTests {
|
||||
void runWhenHasHealthEndpointWebExtensionBeanDoesNotCreateExtraHealthEndpointWebExtension() {
|
||||
this.contextRunner.withUserConfiguration(HealthEndpointWebExtensionConfiguration.class).run((context) -> {
|
||||
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
|
||||
WebEndpointResponse<HealthComponent> response = webExtension.health(SecurityContext.NONE, true, "simple");
|
||||
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
|
||||
true, "simple");
|
||||
assertThat(response).isNull();
|
||||
});
|
||||
}
|
||||
@@ -249,8 +252,8 @@ class HealthEndpointAutoConfigurationTests {
|
||||
void runCreatesReactiveHealthEndpointWebExtension() {
|
||||
this.reactiveContextRunner.run((context) -> {
|
||||
ReactiveHealthEndpointWebExtension webExtension = context.getBean(ReactiveHealthEndpointWebExtension.class);
|
||||
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(SecurityContext.NONE,
|
||||
true, "simple");
|
||||
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
|
||||
SecurityContext.NONE, true, "simple");
|
||||
Health health = (Health) (response.block().getBody());
|
||||
assertThat(health.getDetails()).containsEntry("counter", 42);
|
||||
});
|
||||
@@ -262,8 +265,8 @@ class HealthEndpointAutoConfigurationTests {
|
||||
.run((context) -> {
|
||||
ReactiveHealthEndpointWebExtension webExtension = context
|
||||
.getBean(ReactiveHealthEndpointWebExtension.class);
|
||||
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension
|
||||
.health(SecurityContext.NONE, true, "simple");
|
||||
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
|
||||
SecurityContext.NONE, true, "simple");
|
||||
assertThat(response).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import java.util.TreeMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -35,14 +37,21 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class CompositeHealth extends HealthComponent {
|
||||
|
||||
private Status status;
|
||||
private final Status status;
|
||||
|
||||
private Map<String, HealthComponent> details;
|
||||
private final Map<String, HealthComponent> components;
|
||||
|
||||
CompositeHealth(Status status, Map<String, HealthComponent> details) {
|
||||
private final Map<String, HealthComponent> details;
|
||||
|
||||
CompositeHealth(ApiVersion apiVersion, Status status, Map<String, HealthComponent> components) {
|
||||
Assert.notNull(status, "Status must not be null");
|
||||
this.status = status;
|
||||
this.details = (details != null) ? new TreeMap<>(details) : details;
|
||||
this.components = (apiVersion != ApiVersion.V3) ? null : sort(components);
|
||||
this.details = (apiVersion != ApiVersion.V2) ? null : sort(components);
|
||||
}
|
||||
|
||||
private Map<String, HealthComponent> sort(Map<String, HealthComponent> components) {
|
||||
return (components != null) ? new TreeMap<>(components) : components;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,7 +60,13 @@ public class CompositeHealth extends HealthComponent {
|
||||
}
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
public Map<String, HealthComponent> getDetails() {
|
||||
public Map<String, HealthComponent> getComponents() {
|
||||
return this.components;
|
||||
}
|
||||
|
||||
@JsonInclude(Include.NON_EMPTY)
|
||||
@JsonProperty
|
||||
Map<String, HealthComponent> getDetails() {
|
||||
return this.details;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,11 @@ public final class Health extends HealthComponent {
|
||||
this.details = Collections.unmodifiableMap(builder.details);
|
||||
}
|
||||
|
||||
Health(Status status, Map<String, Object> details) {
|
||||
this.status = status;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status of the health.
|
||||
* @return the status (never {@code null})
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Selector.Match;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
|
||||
/**
|
||||
* {@link Endpoint @Endpoint} to expose application health information.
|
||||
@@ -61,12 +62,16 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
|
||||
|
||||
@ReadOperation
|
||||
public HealthComponent health() {
|
||||
return healthForPath(EMPTY_PATH);
|
||||
return health(ApiVersion.V3, EMPTY_PATH);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public HealthComponent healthForPath(@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
HealthResult<HealthComponent> result = getHealth(SecurityContext.NONE, true, path);
|
||||
return health(ApiVersion.V3, path);
|
||||
}
|
||||
|
||||
private HealthComponent health(ApiVersion apiVersion, String... path) {
|
||||
HealthResult<HealthComponent> result = getHealth(apiVersion, SecurityContext.NONE, true, path);
|
||||
return (result != null) ? result.getHealth() : null;
|
||||
}
|
||||
|
||||
@@ -76,9 +81,9 @@ public class HealthEndpoint extends HealthEndpointSupport<HealthContributor, Hea
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
|
||||
protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map<String, HealthComponent> contributions,
|
||||
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
|
||||
return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
|
||||
return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -59,15 +60,16 @@ abstract class HealthEndpointSupport<C, T> {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
HealthResult<T> getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) {
|
||||
HealthResult<T> getHealth(ApiVersion apiVersion, SecurityContext securityContext, boolean alwaysIncludeDetails,
|
||||
String... path) {
|
||||
HealthEndpointGroup group = (path.length > 0) ? this.groups.get(path[0]) : null;
|
||||
if (group != null) {
|
||||
return getHealth(group, securityContext, alwaysIncludeDetails, path, 1);
|
||||
return getHealth(apiVersion, group, securityContext, alwaysIncludeDetails, path, 1);
|
||||
}
|
||||
return getHealth(this.groups.getPrimary(), securityContext, alwaysIncludeDetails, path, 0);
|
||||
return getHealth(apiVersion, this.groups.getPrimary(), securityContext, alwaysIncludeDetails, path, 0);
|
||||
}
|
||||
|
||||
private HealthResult<T> getHealth(HealthEndpointGroup group, SecurityContext securityContext,
|
||||
private HealthResult<T> getHealth(ApiVersion apiVersion, HealthEndpointGroup group, SecurityContext securityContext,
|
||||
boolean alwaysIncludeDetails, String[] path, int pathOffset) {
|
||||
boolean includeDetails = alwaysIncludeDetails || group.includeDetails(securityContext);
|
||||
boolean isSystemHealth = group == this.groups.getPrimary() && pathOffset == 0;
|
||||
@@ -76,7 +78,8 @@ abstract class HealthEndpointSupport<C, T> {
|
||||
return null;
|
||||
}
|
||||
Object contributor = getContributor(path, pathOffset);
|
||||
T health = getContribution(group, contributor, includeDetails, isSystemHealth ? this.groups.getNames() : null);
|
||||
T health = getContribution(apiVersion, group, contributor, includeDetails,
|
||||
isSystemHealth ? this.groups.getNames() : null);
|
||||
return (health != null) ? new HealthResult<T>(health, group) : null;
|
||||
}
|
||||
|
||||
@@ -94,44 +97,47 @@ abstract class HealthEndpointSupport<C, T> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private T getContribution(HealthEndpointGroup group, Object contributor, boolean includeDetails,
|
||||
Set<String> groupNames) {
|
||||
private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor,
|
||||
boolean includeDetails, Set<String> groupNames) {
|
||||
if (contributor instanceof NamedContributors) {
|
||||
return getAggregateHealth(group, (NamedContributors<C>) contributor, includeDetails, groupNames);
|
||||
return getAggregateHealth(apiVersion, group, (NamedContributors<C>) contributor, includeDetails,
|
||||
groupNames);
|
||||
}
|
||||
return (contributor != null) ? getHealth((C) contributor, includeDetails) : null;
|
||||
}
|
||||
|
||||
private T getAggregateHealth(HealthEndpointGroup group, NamedContributors<C> namedContributors,
|
||||
boolean includeDetails, Set<String> groupNames) {
|
||||
private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group,
|
||||
NamedContributors<C> namedContributors, boolean includeDetails, Set<String> groupNames) {
|
||||
Map<String, T> contributions = new LinkedHashMap<>();
|
||||
for (NamedContributor<C> namedContributor : namedContributors) {
|
||||
String name = namedContributor.getName();
|
||||
if (group.isMember(name)) {
|
||||
T contribution = getContribution(group, namedContributor.getContributor(), includeDetails, null);
|
||||
T contribution = getContribution(apiVersion, group, namedContributor.getContributor(), includeDetails,
|
||||
null);
|
||||
contributions.put(name, contribution);
|
||||
}
|
||||
}
|
||||
if (contributions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return aggregateContributions(contributions, group.getStatusAggregator(), includeDetails, groupNames);
|
||||
return aggregateContributions(apiVersion, contributions, group.getStatusAggregator(), includeDetails,
|
||||
groupNames);
|
||||
}
|
||||
|
||||
protected abstract T getHealth(C contributor, boolean includeDetails);
|
||||
|
||||
protected abstract T aggregateContributions(Map<String, T> contributions, StatusAggregator statusAggregator,
|
||||
boolean includeDetails, Set<String> groupNames);
|
||||
protected abstract T aggregateContributions(ApiVersion apiVersion, Map<String, T> contributions,
|
||||
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames);
|
||||
|
||||
protected final CompositeHealth getCompositeHealth(Map<String, HealthComponent> components,
|
||||
protected final CompositeHealth getCompositeHealth(ApiVersion apiVersion, Map<String, HealthComponent> components,
|
||||
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
|
||||
Status status = statusAggregator.getAggregateStatus(
|
||||
components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet()));
|
||||
Map<String, HealthComponent> includedComponents = includeDetails ? components : null;
|
||||
if (groupNames != null) {
|
||||
return new SystemHealth(status, includedComponents, groupNames);
|
||||
return new SystemHealth(apiVersion, status, includedComponents, groupNames);
|
||||
}
|
||||
return new CompositeHealth(status, includedComponents);
|
||||
return new CompositeHealth(apiVersion, status, includedComponents);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@ 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.annotation.Selector.Match;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||
|
||||
@@ -64,19 +65,19 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext) {
|
||||
return health(securityContext, NO_PATH);
|
||||
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext) {
|
||||
return health(apiVersion, securityContext, false, NO_PATH);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext,
|
||||
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext,
|
||||
@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return health(securityContext, false, path);
|
||||
return health(apiVersion, securityContext, false, path);
|
||||
}
|
||||
|
||||
public WebEndpointResponse<HealthComponent> health(SecurityContext securityContext, boolean alwaysIncludeDetails,
|
||||
String... path) {
|
||||
HealthResult<HealthComponent> result = getHealth(securityContext, alwaysIncludeDetails, path);
|
||||
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion, SecurityContext securityContext,
|
||||
boolean alwaysIncludeDetails, String... path) {
|
||||
HealthResult<HealthComponent> result = getHealth(apiVersion, securityContext, alwaysIncludeDetails, path);
|
||||
if (result == null) {
|
||||
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND);
|
||||
}
|
||||
@@ -92,9 +93,9 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport<HealthCont
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
|
||||
protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map<String, HealthComponent> contributions,
|
||||
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
|
||||
return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
|
||||
return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.annotation.Selector.Match;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
|
||||
|
||||
@@ -65,19 +66,21 @@ public class ReactiveHealthEndpointWebExtension
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext) {
|
||||
return health(securityContext, NO_PATH);
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
|
||||
SecurityContext securityContext) {
|
||||
return health(apiVersion, securityContext, false, NO_PATH);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext,
|
||||
@Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return health(securityContext, false, path);
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
|
||||
SecurityContext securityContext, @Selector(match = Match.ALL_REMAINING) String... path) {
|
||||
return health(apiVersion, securityContext, false, path);
|
||||
}
|
||||
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(SecurityContext securityContext,
|
||||
boolean alwaysIncludeDetails, String... path) {
|
||||
HealthResult<Mono<? extends HealthComponent>> result = getHealth(securityContext, alwaysIncludeDetails, path);
|
||||
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
|
||||
SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) {
|
||||
HealthResult<Mono<? extends HealthComponent>> result = getHealth(apiVersion, securityContext,
|
||||
alwaysIncludeDetails, path);
|
||||
if (result == null) {
|
||||
return Mono.just(new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND));
|
||||
}
|
||||
@@ -94,12 +97,12 @@ public class ReactiveHealthEndpointWebExtension
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<? extends HealthComponent> aggregateContributions(
|
||||
protected Mono<? extends HealthComponent> aggregateContributions(ApiVersion apiVersion,
|
||||
Map<String, Mono<? extends HealthComponent>> contributions, StatusAggregator statusAggregator,
|
||||
boolean includeDetails, Set<String> groupNames) {
|
||||
return Flux.fromIterable(contributions.entrySet()).flatMap(NamedHealthComponent::create)
|
||||
.collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth)
|
||||
.map((components) -> this.getCompositeHealth(components, statusAggregator, includeDetails, groupNames));
|
||||
.collectMap(NamedHealthComponent::getName, NamedHealthComponent::getHealth).map((components) -> this
|
||||
.getCompositeHealth(apiVersion, components, statusAggregator, includeDetails, groupNames));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.util.TreeSet;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
|
||||
/**
|
||||
* A {@link HealthComponent} that represents the overall system health and the available
|
||||
* groups.
|
||||
@@ -34,8 +36,8 @@ public final class SystemHealth extends CompositeHealth {
|
||||
|
||||
private final Set<String> groups;
|
||||
|
||||
SystemHealth(Status status, Map<String, HealthComponent> instances, Set<String> groups) {
|
||||
super(status, instances);
|
||||
SystemHealth(ApiVersion apiVersion, Status status, Map<String, HealthComponent> instances, Set<String> groups) {
|
||||
super(apiVersion, status, instances);
|
||||
this.groups = (groups != null) ? new TreeSet<>(groups) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.util.Map;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
@@ -35,13 +37,14 @@ class CompositeHealthTests {
|
||||
|
||||
@Test
|
||||
void createWhenStatusIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new CompositeHealth(null, Collections.emptyMap()))
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new CompositeHealth(ApiVersion.V3, null, Collections.emptyMap()))
|
||||
.withMessage("Status must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStatusReturnsStatus() {
|
||||
CompositeHealth health = new CompositeHealth(Status.UP, Collections.emptyMap());
|
||||
CompositeHealth health = new CompositeHealth(ApiVersion.V3, Status.UP, Collections.emptyMap());
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
}
|
||||
|
||||
@@ -49,16 +52,28 @@ class CompositeHealthTests {
|
||||
void getComponentReturnsComponents() {
|
||||
Map<String, HealthComponent> components = new LinkedHashMap<>();
|
||||
components.put("a", Health.up().build());
|
||||
CompositeHealth health = new CompositeHealth(Status.UP, components);
|
||||
assertThat(health.getDetails()).isEqualTo(components);
|
||||
CompositeHealth health = new CompositeHealth(ApiVersion.V3, Status.UP, components);
|
||||
assertThat(health.getComponents()).isEqualTo(components);
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializeWithJacksonReturnsValidJson() throws Exception {
|
||||
void serializeV3WithJacksonReturnsValidJson() throws Exception {
|
||||
Map<String, HealthComponent> components = new LinkedHashMap<>();
|
||||
components.put("db1", Health.up().build());
|
||||
components.put("db2", Health.down().withDetail("a", "b").build());
|
||||
CompositeHealth health = new CompositeHealth(Status.UP, components);
|
||||
CompositeHealth health = new CompositeHealth(ApiVersion.V3, Status.UP, components);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(health);
|
||||
assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{" + "\"db1\":{\"status\":\"UP\"},"
|
||||
+ "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializeV2WithJacksonReturnsValidJson() throws Exception {
|
||||
Map<String, HealthComponent> components = new LinkedHashMap<>();
|
||||
components.put("db1", Health.up().build());
|
||||
components.put("db2", Health.down().withDetail("a", "b").build());
|
||||
CompositeHealth health = new CompositeHealth(ApiVersion.V2, Status.UP, components);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(health);
|
||||
assertThat(json).isEqualTo("{\"status\":\"UP\",\"details\":{" + "\"db1\":{\"status\":\"UP\"},"
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -77,7 +78,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
@Test
|
||||
void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false);
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false);
|
||||
assertThat(result.getGroup()).isEqualTo(this.primaryGroup);
|
||||
assertThat(getHealth(result)).isNotSameAs(this.up);
|
||||
assertThat(getHealth(result).getStatus()).isEqualTo(Status.UP);
|
||||
@@ -86,7 +88,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
@Test
|
||||
void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false, "test");
|
||||
assertThat(result.getGroup()).isEqualTo(this.primaryGroup);
|
||||
assertThat(getHealth(result)).isEqualTo(this.up);
|
||||
|
||||
@@ -95,8 +98,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
@Test
|
||||
void getHealthResultWhenPathIsGroupReturnsResultFromGroup() {
|
||||
this.registry.registerContributor("atest", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas",
|
||||
"atest");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false, "alltheas", "atest");
|
||||
assertThat(result.getGroup()).isEqualTo(this.allTheAs);
|
||||
assertThat(getHealth(result)).isEqualTo(this.up);
|
||||
}
|
||||
@@ -104,7 +107,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
@Test
|
||||
void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsTrueIncludesDetails() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false, "test");
|
||||
assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
|
||||
}
|
||||
|
||||
@@ -113,8 +117,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
this.primaryGroup.setIncludeDetails(false);
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthEndpointSupport<C, T> endpoint = create(this.registry, this.groups);
|
||||
HealthResult<T> rootResult = endpoint.getHealth(SecurityContext.NONE, false);
|
||||
HealthResult<T> componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test");
|
||||
HealthResult<T> rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false);
|
||||
HealthResult<T> componentResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false, "test");
|
||||
assertThat(((CompositeHealth) getHealth(rootResult)).getStatus()).isEqualTo(Status.UP);
|
||||
assertThat(componentResult).isNull();
|
||||
}
|
||||
@@ -123,7 +127,8 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() {
|
||||
this.primaryGroup.setIncludeDetails(false);
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, true, "test");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, true,
|
||||
"test");
|
||||
assertThat(((Health) getHealth(result)).getDetails()).containsEntry("spring", "boot");
|
||||
}
|
||||
|
||||
@@ -133,31 +138,35 @@ abstract class HealthEndpointSupportTests<R extends ContributorRegistry<C>, C, T
|
||||
contributors.put("a", createContributor(this.up));
|
||||
contributors.put("b", createContributor(this.down));
|
||||
this.registry.registerContributor("test", createCompositeContributor(contributors));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false);
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false);
|
||||
CompositeHealth root = (CompositeHealth) getHealth(result);
|
||||
CompositeHealth component = (CompositeHealth) root.getDetails().get("test");
|
||||
CompositeHealth component = (CompositeHealth) root.getComponents().get("test");
|
||||
assertThat(root.getStatus()).isEqualTo(Status.DOWN);
|
||||
assertThat(component.getStatus()).isEqualTo(Status.DOWN);
|
||||
assertThat(component.getDetails()).containsOnlyKeys("a", "b");
|
||||
assertThat(component.getComponents()).containsOnlyKeys("a", "b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHealthResultWhenPathDoesNotExistReturnsNull() {
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "missing");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false, "missing");
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHealthResultWhenPathIsEmptyIncludesGroups() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false);
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false);
|
||||
assertThat(((SystemHealth) getHealth(result)).getGroups()).containsOnly("alltheas");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHealthResultWhenPathIsGroupDoesNotIncludesGroups() {
|
||||
this.registry.registerContributor("atest", createContributor(this.up));
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas");
|
||||
HealthResult<T> result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE,
|
||||
false, "alltheas");
|
||||
assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult;
|
||||
|
||||
@@ -42,15 +43,15 @@ class HealthEndpointWebExtensionTests
|
||||
HealthEndpoint delegate = mock(HealthEndpoint.class);
|
||||
HealthWebEndpointResponseMapper responseMapper = mock(HealthWebEndpointResponseMapper.class);
|
||||
assertThatIllegalStateException().isThrownBy(() -> new HealthEndpointWebExtension(delegate, responseMapper))
|
||||
.withMessage(
|
||||
"Unable to create class org.springframework.boot.actuate.health.HealthEndpointWebExtension "
|
||||
+ "using deprecated constructor");
|
||||
.withMessage("Unable to create class org.springframework.boot.actuate."
|
||||
+ "health.HealthEndpointWebExtension using deprecated constructor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void healthReturnsSystemHealth() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE);
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
|
||||
SecurityContext.NONE);
|
||||
HealthComponent health = response.getBody();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
assertThat(health).isInstanceOf(SystemHealth.class);
|
||||
@@ -60,8 +61,8 @@ class HealthEndpointWebExtensionTests
|
||||
@Test
|
||||
void healthWhenPathDoesNotExistReturnsHttp404() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE,
|
||||
"missing");
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
|
||||
SecurityContext.NONE, "missing");
|
||||
assertThat(response.getBody()).isNull();
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
@@ -69,8 +70,8 @@ class HealthEndpointWebExtensionTests
|
||||
@Test
|
||||
void healthWhenPathExistsReturnsHealth() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(SecurityContext.NONE,
|
||||
"test");
|
||||
WebEndpointResponse<HealthComponent> response = create(this.registry, this.groups).health(ApiVersion.LATEST,
|
||||
SecurityContext.NONE, "test");
|
||||
assertThat(response.getBody()).isEqualTo(this.up);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,16 @@ import java.util.Map;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -44,20 +47,45 @@ class HealthEndpointWebIntegrationTests {
|
||||
|
||||
@WebEndpointTest
|
||||
void whenHealthIsUp200ResponseIsReturned(WebTestClient client) {
|
||||
client.get().uri("/actuator/health").exchange().expectStatus().isOk().expectBody().jsonPath("status")
|
||||
.isEqualTo("UP").jsonPath("details.alpha.status").isEqualTo("UP").jsonPath("details.bravo.status")
|
||||
client.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
|
||||
.expectBody().jsonPath("status").isEqualTo("UP").jsonPath("components.alpha.status").isEqualTo("UP")
|
||||
.jsonPath("components.bravo.status").isEqualTo("UP");
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void whenHealthIsUpAndAcceptsV3Request200ResponseIsReturned(WebTestClient client) {
|
||||
client.get().uri("/actuator/health")
|
||||
.headers((headers) -> headers.set(HttpHeaders.ACCEPT, ActuatorMediaType.V3_JSON)).exchange()
|
||||
.expectStatus().isOk().expectBody().jsonPath("status").isEqualTo("UP")
|
||||
.jsonPath("components.alpha.status").isEqualTo("UP").jsonPath("components.bravo.status")
|
||||
.isEqualTo("UP");
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void whenHealthIsUpAndAcceptsAllRequest200ResponseIsReturned(WebTestClient client) {
|
||||
client.get().uri("/actuator/health").headers((headers) -> headers.set(HttpHeaders.ACCEPT, "*/*")).exchange()
|
||||
.expectStatus().isOk().expectBody().jsonPath("status").isEqualTo("UP")
|
||||
.jsonPath("components.alpha.status").isEqualTo("UP").jsonPath("components.bravo.status")
|
||||
.isEqualTo("UP");
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void whenHealthIsUpAndV2Request200ResponseIsReturnedInV2Format(WebTestClient client) {
|
||||
client.get().uri("/actuator/health")
|
||||
.headers((headers) -> headers.set(HttpHeaders.ACCEPT, ActuatorMediaType.V2_JSON)).exchange()
|
||||
.expectStatus().isOk().expectBody().jsonPath("status").isEqualTo("UP").jsonPath("details.alpha.status")
|
||||
.isEqualTo("UP").jsonPath("details.bravo.status").isEqualTo("UP");
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
void whenHealthIsDown503ResponseIsReturned(ApplicationContext context, WebTestClient client) {
|
||||
HealthIndicator healthIndicator = () -> Health.down().build();
|
||||
ReactiveHealthIndicator reactiveHealthIndicator = () -> Mono.just(Health.down().build());
|
||||
withHealthContributor(context, "charlie", healthIndicator, reactiveHealthIndicator,
|
||||
() -> client.get().uri("/actuator/health").exchange().expectStatus()
|
||||
() -> client.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).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"));
|
||||
.jsonPath("components.alpha.status").isEqualTo("UP").jsonPath("components.bravo.status")
|
||||
.isEqualTo("UP").jsonPath("components.charlie.status").isEqualTo("DOWN"));
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
@@ -65,8 +93,9 @@ class HealthEndpointWebIntegrationTests {
|
||||
HealthIndicator healthIndicator = () -> Health.down().build();
|
||||
ReactiveHealthIndicator reactiveHealthIndicator = () -> Mono.just(Health.down().build());
|
||||
withHealthContributor(context, "charlie", healthIndicator, reactiveHealthIndicator,
|
||||
() -> client.get().uri("/actuator/health/charlie").exchange().expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody().jsonPath("status").isEqualTo("DOWN"));
|
||||
() -> client.get().uri("/actuator/health/charlie").accept(MediaType.APPLICATION_JSON).exchange()
|
||||
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody().jsonPath("status")
|
||||
.isEqualTo("DOWN"));
|
||||
}
|
||||
|
||||
@WebEndpointTest
|
||||
@@ -78,8 +107,9 @@ class HealthEndpointWebIntegrationTests {
|
||||
CompositeReactiveHealthContributor reactiveComposite = CompositeReactiveHealthContributor
|
||||
.fromMap(Collections.singletonMap("one", reactiveHealthIndicator));
|
||||
withHealthContributor(context, "charlie", composite, reactiveComposite,
|
||||
() -> client.get().uri("/actuator/health/charlie/one").exchange().expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody().jsonPath("status").isEqualTo("DOWN"));
|
||||
() -> client.get().uri("/actuator/health/charlie/one").accept(MediaType.APPLICATION_JSON).exchange()
|
||||
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE).expectBody().jsonPath("status")
|
||||
.isEqualTo("DOWN"));
|
||||
}
|
||||
|
||||
private void withHealthContributor(ApplicationContext context, String name, HealthContributor healthContributor,
|
||||
@@ -122,9 +152,9 @@ class HealthEndpointWebIntegrationTests {
|
||||
ReactiveHealthContributor reactiveBravo = (reactiveHealthContributorRegistry != null)
|
||||
? reactiveHealthContributorRegistry.unregisterContributor(name) : null;
|
||||
try {
|
||||
client.get().uri("/actuator/health").exchange().expectStatus().isOk().expectBody().jsonPath("status")
|
||||
.isEqualTo("UP").jsonPath("details.alpha.status").isEqualTo("UP").jsonPath("details.bravo.status")
|
||||
.doesNotExist();
|
||||
client.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk()
|
||||
.expectBody().jsonPath("status").isEqualTo("UP").jsonPath("components.alpha.status").isEqualTo("UP")
|
||||
.jsonPath("components.bravo.status").doesNotExist();
|
||||
}
|
||||
finally {
|
||||
healthContributorRegistry.registerContributor(name, bravo);
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult;
|
||||
|
||||
@@ -52,7 +53,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
|
||||
void healthReturnsSystemHealth() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
|
||||
.health(SecurityContext.NONE).block();
|
||||
.health(ApiVersion.LATEST, SecurityContext.NONE).block();
|
||||
HealthComponent health = response.getBody();
|
||||
assertThat(health.getStatus()).isEqualTo(Status.UP);
|
||||
assertThat(health).isInstanceOf(SystemHealth.class);
|
||||
@@ -63,7 +64,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
|
||||
void healthWhenPathDoesNotExistReturnsHttp404() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
|
||||
.health(SecurityContext.NONE, "missing").block();
|
||||
.health(ApiVersion.LATEST, SecurityContext.NONE, "missing").block();
|
||||
assertThat(response.getBody()).isNull();
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
@@ -72,7 +73,7 @@ class ReactiveHealthEndpointWebExtensionTests extends
|
||||
void healthWhenPathExistsReturnsHealth() {
|
||||
this.registry.registerContributor("test", createContributor(this.up));
|
||||
WebEndpointResponse<? extends HealthComponent> response = create(this.registry, this.groups)
|
||||
.health(SecurityContext.NONE, "test").block();
|
||||
.health(ApiVersion.LATEST, SecurityContext.NONE, "test").block();
|
||||
assertThat(response.getBody()).isEqualTo(this.up);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import java.util.Set;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
@@ -40,10 +42,10 @@ class SystemHealthTests {
|
||||
components.put("db1", Health.up().build());
|
||||
components.put("db2", Health.down().withDetail("a", "b").build());
|
||||
Set<String> groups = new LinkedHashSet<>(Arrays.asList("liveness", "readiness"));
|
||||
CompositeHealth health = new SystemHealth(Status.UP, components, groups);
|
||||
CompositeHealth health = new SystemHealth(ApiVersion.V3, Status.UP, components, groups);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(health);
|
||||
assertThat(json).isEqualTo("{\"status\":\"UP\",\"details\":{" + "\"db1\":{\"status\":\"UP\"},"
|
||||
assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{" + "\"db1\":{\"status\":\"UP\"},"
|
||||
+ "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}},"
|
||||
+ "\"groups\":[\"liveness\",\"readiness\"]}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user