Commit 69c561a6 authored by Phillip Webb's avatar Phillip Webb

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
parent cd1b7c1a
......@@ -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 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(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 serializeWithJacksonReturnsValidJson() throws Exception {
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(Status.UP, components);
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\"]}");
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment