From 78d7fe9cb5dd6831ad03adbe384d2f8cc15f7266 Mon Sep 17 00:00:00 2001 From: Mattias Severson Date: Sun, 27 Jul 2014 20:29:08 +0200 Subject: [PATCH 1/2] Add HealthIndicator that checks free disk space See gh-1297 --- .../health/DiskSpaceHealthIndicator.java | 82 +++++++++++++++++ .../health/DiskSpaceHealthIndicatorTests.java | 89 +++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java new file mode 100644 index 0000000000..7fa98bcdce --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014 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.io.File; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * A {@link HealthIndicator} that checks for available disk space. + * + * @author Mattias Severson + * @since 1.2.0 + */ +@Component +public class DiskSpaceHealthIndicator extends AbstractHealthIndicator { + private static Log logger = LogFactory.getLog(DiskSpaceHealthIndicator.class); + + private final File path; + private final long thresholdBytes; + + /** + * Constructor. + * @param path The path to a directory for checking available disk space. The + * application must have read access to this path. Additionally, the + * {@link java.lang.SecurityManager#checkRead(String)} will be called + * if a security manager is used. + * By default, it uses the system property {@code user.dir}, i.e. the + * current directory when the JVM was started. + * @param thresholdBytes The threshold (in Bytes) of remaining disk space that will + * trigger this health indicator when exceeded. + * Default value is 10485760 Bytes (10 MB). + */ + @Autowired + public DiskSpaceHealthIndicator(@Value("${health.path?:${user.dir}}") String path, + @Value("${health.threshold.bytes:10485760}") long thresholdBytes) { + this.path = new File(path); + this.thresholdBytes = thresholdBytes; + verifyPathIsAccessible(this.path); + Assert.isTrue(thresholdBytes >= 0, "thresholdBytes must be greater than 0"); + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + long diskFreeInBytes = this.path.getFreeSpace(); + if (diskFreeInBytes >= this.thresholdBytes) { + builder.up(); + } else { + logger.warn(String.format("Free disk space threshold exceeded. " + + "Available: %d bytes (threshold: %d bytes).", + diskFreeInBytes, this.thresholdBytes)); + builder.down(); + } + } + + private static void verifyPathIsAccessible(File path) { + if (!path.exists()) { + throw new IllegalArgumentException(String.format("Path does not exist: %s", path)); + } + if (!path.canRead()) { + throw new IllegalStateException(String.format("Path cannot be read: %s", path)); + } + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java new file mode 100644 index 0000000000..30bc8f7fdf --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 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.io.File; +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 org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DiskSpaceHealthIndicator}. + * + * @author Mattias Severson + */ +@RunWith(MockitoJUnitRunner.class) +public class DiskSpaceHealthIndicatorTests { + + static final long THRESHOLD_BYTES = 1024; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Mock + File fileMock; + + HealthIndicator healthIndicator; + + @Before + public void setUp() throws Exception { + this.healthIndicator = + new DiskSpaceHealthIndicator(DiskSpaceHealthIndicatorTests.class.getResource("").getPath(), + THRESHOLD_BYTES); + ReflectionTestUtils.setField(this.healthIndicator, "path", this.fileMock); + } + + @Test + public void diskSpaceIsUp() throws Exception { + when(this.fileMock.getFreeSpace()).thenReturn(THRESHOLD_BYTES + 10); + + Health health = this.healthIndicator.health(); + assertEquals(Status.UP, health.getStatus()); + } + + @Test + public void diskSpaceIsDown() throws Exception { + when(this.fileMock.getFreeSpace()).thenReturn(THRESHOLD_BYTES - 10); + + Health health = this.healthIndicator.health(); + assertEquals(Status.DOWN, health.getStatus()); + } + + @Test + public void throwsExceptionForUnknownPath() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Path does not exist: an_path_that_does_not_exist"); + + new DiskSpaceHealthIndicator("an_path_that_does_not_exist", THRESHOLD_BYTES); + } + + @Test + public void throwsExceptionForNegativeThreshold() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("thresholdBytes must be greater than 0"); + + new DiskSpaceHealthIndicator(DiskSpaceHealthIndicatorTests.class.getResource("").getPath(), -1); + } +} \ No newline at end of file From 97178915a429aeb66fef827ce189a060d5dfb79d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 8 Oct 2014 17:02:07 +0100 Subject: [PATCH 2/2] Polish disk space health indicator - Supply auto-configuration for the new indicator - As suggested in the pull request, include the free disk space and configured threshold in the health details - Update the documentation to describe the indicator and its two configuration settings - Use @ConfigurationProperties to bind the indicator's configuration. This should make the changes sympathetic to the work being done to automate the configuration properties documentation Closes gh-1297 --- .../HealthIndicatorAutoConfiguration.java | 21 +++++++ .../health/DiskSpaceHealthIndicator.java | 56 ++++++----------- .../DiskSpaceHealthIndicatorProperties.java | 60 +++++++++++++++++++ .../health/DiskSpaceHealthIndicatorTests.java | 33 +++++----- .../appendix-application-properties.adoc | 4 ++ .../asciidoc/production-ready-features.adoc | 8 +-- 6 files changed, 120 insertions(+), 62 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorProperties.java 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 c6885e11d4..62c4b0af56 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 @@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.health.ApplicationHealthIndicator; import org.springframework.boot.actuate.health.CompositeHealthIndicator; import org.springframework.boot.actuate.health.DataSourceHealthIndicator; +import org.springframework.boot.actuate.health.DiskSpaceHealthIndicator; +import org.springframework.boot.actuate.health.DiskSpaceHealthIndicatorProperties; import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.MongoHealthIndicator; @@ -52,6 +54,7 @@ import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.MongoTemplate; @@ -256,4 +259,22 @@ public class HealthIndicatorAutoConfiguration { } } + @Configuration + @ConditionalOnExpression("${health.diskspace.enabled:true}") + public static class DiskSpaceHealthIndicatorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "diskSpaceHealthIndicator") + public HealthIndicator diskSpaceHealthIndicator( + DiskSpaceHealthIndicatorProperties properties) { + return new DiskSpaceHealthIndicator(properties); + } + + @Bean + @ConfigurationProperties("health.diskspace") + public DiskSpaceHealthIndicatorProperties diskSpaceHealthIndicatorProperties() { + return new DiskSpaceHealthIndicatorProperties(); + } + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java index 7fa98bcdce..37c129e359 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicator.java @@ -16,67 +16,45 @@ package org.springframework.boot.actuate.health; -import java.io.File; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; /** - * A {@link HealthIndicator} that checks for available disk space. + * A {@link HealthIndicator} that checks available disk space and reports a status of + * {@link Status#DOWN} when it drops below a configurable threshold. * * @author Mattias Severson + * @author Andy Wilkinson * @since 1.2.0 */ -@Component public class DiskSpaceHealthIndicator extends AbstractHealthIndicator { + private static Log logger = LogFactory.getLog(DiskSpaceHealthIndicator.class); - private final File path; - private final long thresholdBytes; + private final DiskSpaceHealthIndicatorProperties properties; /** - * Constructor. - * @param path The path to a directory for checking available disk space. The - * application must have read access to this path. Additionally, the - * {@link java.lang.SecurityManager#checkRead(String)} will be called - * if a security manager is used. - * By default, it uses the system property {@code user.dir}, i.e. the - * current directory when the JVM was started. - * @param thresholdBytes The threshold (in Bytes) of remaining disk space that will - * trigger this health indicator when exceeded. - * Default value is 10485760 Bytes (10 MB). + * Create a new {@code DiskSpaceHealthIndicator} */ @Autowired - public DiskSpaceHealthIndicator(@Value("${health.path?:${user.dir}}") String path, - @Value("${health.threshold.bytes:10485760}") long thresholdBytes) { - this.path = new File(path); - this.thresholdBytes = thresholdBytes; - verifyPathIsAccessible(this.path); - Assert.isTrue(thresholdBytes >= 0, "thresholdBytes must be greater than 0"); + public DiskSpaceHealthIndicator(DiskSpaceHealthIndicatorProperties properties) { + this.properties = properties; } @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - long diskFreeInBytes = this.path.getFreeSpace(); - if (diskFreeInBytes >= this.thresholdBytes) { + long diskFreeInBytes = this.properties.getPath().getFreeSpace(); + if (diskFreeInBytes >= this.properties.getThreshold()) { builder.up(); - } else { - logger.warn(String.format("Free disk space threshold exceeded. " + - "Available: %d bytes (threshold: %d bytes).", - diskFreeInBytes, this.thresholdBytes)); + } + else { + logger.warn(String.format("Free disk space below threshold. " + + "Available: %d bytes (threshold: %d bytes)", diskFreeInBytes, + this.properties.getThreshold())); builder.down(); } - } - - private static void verifyPathIsAccessible(File path) { - if (!path.exists()) { - throw new IllegalArgumentException(String.format("Path does not exist: %s", path)); - } - if (!path.canRead()) { - throw new IllegalStateException(String.format("Path cannot be read: %s", path)); - } + builder.withDetail("free", diskFreeInBytes).withDetail("threshold", + this.properties.getThreshold()); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorProperties.java new file mode 100644 index 0000000000..2881eb55ff --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorProperties.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2014 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.io.File; + +import org.springframework.util.Assert; + +/** + * External configuration properties for {@link DiskSpaceHealthIndicator} + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class DiskSpaceHealthIndicatorProperties { + + private File path = new File("."); + + private long threshold = 10 * 1024 * 1024; + + public File getPath() { + return this.path; + } + + public void setPath(File path) { + if (!path.exists()) { + throw new IllegalArgumentException(String.format("Path '%s' does not exist", + path)); + } + if (!path.canRead()) { + throw new IllegalStateException(String.format("Path '%s' cannot be read", + path)); + } + this.path = path; + } + + public long getThreshold() { + + return this.threshold; + } + + public void setThreshold(long threshold) { + Assert.isTrue(threshold >= 0, "threshold must be greater than 0"); + this.threshold = threshold; + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java index 30bc8f7fdf..3797884f97 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DiskSpaceHealthIndicatorTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.health; import java.io.File; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -24,7 +25,6 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @@ -49,10 +49,10 @@ public class DiskSpaceHealthIndicatorTests { @Before public void setUp() throws Exception { - this.healthIndicator = - new DiskSpaceHealthIndicator(DiskSpaceHealthIndicatorTests.class.getResource("").getPath(), - THRESHOLD_BYTES); - ReflectionTestUtils.setField(this.healthIndicator, "path", this.fileMock); + when(this.fileMock.exists()).thenReturn(true); + when(this.fileMock.canRead()).thenReturn(true); + this.healthIndicator = new DiskSpaceHealthIndicator(createProperties( + this.fileMock, THRESHOLD_BYTES)); } @Test @@ -61,6 +61,8 @@ public class DiskSpaceHealthIndicatorTests { Health health = this.healthIndicator.health(); assertEquals(Status.UP, health.getStatus()); + assertEquals(THRESHOLD_BYTES, health.getDetails().get("threshold")); + assertEquals(THRESHOLD_BYTES + 10, health.getDetails().get("free")); } @Test @@ -69,21 +71,14 @@ public class DiskSpaceHealthIndicatorTests { Health health = this.healthIndicator.health(); assertEquals(Status.DOWN, health.getStatus()); + assertEquals(THRESHOLD_BYTES, health.getDetails().get("threshold")); + assertEquals(THRESHOLD_BYTES - 10, health.getDetails().get("free")); } - @Test - public void throwsExceptionForUnknownPath() { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("Path does not exist: an_path_that_does_not_exist"); - - new DiskSpaceHealthIndicator("an_path_that_does_not_exist", THRESHOLD_BYTES); - } - - @Test - public void throwsExceptionForNegativeThreshold() { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("thresholdBytes must be greater than 0"); - - new DiskSpaceHealthIndicator(DiskSpaceHealthIndicatorTests.class.getResource("").getPath(), -1); + private DiskSpaceHealthIndicatorProperties createProperties(File path, long threshold) { + DiskSpaceHealthIndicatorProperties properties = new DiskSpaceHealthIndicatorProperties(); + properties.setPath(path); + properties.setThreshold(threshold); + return properties; } } \ No newline at end of file diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 0bc598304c..f3789bfe6c 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -387,6 +387,10 @@ content into your application; rather pick only the properties that you need. endpoints.trace.sensitive=true endpoints.trace.enabled=true + # HEALTH INDICATORS + health.diskspace.path=. + health.diskspace.threshold=10485760 + # MVC ONLY ENDPOINTS endpoints.jolokia.path=jolokia endpoints.jolokia.sensitive=true 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 0994f1b8a4..179d3b1816 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -165,10 +165,10 @@ To provide custom health information you can register a Spring bean that impleme Spring Boot provides a {sc-spring-boot-actuator}/health/DataSourceHealthIndicator.{sc-ext}[`DataSourceHealthIndicator`] implementation that attempts a simple database test (reusing the validation query set on the data -source, if any) as well as implementations for Redis, MongoDB and RabbitMQ. - -Spring Boot adds the `HealthIndicator` instances automatically if beans of type `DataSource`, -`MongoTemplate`, `RedisConnectionFactory`, `RabbitTemplate` are present in the `ApplicationContext`. +source, if any) as well as implementations for Redis, MongoDB and RabbitMQ. Spring Boot adds the +`HealthIndicator` instances automatically if beans of type `DataSource`, `MongoTemplate`, +`RedisConnectionFactory`, and `RabbitTemplate` respectively are present in the +`ApplicationContext`. A health indicator that checks free disk space is also provided. Besides implementing custom a `HealthIndicator` type and using out-of-box {sc-spring-boot-actuator}/health/Status.{sc-ext}[`Status`] types, it is also possible to introduce custom `Status` types for different or more complex system