From c166a8a241ae968c4a3d343d3bff41447e860193 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Mon, 18 Jan 2016 21:17:55 +0100 Subject: [PATCH] Introduce HealthIndicatorRegistry This commit introduces HealthIndicatorRegistry which handles registration of HealthIndicator instances. Registering new HealthIndicator instances is now possible in runtime. --- .../EndpointAutoConfiguration.java | 11 ++- .../HealthIndicatorAutoConfiguration.java | 18 +++- .../boot/actuate/endpoint/HealthEndpoint.java | 40 +++----- .../DefaultHealthIndicatorRegistry.java | 69 +++++++++++++ .../health/HealthIndicatorRegistry.java | 65 +++++++++++++ ...althMvcEndpointAutoConfigurationTests.java | 12 ++- .../actuate/endpoint/HealthEndpointTests.java | 19 +++- .../DefaultHealthIndicatorRegistryTest.java | 96 +++++++++++++++++++ .../asciidoc/production-ready-features.adoc | 10 +- 9 files changed, 299 insertions(+), 41 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTest.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index 919586a2ab00..6c3268f445d6 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -46,8 +45,9 @@ import org.springframework.boot.actuate.endpoint.RequestMappingEndpoint; import org.springframework.boot.actuate.endpoint.ShutdownEndpoint; import org.springframework.boot.actuate.endpoint.TraceEndpoint; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry; import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.trace.TraceRepository; @@ -81,6 +81,7 @@ * @author Christian Dupuis * @author Stephane Nicoll * @author EddĂș MelĂ©ndez + * @author Vedran Pavic */ @Configuration @AutoConfigureAfter({ FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class }) @@ -94,7 +95,7 @@ public class EndpointAutoConfiguration { private HealthAggregator healthAggregator = new OrderedHealthAggregator(); @Autowired(required = false) - private Map healthIndicators = new HashMap(); + private HealthIndicatorRegistry healthIndicatorRegistry = new DefaultHealthIndicatorRegistry(); @Autowired(required = false) private Collection publicMetrics; @@ -111,7 +112,7 @@ public EnvironmentEndpoint environmentEndpoint() { @Bean @ConditionalOnMissingBean public HealthEndpoint healthEndpoint() { - return new HealthEndpoint(this.healthAggregator, this.healthIndicators); + return new HealthEndpoint(this.healthAggregator, this.healthIndicatorRegistry); } @Bean diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java index c843105d726c..e2e1cfa1684b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/HealthIndicatorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -33,12 +33,14 @@ import org.springframework.boot.actuate.health.CassandraHealthIndicator; import org.springframework.boot.actuate.health.CompositeHealthIndicator; import org.springframework.boot.actuate.health.DataSourceHealthIndicator; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry; import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator; import org.springframework.boot.actuate.health.DiskSpaceHealthIndicatorProperties; import org.springframework.boot.actuate.health.ElasticsearchHealthIndicator; import org.springframework.boot.actuate.health.ElasticsearchHealthIndicatorProperties; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.JmsHealthIndicator; import org.springframework.boot.actuate.health.MailHealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; @@ -84,6 +86,7 @@ * @author Stephane Nicoll * @author Phillip Webb * @author Tommy Ludwig + * @author Vedran Pavic * @since 1.1.0 */ @Configuration @@ -110,6 +113,19 @@ public OrderedHealthAggregator healthAggregator() { return healthAggregator; } + @Bean + @ConditionalOnMissingBean(HealthIndicatorRegistry.class) + public HealthIndicatorRegistry healthIndicatorRegistry( + Map healthIndicators) { + HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(); + for (Map.Entry entry : healthIndicators.entrySet()) { + String name = entry.getKey(); + int index = name.toLowerCase().indexOf("healthindicator"); + registry.register(index > 0 ? name.substring(0, index) : name, entry.getValue()); + } + return registry; + } + @Bean @ConditionalOnMissingBean(HealthIndicator.class) public ApplicationHealthIndicator applicationHealthIndicator() { 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 7e171bab6801..8ccb64358d91 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -16,12 +16,11 @@ 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.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; @@ -31,11 +30,13 @@ * @author Dave Syer * @author Christian Dupuis * @author Andy Wilkinson + * @author Vedran Pavic */ @ConfigurationProperties(prefix = "endpoints.health", ignoreUnknownFields = true) public class HealthEndpoint extends AbstractEndpoint { - private final HealthIndicator healthIndicator; + private final HealthAggregator healthAggregator; + private final HealthIndicatorRegistry healthIndicatorRegistry; /** * Time to live for cached result, in milliseconds. @@ -45,19 +46,16 @@ public class HealthEndpoint extends AbstractEndpoint { /** * Create a new {@link HealthIndicator} instance. * @param healthAggregator the health aggregator - * @param healthIndicators the health indicators + * @param healthIndicatorRegistry the health indicator registry */ public HealthEndpoint(HealthAggregator healthAggregator, - Map healthIndicators) { + HealthIndicatorRegistry healthIndicatorRegistry) { super("health", false); 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()); - } - this.healthIndicator = healthIndicator; + Assert.notNull(healthIndicatorRegistry, + "healthIndicatorRegistry must not be null"); + this.healthAggregator = healthAggregator; + this.healthIndicatorRegistry = healthIndicatorRegistry; } /** @@ -78,19 +76,9 @@ public void setTimeToLive(long ttl) { */ @Override public Health invoke() { - return this.healthIndicator.health(); + HealthIndicator indicator = new CompositeHealthIndicator( + this.healthAggregator, this.healthIndicatorRegistry.getAll()); + return indicator.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/health/DefaultHealthIndicatorRegistry.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java new file mode 100644 index 000000000000..b8bd650626f6 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2016 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.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Default implementation of {@link HealthIndicatorRegistry}. + * + * @author Vedran Pavic + */ +public class DefaultHealthIndicatorRegistry implements HealthIndicatorRegistry { + + private final Map healthIndicators = + new HashMap(); + + @Override + public void register(String name, HealthIndicator healthIndicator) { + Assert.notNull(healthIndicator, "HealthIndicator must not be null"); + synchronized (this.healthIndicators) { + if (this.healthIndicators.get(name) != null) { + throw new IllegalStateException( + "HealthIndicator with name '" + name + "' already registered"); + } + this.healthIndicators.put(name, healthIndicator); + } + } + + @Override + public HealthIndicator unregister(String name) { + synchronized (this.healthIndicators) { + return this.healthIndicators.remove(name); + } + } + + @Override + public HealthIndicator get(String name) { + synchronized (this.healthIndicators) { + return this.healthIndicators.get(name); + } + } + + @Override + public Map getAll() { + synchronized (this.healthIndicators) { + return Collections.unmodifiableMap( + new HashMap(this.healthIndicators)); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java new file mode 100644 index 000000000000..6b2d2cacf006 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistry.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2016 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; + +/** + * A registry of {@link HealthIndicator}s. + *

+ * Implementations must be thread-safe. + * + * @author Andy Wilkinson + * @author Vedran Pavic + */ +public interface HealthIndicatorRegistry { + + /** + * Registers the given {@code healthIndicator}, associating it with the given + * {@code name}. + * @param name the name of the indicator + * @param healthIndicator the indicator + * @throws IllegalStateException if an indicator with the given {@code name} is + * already registered. + */ + void register(String name, HealthIndicator healthIndicator); + + /** + * Unregisters the {@code HealthIndicator} previously registered with the given + * {@code name}. + * @param name the name of the indicator + * @return the unregistered indicator, or {@code null} if no indicator was found in + * the registry for the given {@code name}. + */ + HealthIndicator unregister(String name); + + /** + * Returns the health indicator registered with the given {@code name}. + * @param name the name of the indicator + * @return the health indicator, or {@code null} if no indicator was registered with + * the given {@code name}. + */ + HealthIndicator get(String name); + + /** + * Returns a snapshot of the registered health indicators and their names. The + * contents of the map do not reflect subsequent changes to the registry. + * @return the snapshot of registered health indicators + */ + Map getAll(); + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java index 0c00865d8626..942b9c4f4efb 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -21,8 +21,10 @@ import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.Status; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; @@ -42,6 +44,7 @@ * * @author Dave Syer * @author Andy Wilkinson + * @author Vedran Pavic */ public class HealthMvcEndpointAutoConfigurationTests { @@ -94,6 +97,13 @@ public TestHealthIndicator testHealthIndicator() { return new TestHealthIndicator(); } + @Bean + public HealthIndicatorRegistry healthIndicatorRegistry() { + DefaultHealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(); + registry.register("test", testHealthIndicator()); + return registry; + } + } static class TestHealthIndicator extends AbstractHealthIndicator { 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 3087538780e7..9cdf810bf897 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -16,13 +16,13 @@ package org.springframework.boot.actuate.endpoint; -import java.util.Map; - import org.junit.Test; +import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry; 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.actuate.health.HealthIndicatorRegistry; import org.springframework.boot.actuate.health.OrderedHealthAggregator; import org.springframework.boot.actuate.health.Status; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -37,6 +37,7 @@ * * @author Phillip Webb * @author Christian Dupuis + * @author Vedran Pavic */ public class HealthEndpointTests extends AbstractEndpointTests { @@ -56,8 +57,8 @@ public static class Config { @Bean public HealthEndpoint endpoint(HealthAggregator healthAggregator, - Map healthIndicators) { - return new HealthEndpoint(healthAggregator, healthIndicators); + HealthIndicatorRegistry healthIndicatorRegistry) { + return new HealthEndpoint(healthAggregator, healthIndicatorRegistry); } @Bean @@ -75,5 +76,13 @@ public Health health() { public HealthAggregator healthAggregator() { return new OrderedHealthAggregator(); } + + @Bean + public HealthIndicatorRegistry healthIndicatorRegistry() { + HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(); + registry.register("status", statusHealthIndicator()); + return registry; + } + } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTest.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTest.java new file mode 100644 index 000000000000..627552e8965d --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2016 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 org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link DefaultHealthIndicatorRegistry}. + * + * @author Vedran Pavic + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultHealthIndicatorRegistryTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private HealthIndicator one; + + @Mock + private HealthIndicator two; + + private DefaultHealthIndicatorRegistry registry; + + @Before + public void setup() { + given(this.one.health()).willReturn(new Health.Builder().up().build()); + given(this.two.health()).willReturn(new Health.Builder().unknown().build()); + + this.registry = new DefaultHealthIndicatorRegistry(); + } + + @Test + public void register() { + this.registry.register("one", this.one); + this.registry.register("two", this.two); + assertThat(this.registry.getAll().size(), equalTo(2)); + assertThat(this.registry.get("one"), is(this.one)); + assertThat(this.registry.get("two"), is(this.two)); + } + + @Test + public void registerAlreadyUsedName() { + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("HealthIndicator with name 'one' already registered"); + this.registry.register("one", this.one); + this.registry.register("one", this.two); + } + + @Test + public void unregister() { + this.registry.register("one", this.one); + this.registry.register("two", this.two); + assertThat(this.registry.getAll().size(), equalTo(2)); + HealthIndicator two = this.registry.unregister("two"); + assertThat(two, is(this.two)); + assertThat(this.registry.getAll().size(), equalTo(1)); + } + + @Test + public void unregisterNotKnown() { + this.registry.register("one", this.one); + assertThat(this.registry.getAll().size(), equalTo(1)); + HealthIndicator two = this.registry.unregister("two"); + assertThat(two, nullValue()); + assertThat(this.registry.getAll().size(), equalTo(1)); + } + +} 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 c70007b86559..987f5c712017 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -255,9 +255,10 @@ returned, and for an authenticated connection additional details are also displa <> for HTTP details). Health information is collected from all -{sc-spring-boot-actuator}/health/HealthIndicator.{sc-ext}[`HealthIndicator`] beans defined -in your `ApplicationContext`. Spring Boot includes a number of auto-configured -`HealthIndicators` and you can also write your own. +{sc-spring-boot-actuator}/health/HealthIndicator.{sc-ext}[`HealthIndicator`] instances +registered with {sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[`HealthIndicatorRegistry`]. +Spring Boot includes a number of auto-configured `HealthIndicators` and you can also write +your own. @@ -347,6 +348,9 @@ NOTE: The identifier for a given `HealthIndicator` is the name of the bean witho `HealthIndicator` suffix if it exists. In the example above, the health information will be available in an entry named `my`. +Additionally, you can register (and unregister) `HealthIndicator` instances in runtime +using {sc-spring-boot-actuator}/health/HealthIndicatorRegistry.{sc-ext}[`HealthIndicatorRegistry`]. + In addition to Spring Boot's predefined {sc-spring-boot-actuator}/health/Status.{sc-ext}[`Status`] types, it is also possible for `Health` to return a custom `Status` that represents a new system state. In such cases a custom implementation of the