From 69c561a69a54a32ba31f4bc66e7954ee21e6445f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Sep 2019 22:46:37 -0700 Subject: [PATCH] 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 --- ...dryReactiveHealthEndpointWebExtension.java | 9 ++-- ...loudFoundryHealthEndpointWebExtension.java | 10 ++-- ...activeHealthEndpointWebExtensionTests.java | 7 +-- ...FoundryActuatorAutoConfigurationTests.java | 5 +- ...FoundryActuatorAutoConfigurationTests.java | 5 +- ...oundryHealthEndpointWebExtensionTests.java | 7 +-- .../HealthEndpointDocumentationTests.java | 20 ++++--- .../HealthEndpointAutoConfigurationTests.java | 15 +++--- .../boot/actuate/health/CompositeHealth.java | 25 +++++++-- .../boot/actuate/health/Health.java | 5 ++ .../boot/actuate/health/HealthEndpoint.java | 13 +++-- .../actuate/health/HealthEndpointSupport.java | 40 ++++++++------ .../health/HealthEndpointWebExtension.java | 19 +++---- .../ReactiveHealthEndpointWebExtension.java | 25 +++++---- .../boot/actuate/health/SystemHealth.java | 6 ++- .../actuate/health/CompositeHealthTests.java | 27 +++++++--- .../health/HealthEndpointSupportTests.java | 37 ++++++++----- .../HealthEndpointWebExtensionTests.java | 17 +++--- .../HealthEndpointWebIntegrationTests.java | 54 ++++++++++++++----- ...activeHealthEndpointWebExtensionTests.java | 7 +-- .../actuate/health/SystemHealthTests.java | 6 ++- 21 files changed, 236 insertions(+), 123 deletions(-) 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\"]}"); }