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:
Phillip Webb
2019-09-25 22:46:37 -07:00
parent cd1b7c1a9c
commit 69c561a69a
21 changed files with 236 additions and 123 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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");
});
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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");
});
}

View File

@@ -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)

View File

@@ -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();
});
}

View File

@@ -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;
}

View File

@@ -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})

View File

@@ -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);
}
}

View File

@@ -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);
}
/**

View File

@@ -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);
}
}

View File

@@ -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));
}
/**

View File

@@ -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;
}

View File

@@ -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\"},"

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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\"]}");
}