From 19e211a1c2baa7a0488d10b00f9dcb9024b156bb Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2017 15:30:41 +0200 Subject: [PATCH] Add Status endpoint This commit adds a new `/application/status` endpoint that provides only the Health's status of an application. Previously, `/application/health` was returning full health details or only the status depending on configuration. Those two use cases are now separate in two endpoints that can be configured, secured and enabled separately. Closes gh-9721 --- .../endpoint/EndpointAutoConfiguration.java | 41 ++++-- ...ndpointManagementContextConfiguration.java | 26 +++- .../boot/actuate/endpoint/HealthEndpoint.java | 31 +---- .../boot/actuate/endpoint/StatusEndpoint.java | 49 +++++++ .../endpoint/web/HealthStatusHttpMapper.java | 127 ++++++++++++++++++ .../web/HealthWebEndpointExtension.java | 85 +----------- .../web/StatusWebEndpointExtension.java | 51 +++++++ .../health/HealthIndicatorFactory.java | 65 +++++++++ ...ntManagementContextConfigurationTests.java | 53 +++++++- .../actuate/endpoint/HealthEndpointTests.java | 45 +++---- .../actuate/endpoint/StatusEndpointTests.java | 59 ++++++++ ...rityHealthMvcEndpointIntegrationTests.java | 7 +- .../HealthEndpointWebIntegrationTests.java | 6 +- .../StatusEndpointWebIntegrationTests.java | 108 +++++++++++++++ .../health/HealthIndicatorFactoryTests.java | 69 ++++++++++ .../asciidoc/production-ready-features.adoc | 3 + 16 files changed, 667 insertions(+), 158 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/StatusEndpoint.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthStatusHttpMapper.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/StatusWebEndpointExtension.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorFactory.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/StatusEndpointTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/StatusEndpointWebIntegrationTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorFactoryTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index 60aeb31930..a935c6f945 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -39,10 +39,12 @@ import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.PublicMetrics; import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; +import org.springframework.boot.actuate.endpoint.StatusEndpoint; import org.springframework.boot.actuate.endpoint.ThreadDumpEndpoint; import org.springframework.boot.actuate.endpoint.TraceEndpoint; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.boot.actuate.trace.InMemoryTraceRepository; @@ -90,17 +92,6 @@ public class EndpointAutoConfiguration { return new EnvironmentEndpoint(environment); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnEnabledEndpoint - public HealthEndpoint healthEndpoint( - ObjectProvider healthAggregator, - ObjectProvider> healthIndicators) { - return new HealthEndpoint( - healthAggregator.getIfAvailable(() -> new OrderedHealthAggregator()), - healthIndicators.getIfAvailable(Collections::emptyMap)); - } - @Bean @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint @@ -182,6 +173,34 @@ public class EndpointAutoConfiguration { return new AuditEventsEndpoint(auditEventRepository); } + @Configuration + static class HealthEndpointConfiguration { + + private final HealthIndicator healthIndicator; + + HealthEndpointConfiguration(ObjectProvider healthAggregator, + ObjectProvider> healthIndicators) { + this.healthIndicator = new HealthIndicatorFactory().createHealthIndicator( + healthAggregator.getIfAvailable(OrderedHealthAggregator::new), + healthIndicators.getIfAvailable(Collections::emptyMap)); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public HealthEndpoint healthEndpoint() { + return new HealthEndpoint(this.healthIndicator); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public StatusEndpoint statusEndpoint() { + return new StatusEndpoint(this.healthIndicator); + } + + } + @Configuration @ConditionalOnBean(Flyway.class) @ConditionalOnClass(Flyway.class) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfiguration.java index 9d0f929c82..33a21f16e8 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfiguration.java @@ -20,10 +20,13 @@ import org.springframework.boot.actuate.autoconfigure.ManagementContextConfigura import org.springframework.boot.actuate.autoconfigure.endpoint.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.StatusEndpoint; import org.springframework.boot.actuate.endpoint.web.AuditEventsWebEndpointExtension; +import org.springframework.boot.actuate.endpoint.web.HealthStatusHttpMapper; import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension; import org.springframework.boot.actuate.endpoint.web.HeapDumpWebEndpoint; import org.springframework.boot.actuate.endpoint.web.LogFileWebEndpoint; +import org.springframework.boot.actuate.endpoint.web.StatusWebEndpointExtension; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -61,12 +64,27 @@ public class WebEndpointManagementContextConfiguration { @ConditionalOnBean(value = HealthEndpoint.class, search = SearchStrategy.CURRENT) public HealthWebEndpointExtension healthWebEndpointExtension(HealthEndpoint delegate, HealthWebEndpointExtensionProperties extensionProperties) { - HealthWebEndpointExtension webExtension = new HealthWebEndpointExtension( - delegate); + return new HealthWebEndpointExtension(delegate, + createHealthStatusHttpMapper(extensionProperties)); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + @ConditionalOnBean(value = StatusEndpoint.class, search = SearchStrategy.CURRENT) + public StatusWebEndpointExtension statusWebEndpointExtension(StatusEndpoint delegate, + HealthWebEndpointExtensionProperties extensionProperties) { + return new StatusWebEndpointExtension(delegate, + createHealthStatusHttpMapper(extensionProperties)); + } + + private HealthStatusHttpMapper createHealthStatusHttpMapper( + HealthWebEndpointExtensionProperties extensionProperties) { + HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper(); if (extensionProperties.getMapping() != null) { - webExtension.addStatusMapping(extensionProperties.getMapping()); + statusHttpMapper.addStatusMapping(extensionProperties.getMapping()); } - return webExtension; + return statusHttpMapper; } @Bean diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java index a352d57b60..a2f74e30ac 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/HealthEndpoint.java @@ -16,15 +16,10 @@ package org.springframework.boot.actuate.endpoint; -import java.util.Map; - -import org.springframework.boot.actuate.health.CompositeHealthIndicator; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.ReadOperation; -import org.springframework.util.Assert; /** * {@link Endpoint} to expose application health. @@ -40,18 +35,9 @@ public class HealthEndpoint { /** * Create a new {@link HealthEndpoint} instance. - * @param healthAggregator the health aggregator - * @param healthIndicators the health indicators + * @param healthIndicator the health indicator */ - public HealthEndpoint(HealthAggregator healthAggregator, - Map healthIndicators) { - Assert.notNull(healthAggregator, "HealthAggregator must not be null"); - Assert.notNull(healthIndicators, "HealthIndicators must not be null"); - CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator( - healthAggregator); - for (Map.Entry entry : healthIndicators.entrySet()) { - healthIndicator.addHealthIndicator(getKey(entry.getKey()), entry.getValue()); - } + public HealthEndpoint(HealthIndicator healthIndicator) { this.healthIndicator = healthIndicator; } @@ -60,17 +46,4 @@ public class HealthEndpoint { return this.healthIndicator.health(); } - /** - * Turns the bean name into a key that can be used in the map of health information. - * @param name the bean name - * @return the key - */ - private String getKey(String name) { - int index = name.toLowerCase().indexOf("healthindicator"); - if (index > 0) { - return name.substring(0, index); - } - return name; - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/StatusEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/StatusEndpoint.java new file mode 100644 index 0000000000..56c60811c5 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/StatusEndpoint.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.endpoint.Endpoint; +import org.springframework.boot.endpoint.ReadOperation; + +/** + * {@link Endpoint} to expose application {@link Status}. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +@Endpoint(id = "status") +public class StatusEndpoint { + + private final HealthIndicator healthIndicator; + + /** + * Create a new {@link StatusEndpoint} instance. + * @param healthIndicator the health indicator + */ + public StatusEndpoint(HealthIndicator healthIndicator) { + this.healthIndicator = healthIndicator; + } + + @ReadOperation + public Health health() { + return Health.status(this.healthIndicator.health().getStatus()).build(); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthStatusHttpMapper.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthStatusHttpMapper.java new file mode 100644 index 0000000000..8ca471aa04 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthStatusHttpMapper.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * Map a {@link Status} to an HTTP status code. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class HealthStatusHttpMapper { + + private Map statusMapping = new HashMap<>(); + + /** + * Create a new instance. + */ + public HealthStatusHttpMapper() { + setupDefaultStatusMapping(); + } + + private void setupDefaultStatusMapping() { + addStatusMapping(Status.DOWN, 503); + addStatusMapping(Status.OUT_OF_SERVICE, 503); + } + + /** + * Set specific status mappings. + * @param statusMapping a map of status code to {@link HttpStatus} + */ + public void setStatusMapping(Map statusMapping) { + Assert.notNull(statusMapping, "StatusMapping must not be null"); + this.statusMapping = new HashMap<>(statusMapping); + } + + /** + * Add specific status mappings to the existing set. + * @param statusMapping a map of status code to {@link HttpStatus} + */ + public void addStatusMapping(Map statusMapping) { + Assert.notNull(statusMapping, "StatusMapping must not be null"); + this.statusMapping.putAll(statusMapping); + } + + /** + * Add a status mapping to the existing set. + * @param status the status to map + * @param httpStatus the http status + */ + public void addStatusMapping(Status status, Integer httpStatus) { + Assert.notNull(status, "Status must not be null"); + Assert.notNull(httpStatus, "HttpStatus must not be null"); + addStatusMapping(status.getCode(), httpStatus); + } + + /** + * Add a status mapping to the existing set. + * @param statusCode the status code to map + * @param httpStatus the http status + */ + public void addStatusMapping(String statusCode, Integer httpStatus) { + Assert.notNull(statusCode, "StatusCode must not be null"); + Assert.notNull(httpStatus, "HttpStatus must not be null"); + this.statusMapping.put(statusCode, httpStatus); + } + + /** + * Return an immutable view of the status mapping. + * @return the http status codes mapped by status name + */ + public Map getStatusMapping() { + return Collections.unmodifiableMap(this.statusMapping); + } + + /** + * Map the specified {@link Status} to an HTTP status code. + * @param status the health {@link Status} + * @return the corresponding HTTP status code + */ + public int mapStatus(Status status) { + String code = getUniformValue(status.getCode()); + if (code != null) { + return this.statusMapping.keySet().stream() + .filter((key) -> code.equals(getUniformValue(key))) + .map(this.statusMapping::get).findFirst().orElse(200); + } + return 200; + } + + + + private String getUniformValue(String code) { + if (code == null) { + return null; + } + StringBuilder builder = new StringBuilder(); + for (char ch : code.toCharArray()) { + if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { + builder.append(Character.toLowerCase(ch)); + } + } + return builder.toString(); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthWebEndpointExtension.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthWebEndpointExtension.java index 3235b33282..01fcfa9e4f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthWebEndpointExtension.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/HealthWebEndpointExtension.java @@ -16,17 +16,11 @@ package org.springframework.boot.actuate.endpoint.web; -import java.util.HashMap; -import java.util.Map; - import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; import org.springframework.boot.endpoint.ReadOperation; import org.springframework.boot.endpoint.web.WebEndpointExtension; import org.springframework.boot.endpoint.web.WebEndpointResponse; -import org.springframework.http.HttpStatus; -import org.springframework.util.Assert; /** * {@link WebEndpointExtension} for the {@link HealthEndpoint}. @@ -42,88 +36,21 @@ import org.springframework.util.Assert; @WebEndpointExtension(endpoint = HealthEndpoint.class) public class HealthWebEndpointExtension { - private Map statusMapping = new HashMap<>(); - private final HealthEndpoint delegate; - public HealthWebEndpointExtension(HealthEndpoint delegate) { - this.delegate = delegate; - setupDefaultStatusMapping(); - } + private final HealthStatusHttpMapper statusHttpMapper; - private void setupDefaultStatusMapping() { - addStatusMapping(Status.DOWN, 503); - addStatusMapping(Status.OUT_OF_SERVICE, 503); - } - - /** - * Set specific status mappings. - * @param statusMapping a map of status code to {@link HttpStatus} - */ - public void setStatusMapping(Map statusMapping) { - Assert.notNull(statusMapping, "StatusMapping must not be null"); - this.statusMapping = new HashMap<>(statusMapping); - } - - /** - * Add specific status mappings to the existing set. - * @param statusMapping a map of status code to {@link HttpStatus} - */ - public void addStatusMapping(Map statusMapping) { - Assert.notNull(statusMapping, "StatusMapping must not be null"); - this.statusMapping.putAll(statusMapping); - } - - /** - * Add a status mapping to the existing set. - * @param status the status to map - * @param httpStatus the http status - */ - public void addStatusMapping(Status status, Integer httpStatus) { - Assert.notNull(status, "Status must not be null"); - Assert.notNull(httpStatus, "HttpStatus must not be null"); - addStatusMapping(status.getCode(), httpStatus); - } - - /** - * Add a status mapping to the existing set. - * @param statusCode the status code to map - * @param httpStatus the http status - */ - public void addStatusMapping(String statusCode, Integer httpStatus) { - Assert.notNull(statusCode, "StatusCode must not be null"); - Assert.notNull(httpStatus, "HttpStatus must not be null"); - this.statusMapping.put(statusCode, httpStatus); + public HealthWebEndpointExtension(HealthEndpoint delegate, + HealthStatusHttpMapper statusHttpMapper) { + this.delegate = delegate; + this.statusHttpMapper = statusHttpMapper; } @ReadOperation public WebEndpointResponse getHealth() { Health health = this.delegate.health(); - Integer status = getStatus(health); + Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); return new WebEndpointResponse<>(health, status); } - private int getStatus(Health health) { - String code = getUniformValue(health.getStatus().getCode()); - if (code != null) { - return this.statusMapping.keySet().stream() - .filter((key) -> code.equals(getUniformValue(key))) - .map(this.statusMapping::get).findFirst().orElse(200); - } - return 200; - } - - private String getUniformValue(String code) { - if (code == null) { - return null; - } - StringBuilder builder = new StringBuilder(); - for (char ch : code.toCharArray()) { - if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { - builder.append(Character.toLowerCase(ch)); - } - } - return builder.toString(); - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/StatusWebEndpointExtension.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/StatusWebEndpointExtension.java new file mode 100644 index 0000000000..f43b558f65 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/StatusWebEndpointExtension.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web; + +import org.springframework.boot.actuate.endpoint.StatusEndpoint; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.endpoint.ReadOperation; +import org.springframework.boot.endpoint.web.WebEndpointExtension; +import org.springframework.boot.endpoint.web.WebEndpointResponse; + +/** + * {@link WebEndpointExtension} for the {@link StatusEndpoint}. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +@WebEndpointExtension(endpoint = StatusEndpoint.class) +public class StatusWebEndpointExtension { + + private final StatusEndpoint delegate; + + private final HealthStatusHttpMapper statusHttpMapper; + + public StatusWebEndpointExtension(StatusEndpoint delegate, + HealthStatusHttpMapper statusHttpMapper) { + this.delegate = delegate; + this.statusHttpMapper = statusHttpMapper; + } + + @ReadOperation + public WebEndpointResponse getHealth() { + Health health = this.delegate.health(); + Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); + return new WebEndpointResponse<>(health, status); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorFactory.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorFactory.java new file mode 100644 index 0000000000..ec216065fa --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Factory to create a {@link HealthIndicator}. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +public class HealthIndicatorFactory { + + /** + * Create a {@link CompositeHealthIndicator} based on the specified health indicators. + * @param healthAggregator the {@link HealthAggregator} + * @param healthIndicators the {@link HealthIndicator} instances mapped by name + * @return an {@link HealthIndicator} that delegates to the specified + * {@code healthIndicators}. + */ + public HealthIndicator createHealthIndicator( + HealthAggregator healthAggregator, + Map healthIndicators) { + Assert.notNull(healthAggregator, "HealthAggregator must not be null"); + Assert.notNull(healthIndicators, "HealthIndicators must not be null"); + CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator( + healthAggregator); + for (Map.Entry entry : healthIndicators.entrySet()) { + healthIndicator.addHealthIndicator(getKey(entry.getKey()), entry.getValue()); + } + return healthIndicator; + } + + /** + * Turns the health indicator name into a key that can be used in the map of health + * information. + * @param name the health indicator name + * @return the key + */ + private String getKey(String name) { + int index = name.toLowerCase().indexOf("healthindicator"); + if (index > 0) { + return name.substring(0, index); + } + return name; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfigurationTests.java index c12f23587d..b1e47eda64 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointManagementContextConfigurationTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web; -import java.util.Collections; import java.util.Map; import org.junit.Test; @@ -24,11 +23,14 @@ import org.junit.Test; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.endpoint.AuditEventsEndpoint; import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.StatusEndpoint; import org.springframework.boot.actuate.endpoint.web.AuditEventsWebEndpointExtension; +import org.springframework.boot.actuate.endpoint.web.HealthStatusHttpMapper; import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension; import org.springframework.boot.actuate.endpoint.web.HeapDumpWebEndpoint; import org.springframework.boot.actuate.endpoint.web.LogFileWebEndpoint; -import org.springframework.boot.actuate.health.OrderedHealthAggregator; +import org.springframework.boot.actuate.endpoint.web.StatusWebEndpointExtension; +import org.springframework.boot.actuate.health.Health; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -42,6 +44,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link WebEndpointManagementContextConfiguration}. * * @author Andy Wilkinson + * @author Stephane Nicoll */ public class WebEndpointManagementContextConfigurationTests { @@ -71,8 +74,8 @@ public class WebEndpointManagementContextConfigurationTests { HealthWebEndpointExtension extension = context .getBean(HealthWebEndpointExtension.class); @SuppressWarnings("unchecked") - Map statusMappings = (Map) ReflectionTestUtils - .getField(extension, "statusMapping"); + Map statusMappings = ((HealthStatusHttpMapper) ReflectionTestUtils + .getField(extension, "statusHttpMapper")).getStatusMapping(); assertThat(statusMappings).containsEntry("DOWN", 503); assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503); assertThat(statusMappings).containsEntry("CUSTOM", 500); @@ -85,6 +88,35 @@ public class WebEndpointManagementContextConfigurationTests { "health", HealthEndpointConfiguration.class); } + @Test + public void statusWebEndpointExtensionIsAutoConfigured() { + beanIsAutoConfigured(StatusWebEndpointExtension.class, + StatusEndpointConfiguration.class); + } + + @Test + public void statusMappingCanBeCustomized() { + ApplicationContextRunner contextRunner = contextRunner() + .withPropertyValues("endpoints.health.mapping.CUSTOM=500") + .withUserConfiguration(StatusEndpointConfiguration.class); + contextRunner.run((context) -> { + StatusWebEndpointExtension extension = context + .getBean(StatusWebEndpointExtension.class); + @SuppressWarnings("unchecked") + Map statusMappings = ((HealthStatusHttpMapper) ReflectionTestUtils + .getField(extension, "statusHttpMapper")).getStatusMapping(); + assertThat(statusMappings).containsEntry("DOWN", 503); + assertThat(statusMappings).containsEntry("OUT_OF_SERVICE", 503); + assertThat(statusMappings).containsEntry("CUSTOM", 500); + }); + } + + @Test + public void statusWebEndpointExtensionCanBeDisabled() { + beanIsNotAutoConfiguredWhenEndpointIsDisabled(StatusWebEndpointExtension.class, + "status", StatusEndpointConfiguration.class); + } + @Test public void auditEventsWebEndpointExtensionIsAutoConfigured() { beanIsAutoConfigured(AuditEventsWebEndpointExtension.class, @@ -149,8 +181,17 @@ public class WebEndpointManagementContextConfigurationTests { @Bean public HealthEndpoint healthEndpoint() { - return new HealthEndpoint(new OrderedHealthAggregator(), - Collections.emptyMap()); + return new HealthEndpoint(() -> Health.up().build()); + } + + } + + @Configuration + static class StatusEndpointConfiguration { + + @Bean + public StatusEndpoint statusEndpoint() { + return new StatusEndpoint(() -> Health.up().build()); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/HealthEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/HealthEndpointTests.java index 72a21a8470..0eb59103cf 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/HealthEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/HealthEndpointTests.java @@ -23,10 +23,12 @@ import org.junit.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.Status; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link HealthEndpoint}. @@ -38,34 +40,27 @@ import static org.assertj.core.api.Assertions.assertThat; public class HealthEndpointTests { @Test - public void upAndUpIsAggregatedToUp() throws Exception { + public void statusAndFullDetailsAreExposed() { Map healthIndicators = new HashMap<>(); - healthIndicators.put("up", () -> new Health.Builder().status(Status.UP).build()); - healthIndicators.put("upAgain", - () -> new Health.Builder().status(Status.UP).build()); - HealthEndpoint endpoint = new HealthEndpoint(new OrderedHealthAggregator(), - healthIndicators); - assertThat(endpoint.health().getStatus()).isEqualTo(Status.UP); + healthIndicators.put("up", () -> new Health.Builder().status(Status.UP) + .withDetail("first", "1").build()); + healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP) + .withDetail("second", "2").build()); + HealthEndpoint endpoint = new HealthEndpoint(createHealthIndicator( + healthIndicators)); + Health health = endpoint.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsOnlyKeys("up", "upAgain"); + Health upHealth = (Health) health.getDetails().get("up"); + assertThat(upHealth.getDetails()).containsOnly(entry("first", "1")); + Health upAgainHealth = (Health) health.getDetails().get("upAgain"); + assertThat(upAgainHealth.getDetails()).containsOnly(entry("second", "2")); } - @Test - public void upAndDownIsAggregatedToDown() throws Exception { - Map healthIndicators = new HashMap<>(); - healthIndicators.put("up", () -> new Health.Builder().status(Status.UP).build()); - healthIndicators.put("down", - () -> new Health.Builder().status(Status.DOWN).build()); - HealthEndpoint endpoint = new HealthEndpoint(new OrderedHealthAggregator(), - healthIndicators); - assertThat(endpoint.health().getStatus()).isEqualTo(Status.DOWN); - } - - @Test - public void unknownStatusMapsToUnknown() throws Exception { - Map healthIndicators = new HashMap<>(); - healthIndicators.put("status", () -> new Health.Builder().status("FINE").build()); - HealthEndpoint endpoint = new HealthEndpoint(new OrderedHealthAggregator(), - healthIndicators); - assertThat(endpoint.health().getStatus()).isEqualTo(Status.UNKNOWN); + private HealthIndicator createHealthIndicator( + Map healthIndicators) { + return new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/StatusEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/StatusEndpointTests.java new file mode 100644 index 0000000000..2652397451 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/StatusEndpointTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; +import org.springframework.boot.actuate.health.OrderedHealthAggregator; +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StatusEndpoint}. + * + * @author Stephane Nicoll + */ +public class StatusEndpointTests { + + @Test + public void onlyStatusIsExposed() { + Map healthIndicators = new HashMap<>(); + healthIndicators.put("up", () -> new Health.Builder().status(Status.UP) + .withDetail("first", "1").build()); + healthIndicators.put("upAgain", () -> new Health.Builder().status(Status.UP) + .withDetail("second", "2").build()); + StatusEndpoint endpoint = new StatusEndpoint(createHealthIndicator( + healthIndicators)); + Health health = endpoint.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).isEmpty(); + } + + private HealthIndicator createHealthIndicator( + Map healthIndicators) { + return new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java index 4329b23ece..f3aa49b179 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/NoSpringSecurityHealthMvcEndpointIntegrationTests.java @@ -26,9 +26,11 @@ import org.springframework.boot.actuate.autoconfigure.ManagementContextAutoConfi import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.EndpointInfrastructureAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.infrastructure.ServletEndpointAutoConfiguration; import org.springframework.boot.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.actuate.endpoint.web.HealthStatusHttpMapper; import org.springframework.boot.actuate.endpoint.web.HealthWebEndpointExtension; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -97,13 +99,14 @@ public class NoSpringSecurityHealthMvcEndpointIntegrationTests { @Bean public HealthEndpoint healthEndpoint( Map healthIndicators) { - return new HealthEndpoint(new OrderedHealthAggregator(), healthIndicators); + return new HealthEndpoint(new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators)); } @Bean public HealthWebEndpointExtension healthWebEndpointExtension( HealthEndpoint delegate) { - return new HealthWebEndpointExtension(delegate); + return new HealthWebEndpointExtension(delegate, new HealthStatusHttpMapper()); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/HealthEndpointWebIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/HealthEndpointWebIntegrationTests.java index 6b063eb327..5db8b005b6 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/HealthEndpointWebIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/HealthEndpointWebIntegrationTests.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.springframework.boot.actuate.endpoint.HealthEndpoint; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -67,13 +68,14 @@ public class HealthEndpointWebIntegrationTests { @Bean public HealthEndpoint healthEndpoint( Map healthIndicators) { - return new HealthEndpoint(new OrderedHealthAggregator(), healthIndicators); + return new HealthEndpoint(new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators)); } @Bean public HealthWebEndpointExtension healthWebEndpointExtension( HealthEndpoint delegate) { - return new HealthWebEndpointExtension(delegate); + return new HealthWebEndpointExtension(delegate, new HealthStatusHttpMapper()); } @Bean diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/StatusEndpointWebIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/StatusEndpointWebIntegrationTests.java new file mode 100644 index 0000000000..14f316ea68 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/StatusEndpointWebIntegrationTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.actuate.endpoint.StatusEndpoint; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorFactory; +import org.springframework.boot.actuate.health.OrderedHealthAggregator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for {@link StatusEndpoint} and {@link StatusWebEndpointExtension} + * exposed by Jersey, Spring MVC, and WebFlux. + * + * @author Stephane Nicoll + */ +@RunWith(WebEndpointsRunner.class) +public class StatusEndpointWebIntegrationTests { + + private static WebTestClient client; + + private static ConfigurableApplicationContext context; + + @Test + public void whenStatusIsUp200ResponseIsReturned() throws Exception { + client.get().uri("/application/status").exchange().expectStatus().isOk() + .expectBody().json("{\"status\":\"UP\"}"); + } + + @Test + public void whenStatusIsDown503ResponseIsReturned() throws Exception { + context.getBean("alphaHealthIndicator", TestHealthIndicator.class) + .setHealth(Health.down().build()); + client.get().uri("/application/status").exchange().expectStatus() + .isEqualTo(HttpStatus.SERVICE_UNAVAILABLE) + .expectBody().json("{\"status\":\"DOWN\"}"); + } + + @Configuration + public static class TestConfiguration { + + @Bean + public StatusEndpoint statusEndpoint( + Map healthIndicators) { + return new StatusEndpoint(new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators)); + } + + @Bean + public StatusWebEndpointExtension statusWebEndpointExtension( + StatusEndpoint delegate) { + return new StatusWebEndpointExtension(delegate, new HealthStatusHttpMapper()); + } + + @Bean + public TestHealthIndicator alphaHealthIndicator() { + return new TestHealthIndicator(); + } + + @Bean + public TestHealthIndicator bravoHealthIndicator() { + return new TestHealthIndicator(); + } + + } + + private static class TestHealthIndicator implements HealthIndicator { + + private Health health = Health.up().build(); + + @Override + public Health health() { + Health result = this.health; + this.health = Health.up().build(); + return result; + } + + void setHealth(Health health) { + this.health = health; + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorFactoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorFactoryTests.java new file mode 100644 index 0000000000..2101f09cea --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorFactoryTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.health; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HealthIndicatorFactory}. + * + * @author Phillip Webb + * @author Christian Dupuis + * @author Andy Wilkinson + */ +public class HealthIndicatorFactoryTests { + + @Test + public void upAndUpIsAggregatedToUp() throws Exception { + Map healthIndicators = new HashMap<>(); + healthIndicators.put("up", () -> new Health.Builder().status(Status.UP).build()); + healthIndicators.put("upAgain", + () -> new Health.Builder().status(Status.UP).build()); + HealthIndicator healthIndicator = createHealthIndicator(healthIndicators); + assertThat(healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + public void upAndDownIsAggregatedToDown() throws Exception { + Map healthIndicators = new HashMap<>(); + healthIndicators.put("up", () -> new Health.Builder().status(Status.UP).build()); + healthIndicators.put("down", + () -> new Health.Builder().status(Status.DOWN).build()); + HealthIndicator healthIndicator = createHealthIndicator(healthIndicators); + assertThat(healthIndicator.health().getStatus()).isEqualTo(Status.DOWN); + } + + @Test + public void unknownStatusMapsToUnknown() throws Exception { + Map healthIndicators = new HashMap<>(); + healthIndicators.put("status", () -> new Health.Builder().status("FINE").build()); + HealthIndicator healthIndicator = createHealthIndicator(healthIndicators); + assertThat(healthIndicator.health().getStatus()).isEqualTo(Status.UNKNOWN); + } + + private HealthIndicator createHealthIndicator( + Map healthIndicators) { + return new HealthIndicatorFactory().createHealthIndicator( + new OrderedHealthAggregator(), healthIndicators); + } + +} diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 3112acc608..666a401b3b 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -109,6 +109,9 @@ The following technology agnostic endpoints are available: |`shutdown` |Allows the application to be gracefully shutdown (not enabled by default). +|`status` +|Show application status information (i.e. `health` status with no additional details) + |`threaddump` |Performs a thread dump.