diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java index 5646182221..589f952bc7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java @@ -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> health() { - return this.delegate.health(SecurityContext.NONE, true); + public Mono> health(ApiVersion apiVersion) { + return this.delegate.health(apiVersion, SecurityContext.NONE, true); } @ReadOperation - public Mono> health( + public Mono> 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java index 8f1328b0c5..e91a1fbe4b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java @@ -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 health() { - return this.delegate.health(SecurityContext.NONE, true); + public WebEndpointResponse health(ApiVersion apiVersion) { + return this.delegate.health(apiVersion, SecurityContext.NONE, true); } @ReadOperation - public WebEndpointResponse health(@Selector(match = Match.ALL_REMAINING) String... path) { - return this.delegate.health(SecurityContext.NONE, true, path); + public WebEndpointResponse health(ApiVersion apiVersion, + @Selector(match = Match.ALL_REMAINING) String... path) { + return this.delegate.health(apiVersion, SecurityContext.NONE, true, path); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java index 0890ce3d0f..1cd7882a08 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java @@ -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"); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index ce7f8aac15..62fe9f9f3a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index eaeed62e86..d895df495c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -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; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java index 9db6e3df49..7a940aebfc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java @@ -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"); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java index b097ba58bf..ea39a03ba6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java @@ -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) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index f70fad83bb..a82e79331b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -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 response = webExtension.health(SecurityContext.NONE, true, "simple"); + WebEndpointResponse 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 response = webExtension.health(SecurityContext.NONE, true, "simple"); + WebEndpointResponse 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> response = webExtension.health(SecurityContext.NONE, - true, "simple"); + Mono> 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> response = webExtension - .health(SecurityContext.NONE, true, "simple"); + Mono> response = webExtension.health(ApiVersion.V3, + SecurityContext.NONE, true, "simple"); assertThat(response).isNull(); }); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java index f55a40ce20..8e897dd148 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java @@ -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 details; + private final Map components; - CompositeHealth(Status status, Map details) { + private final Map details; + + CompositeHealth(ApiVersion apiVersion, Status status, Map 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 sort(Map components) { + return (components != null) ? new TreeMap<>(components) : components; } @Override @@ -51,7 +60,13 @@ public class CompositeHealth extends HealthComponent { } @JsonInclude(Include.NON_EMPTY) - public Map getDetails() { + public Map getComponents() { + return this.components; + } + + @JsonInclude(Include.NON_EMPTY) + @JsonProperty + Map getDetails() { return this.details; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java index 9c394bbb4c..438bec08c6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java @@ -65,6 +65,11 @@ public final class Health extends HealthComponent { this.details = Collections.unmodifiableMap(builder.details); } + Health(Status status, Map details) { + this.status = status; + this.details = details; + } + /** * Return the status of the health. * @return the status (never {@code null}) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index a8729af245..a0ffb82246 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -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 result = getHealth(SecurityContext.NONE, true, path); + return health(ApiVersion.V3, path); + } + + private HealthComponent health(ApiVersion apiVersion, String... path) { + HealthResult result = getHealth(apiVersion, SecurityContext.NONE, true, path); return (result != null) ? result.getHealth() : null; } @@ -76,9 +81,9 @@ public class HealthEndpoint extends HealthEndpointSupport contributions, + protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map contributions, StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { - return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames); + return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java index 486b04ed33..ae8a8980fc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointSupport.java @@ -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 { this.groups = groups; } - HealthResult getHealth(SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { + HealthResult 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 getHealth(HealthEndpointGroup group, SecurityContext securityContext, + private HealthResult 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 { 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(health, group) : null; } @@ -94,44 +97,47 @@ abstract class HealthEndpointSupport { } @SuppressWarnings("unchecked") - private T getContribution(HealthEndpointGroup group, Object contributor, boolean includeDetails, - Set groupNames) { + private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor, + boolean includeDetails, Set groupNames) { if (contributor instanceof NamedContributors) { - return getAggregateHealth(group, (NamedContributors) contributor, includeDetails, groupNames); + return getAggregateHealth(apiVersion, group, (NamedContributors) contributor, includeDetails, + groupNames); } return (contributor != null) ? getHealth((C) contributor, includeDetails) : null; } - private T getAggregateHealth(HealthEndpointGroup group, NamedContributors namedContributors, - boolean includeDetails, Set groupNames) { + private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group, + NamedContributors namedContributors, boolean includeDetails, Set groupNames) { Map contributions = new LinkedHashMap<>(); for (NamedContributor 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 contributions, StatusAggregator statusAggregator, - boolean includeDetails, Set groupNames); + protected abstract T aggregateContributions(ApiVersion apiVersion, Map contributions, + StatusAggregator statusAggregator, boolean includeDetails, Set groupNames); - protected final CompositeHealth getCompositeHealth(Map components, + protected final CompositeHealth getCompositeHealth(ApiVersion apiVersion, Map components, StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { Status status = statusAggregator.getAggregateStatus( components.values().stream().map(HealthComponent::getStatus).collect(Collectors.toSet())); Map 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); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java index fb6eeaecd5..a3bf307305 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java @@ -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 health(SecurityContext securityContext) { - return health(securityContext, NO_PATH); + public WebEndpointResponse health(ApiVersion apiVersion, SecurityContext securityContext) { + return health(apiVersion, securityContext, false, NO_PATH); } @ReadOperation - public WebEndpointResponse health(SecurityContext securityContext, + public WebEndpointResponse 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 health(SecurityContext securityContext, boolean alwaysIncludeDetails, - String... path) { - HealthResult result = getHealth(securityContext, alwaysIncludeDetails, path); + public WebEndpointResponse health(ApiVersion apiVersion, SecurityContext securityContext, + boolean alwaysIncludeDetails, String... path) { + HealthResult 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 contributions, + protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map contributions, StatusAggregator statusAggregator, boolean includeDetails, Set groupNames) { - return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames); + return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index fc8ecf79e8..75f933bc19 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -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> health(SecurityContext securityContext) { - return health(securityContext, NO_PATH); + public Mono> health(ApiVersion apiVersion, + SecurityContext securityContext) { + return health(apiVersion, securityContext, false, NO_PATH); } @ReadOperation - public Mono> health(SecurityContext securityContext, - @Selector(match = Match.ALL_REMAINING) String... path) { - return health(securityContext, false, path); + public Mono> health(ApiVersion apiVersion, + SecurityContext securityContext, @Selector(match = Match.ALL_REMAINING) String... path) { + return health(apiVersion, securityContext, false, path); } - public Mono> health(SecurityContext securityContext, - boolean alwaysIncludeDetails, String... path) { - HealthResult> result = getHealth(securityContext, alwaysIncludeDetails, path); + public Mono> health(ApiVersion apiVersion, + SecurityContext securityContext, boolean alwaysIncludeDetails, String... path) { + HealthResult> 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 aggregateContributions( + protected Mono aggregateContributions(ApiVersion apiVersion, Map> contributions, StatusAggregator statusAggregator, boolean includeDetails, Set 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)); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java index bb3acd6e4f..3ea7ce787c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java @@ -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 groups; - SystemHealth(Status status, Map instances, Set groups) { - super(status, instances); + SystemHealth(ApiVersion apiVersion, Status status, Map instances, Set groups) { + super(apiVersion, status, instances); this.groups = (groups != null) ? new TreeSet<>(groups) : null; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java index cd13ef75f5..ec4e1ee3be 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java @@ -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 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 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 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\"}," diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java index 58583e16db..3362f6ada3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java @@ -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, C, T @Test void getHealthResultWhenPathIsEmptyUsesPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); + HealthResult 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, C, T @Test void getHealthResultWhenPathIsNotGroupReturnsResultFromPrimaryGroup() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test"); + HealthResult 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, C, T @Test void getHealthResultWhenPathIsGroupReturnsResultFromGroup() { this.registry.registerContributor("atest", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas", - "atest"); + HealthResult 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, C, T @Test void getHealthResultWhenAlwaysIncludesDetailsIsFalseAndGroupIsTrueIncludesDetails() { this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "test"); + HealthResult 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, C, T this.primaryGroup.setIncludeDetails(false); this.registry.registerContributor("test", createContributor(this.up)); HealthEndpointSupport endpoint = create(this.registry, this.groups); - HealthResult rootResult = endpoint.getHealth(SecurityContext.NONE, false); - HealthResult componentResult = endpoint.getHealth(SecurityContext.NONE, false, "test"); + HealthResult rootResult = endpoint.getHealth(ApiVersion.V3, SecurityContext.NONE, false); + HealthResult 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, C, T void getHealthResultWhenAlwaysIncludesDetailsIsTrueIncludesDetails() { this.primaryGroup.setIncludeDetails(false); this.registry.registerContributor("test", createContributor(this.up)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, true, "test"); + HealthResult 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, C, T contributors.put("a", createContributor(this.up)); contributors.put("b", createContributor(this.down)); this.registry.registerContributor("test", createCompositeContributor(contributors)); - HealthResult result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); + HealthResult 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 result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "missing"); + HealthResult 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 result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false); + HealthResult 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 result = create(this.registry, this.groups).getHealth(SecurityContext.NONE, false, "alltheas"); + HealthResult result = create(this.registry, this.groups).getHealth(ApiVersion.V3, SecurityContext.NONE, + false, "alltheas"); assertThat(getHealth(result)).isNotInstanceOf(SystemHealth.class); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java index 2e191e6269..8d36fc0969 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java @@ -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 response = create(this.registry, this.groups).health(SecurityContext.NONE); + WebEndpointResponse 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 response = create(this.registry, this.groups).health(SecurityContext.NONE, - "missing"); + WebEndpointResponse 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 response = create(this.registry, this.groups).health(SecurityContext.NONE, - "test"); + WebEndpointResponse response = create(this.registry, this.groups).health(ApiVersion.LATEST, + SecurityContext.NONE, "test"); assertThat(response.getBody()).isEqualTo(this.up); assertThat(response.getStatus()).isEqualTo(200); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java index b75729cee8..ca11aec206 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java @@ -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); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java index bf7302ce6f..2820aa4e49 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java @@ -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 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 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 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); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java index 46dc358164..45eafce64e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java @@ -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 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\"]}"); }