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.