Skip to content

Commit

Permalink
Rename health JSON 'details' to 'components' in v3
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Sep 26, 2019
1 parent cd1b7c1 commit 69c561a
Show file tree
Hide file tree
Showing 21 changed files with 236 additions and 123 deletions.
Expand Up @@ -24,6 +24,7 @@
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;
Expand All @@ -46,14 +47,14 @@ public CloudFoundryReactiveHealthEndpointWebExtension(ReactiveHealthEndpointWebE
}

@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health() {
return this.delegate.health(SecurityContext.NONE, true);
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
}

@ReadOperation
public Mono<WebEndpointResponse<? extends HealthComponent>> health(
public Mono<WebEndpointResponse<? extends HealthComponent>> health(ApiVersion apiVersion,
@Selector(match = Match.ALL_REMAINING) String... path) {
return this.delegate.health(SecurityContext.NONE, true, path);
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
}

}
Expand Up @@ -22,6 +22,7 @@
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;
Expand All @@ -44,13 +45,14 @@ public CloudFoundryHealthEndpointWebExtension(HealthEndpointWebExtension delegat
}

@ReadOperation
public WebEndpointResponse<HealthComponent> health() {
return this.delegate.health(SecurityContext.NONE, true);
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true);
}

@ReadOperation
public WebEndpointResponse<HealthComponent> health(@Selector(match = Match.ALL_REMAINING) String... path) {
return this.delegate.health(SecurityContext.NONE, true, path);
public WebEndpointResponse<HealthComponent> health(ApiVersion apiVersion,
@Selector(match = Match.ALL_REMAINING) String... path) {
return this.delegate.health(apiVersion, SecurityContext.NONE, true, path);
}

}
Expand Up @@ -25,6 +25,7 @@
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;
Expand Down Expand Up @@ -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");
});
}
Expand Down
Expand Up @@ -45,6 +45,7 @@
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;
Expand Down Expand Up @@ -299,7 +300,9 @@ private CloudFoundryWebFluxEndpointHandlerMapping getHandlerMapping(ApplicationC

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;
}
}
Expand Down
Expand Up @@ -34,6 +34,7 @@
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;
Expand Down Expand Up @@ -243,7 +244,9 @@ private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping(Applicati

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;
}
}
Expand Down
Expand Up @@ -24,6 +24,7 @@
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;
Expand Down Expand Up @@ -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");
});
}
Expand Down
Expand Up @@ -48,6 +48,7 @@
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;

Expand All @@ -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)
Expand Down
Expand Up @@ -23,6 +23,7 @@
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;
Expand Down Expand Up @@ -229,7 +230,8 @@ void runWhenHasReactiveHealthContributorRegistryBeanDoesNotCreateAdditionalReact
void runCreatesHealthEndpointWebExtension() {
this.contextRunner.run((context) -> {
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
WebEndpointResponse<HealthComponent> response = webExtension.health(SecurityContext.NONE, true, "simple");
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
true, "simple");
Health health = (Health) response.getBody();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(health.getDetails()).containsEntry("counter", 42);
Expand All @@ -240,7 +242,8 @@ void runCreatesHealthEndpointWebExtension() {
void runWhenHasHealthEndpointWebExtensionBeanDoesNotCreateExtraHealthEndpointWebExtension() {
this.contextRunner.withUserConfiguration(HealthEndpointWebExtensionConfiguration.class).run((context) -> {
HealthEndpointWebExtension webExtension = context.getBean(HealthEndpointWebExtension.class);
WebEndpointResponse<HealthComponent> response = webExtension.health(SecurityContext.NONE, true, "simple");
WebEndpointResponse<HealthComponent> response = webExtension.health(ApiVersion.V3, SecurityContext.NONE,
true, "simple");
assertThat(response).isNull();
});
}
Expand All @@ -249,8 +252,8 @@ void runWhenHasHealthEndpointWebExtensionBeanDoesNotCreateExtraHealthEndpointWeb
void runCreatesReactiveHealthEndpointWebExtension() {
this.reactiveContextRunner.run((context) -> {
ReactiveHealthEndpointWebExtension webExtension = context.getBean(ReactiveHealthEndpointWebExtension.class);
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(SecurityContext.NONE,
true, "simple");
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
SecurityContext.NONE, true, "simple");
Health health = (Health) (response.block().getBody());
assertThat(health.getDetails()).containsEntry("counter", 42);
});
Expand All @@ -262,8 +265,8 @@ void runWhenHasReactiveHealthEndpointWebExtensionBeanDoesNotCreateExtraReactiveH
.run((context) -> {
ReactiveHealthEndpointWebExtension webExtension = context
.getBean(ReactiveHealthEndpointWebExtension.class);
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension
.health(SecurityContext.NONE, true, "simple");
Mono<WebEndpointResponse<? extends HealthComponent>> response = webExtension.health(ApiVersion.V3,
SecurityContext.NONE, true, "simple");
assertThat(response).isNull();
});
}
Expand Down
Expand Up @@ -21,7 +21,9 @@

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;

/**
Expand All @@ -35,14 +37,21 @@
*/
public class CompositeHealth extends HealthComponent {

private Status status;
private final Status status;

private Map<String, HealthComponent> details;
private final Map<String, HealthComponent> components;

CompositeHealth(Status status, Map<String, HealthComponent> details) {
private final Map<String, HealthComponent> details;

CompositeHealth(ApiVersion apiVersion, Status status, Map<String, HealthComponent> components) {
Assert.notNull(status, "Status must not be null");
this.status = status;
this.details = (details != null) ? new TreeMap<>(details) : details;
this.components = (apiVersion != ApiVersion.V3) ? null : sort(components);
this.details = (apiVersion != ApiVersion.V2) ? null : sort(components);
}

private Map<String, HealthComponent> sort(Map<String, HealthComponent> components) {
return (components != null) ? new TreeMap<>(components) : components;
}

@Override
Expand All @@ -51,7 +60,13 @@ public Status getStatus() {
}

@JsonInclude(Include.NON_EMPTY)
public Map<String, HealthComponent> getDetails() {
public Map<String, HealthComponent> getComponents() {
return this.components;
}

@JsonInclude(Include.NON_EMPTY)
@JsonProperty
Map<String, HealthComponent> getDetails() {
return this.details;
}

Expand Down
Expand Up @@ -65,6 +65,11 @@ private Health(Builder builder) {
this.details = Collections.unmodifiableMap(builder.details);
}

Health(Status status, Map<String, Object> details) {
this.status = status;
this.details = details;
}

/**
* Return the status of the health.
* @return the status (never {@code null})
Expand Down
Expand Up @@ -24,6 +24,7 @@
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.
Expand Down Expand Up @@ -61,12 +62,16 @@ public HealthEndpoint(HealthContributorRegistry registry, HealthEndpointGroups g

@ReadOperation
public HealthComponent health() {
return healthForPath(EMPTY_PATH);
return health(ApiVersion.V3, EMPTY_PATH);
}

@ReadOperation
public HealthComponent healthForPath(@Selector(match = Match.ALL_REMAINING) String... path) {
HealthResult<HealthComponent> result = getHealth(SecurityContext.NONE, true, path);
return health(ApiVersion.V3, path);
}

private HealthComponent health(ApiVersion apiVersion, String... path) {
HealthResult<HealthComponent> result = getHealth(apiVersion, SecurityContext.NONE, true, path);
return (result != null) ? result.getHealth() : null;
}

Expand All @@ -76,9 +81,9 @@ protected HealthComponent getHealth(HealthContributor contributor, boolean inclu
}

@Override
protected HealthComponent aggregateContributions(Map<String, HealthComponent> contributions,
protected HealthComponent aggregateContributions(ApiVersion apiVersion, Map<String, HealthComponent> contributions,
StatusAggregator statusAggregator, boolean includeDetails, Set<String> groupNames) {
return getCompositeHealth(contributions, statusAggregator, includeDetails, groupNames);
return getCompositeHealth(apiVersion, contributions, statusAggregator, includeDetails, groupNames);
}

}

0 comments on commit 69c561a

Please sign in to comment.