Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple paths in DiskSpaceHealthIndicator #27663

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
Expand All @@ -16,22 +16,30 @@

package org.springframework.boot.actuate.autoconfigure.system;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthIndicatorProperties.PathInfo;
import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;

/**
* {@link EnableAutoConfiguration Auto-configuration} for
* {@link DiskSpaceHealthIndicator}.
*
* @author Mattias Severson
* @author Andy Wilkinson
* @author Chris Bono
* @since 2.0.0
*/
@Configuration(proxyBeanMethods = false)
Expand All @@ -43,7 +51,9 @@ public class DiskSpaceHealthContributorAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "diskSpaceHealthIndicator")
public DiskSpaceHealthIndicator diskSpaceHealthIndicator(DiskSpaceHealthIndicatorProperties properties) {
return new DiskSpaceHealthIndicator(properties.getPath(), properties.getThreshold());
Map<File, DataSize> paths = properties.getPaths().stream()
.collect(Collectors.toMap(PathInfo::getPath, PathInfo::getThreshold, (u, v) -> u, LinkedHashMap::new));
return new DiskSpaceHealthIndicator(paths);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand All @@ -17,6 +17,8 @@
package org.springframework.boot.actuate.autoconfigure.system;

import java.io.File;
import java.util.Arrays;
import java.util.List;

import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand All @@ -28,36 +30,54 @@
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Chris Bono
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "management.health.diskspace")
public class DiskSpaceHealthIndicatorProperties {

/**
* Path used to compute the available disk space.
* Paths to consider for computing the available disk space.
*/
private File path = new File(".");
private List<PathInfo> paths = Arrays.asList(new PathInfo());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated in the overview comment, we could keep support of the management.health.diskspace.path (for singular case only) by mapping a private PathInfo path = new PathInfo(); here as well.


/**
* Minimum disk space that should be available.
*/
private DataSize threshold = DataSize.ofMegabytes(10);

public File getPath() {
return this.path;
public List<PathInfo> getPaths() {
return this.paths;
}

public void setPath(File path) {
this.path = path;
public void setPaths(List<PathInfo> paths) {
this.paths = paths;
}

public DataSize getThreshold() {
return this.threshold;
}
public static class PathInfo {

/**
* Path used to compute the available disk space.
*/
private File path = new File(".");

/**
* Minimum disk space that should be available.
*/
private DataSize threshold = DataSize.ofMegabytes(10);

public File getPath() {
return this.path;
}

public void setPath(File path) {
this.path = path;
}

public DataSize getThreshold() {
return this.threshold;
}

public void setThreshold(DataSize threshold) {
Assert.isTrue(!threshold.isNegative(), "threshold must be greater than or equal to 0");
this.threshold = threshold;
}

public void setThreshold(DataSize threshold) {
Assert.isTrue(!threshold.isNegative(), "threshold must be greater than or equal to 0");
this.threshold = threshold;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand All @@ -16,21 +16,29 @@

package org.springframework.boot.actuate.autoconfigure.system;

import java.io.File;
import java.util.Map;

import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;

import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.system.DiskSpaceHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.util.unit.DataSize;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;

/**
* Tests for {@link DiskSpaceHealthContributorAutoConfiguration}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Chris Bono
*/
class DiskSpaceHealthContributorAutoConfigurationTests {

Expand All @@ -39,29 +47,49 @@ class DiskSpaceHealthContributorAutoConfigurationTests {
HealthContributorAutoConfiguration.class));

@Test
void runShouldCreateIndicator() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DiskSpaceHealthIndicator.class));
void runShouldCreateIndicatorWithDefaultSinglePathAndThreshold() {
this.contextRunner
.run((context) -> validateIndicatorHasPathsExactly(entry(new File("."), DataSize.ofMegabytes(10))));
}

@Test
void thresholdMustBePositive() {
this.contextRunner.withPropertyValues("management.health.diskspace.threshold=-10MB")
.run((context) -> assertThat(context).hasFailed().getFailure()
.hasMessageContaining("Failed to bind properties under 'management.health.diskspace'"));
void pathCanBeCustomized() {
this.contextRunner.withPropertyValues("management.health.diskspace.paths[0].path=..")
.run((context) -> validateIndicatorHasPathsExactly(entry(new File(".."), DataSize.ofMegabytes(10))));
}

@Test
void thresholdCanBeCustomized() {
this.contextRunner.withPropertyValues("management.health.diskspace.threshold=20MB").run((context) -> {
assertThat(context).hasSingleBean(DiskSpaceHealthIndicator.class);
assertThat(context.getBean(DiskSpaceHealthIndicator.class)).hasFieldOrPropertyWithValue("threshold",
DataSize.ofMegabytes(20));
});
this.contextRunner.withPropertyValues("management.health.diskspace.paths[0].threshold=20MB")
.run((context) -> validateIndicatorHasPathsExactly(entry(new File("."), DataSize.ofMegabytes(20))));
}

@Test
void pathAndThresholdCanBeCustomized() {
this.contextRunner
.withPropertyValues("management.health.diskspace.paths[0].path=..",
"management.health.diskspace.paths[0].threshold=20MB")
.run((context) -> validateIndicatorHasPathsExactly(entry(new File(".."), DataSize.ofMegabytes(20))));
}

@Test
void multiplePathsCanBeConfigured() {
this.contextRunner.withPropertyValues("management.health.diskspace.paths[0].path=.",
"management.health.diskspace.paths[1].path=..", "management.health.diskspace.paths[1].threshold=33MB")
.run((context) -> validateIndicatorHasPathsExactly(entry(new File("."), DataSize.ofMegabytes(10)),
entry(new File(".."), DataSize.ofMegabytes(33))));
}

@Test
void thresholdMustBePositive() {
this.contextRunner.withPropertyValues("management.health.diskspace.paths[0].threshold=-10MB")
.run((context) -> assertThat(context).hasFailed().getFailure().getCause().hasMessageContaining(
"Failed to bind properties under 'management.health.diskspace.paths[0]'"));
}

@Test
void runWhenPathDoesNotExistShouldCreateIndicator() {
this.contextRunner.withPropertyValues("management.health.diskspace.path=does/not/exist")
this.contextRunner.withPropertyValues("management.health.diskspace.paths[0].path=does/not/exist")
.run((context) -> assertThat(context).hasSingleBean(DiskSpaceHealthIndicator.class));
}

Expand All @@ -71,4 +99,13 @@ void runWhenDisabledShouldNotCreateIndicator() {
.run((context) -> assertThat(context).doesNotHaveBean(DiskSpaceHealthIndicator.class));
}

@SafeVarargs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yuk....

Alternative is to use List<Map.Entry<K, V> entries> which then leads to the tests having to use something like Arrays.asList(...). I went w/ convenience of callers namely because this is test code.

@SuppressWarnings("varargs")
private final ContextConsumer<AssertableApplicationContext> validateIndicatorHasPathsExactly(
Map.Entry<File, DataSize>... entries) {
return (context) -> assertThat(context).hasSingleBean(DiskSpaceHealthIndicator.class)
.getBean(DiskSpaceHealthIndicator.class).extracting("paths")
.asInstanceOf(InstanceOfAssertFactories.map(File.class, DataSize.class)).containsExactly(entries);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
Expand All @@ -17,6 +17,8 @@
package org.springframework.boot.actuate.system;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Expand All @@ -29,46 +31,66 @@
import org.springframework.util.unit.DataSize;

/**
* A {@link HealthIndicator} that checks available disk space and reports a status of
* {@link Status#DOWN} when it drops below a configurable threshold.
* A {@link HealthIndicator} that checks one or more paths for available disk space and
* reports a status of {@link Status#DOWN} when any of the paths drops below a
* configurable threshold.
*
* @author Mattias Severson
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Chris Bono
* @since 2.0.0
*/
public class DiskSpaceHealthIndicator extends AbstractHealthIndicator {

private static final Log logger = LogFactory.getLog(DiskSpaceHealthIndicator.class);

private final File path;

private final DataSize threshold;
private final Map<File, DataSize> paths = new LinkedHashMap<>();

/**
* Create a new {@code DiskSpaceHealthIndicator} instance.
* Create a new {@code DiskSpaceHealthIndicator} instance for a single path.
* @param path the Path used to compute the available disk space
* @param threshold the minimum disk space that should be available
*/
public DiskSpaceHealthIndicator(File path, DataSize threshold) {
super("DiskSpace health check failed");
this.path = path;
this.threshold = threshold;
this.paths.put(path, threshold);
}

/**
* Create a new {@code DiskSpaceHealthIndicator} instance for one or more paths.
* @param paths the paths to compute available disk space for and their corresponding
* minimum disk space that should be available.
*/
public DiskSpaceHealthIndicator(Map<File, DataSize> paths) {
super("DiskSpace health check failed");
this.paths.putAll(paths);
}

@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
long diskFreeInBytes = this.path.getUsableSpace();
if (diskFreeInBytes >= this.threshold.toBytes()) {
builder.up();
}
else {
logger.warn(LogMessage.format("Free disk space below threshold. Available: %d bytes (threshold: %s)",
diskFreeInBytes, this.threshold));
// assume all is well - prove otherwise when checking paths
builder.up();

Map<String, Map<String, Object>> details = new LinkedHashMap<>();
this.paths.forEach((path, threshold) -> details.put(path.getAbsolutePath(),
checkPathAndGetDetails(path, threshold, builder)));
builder.withDetail("paths", details);
}

private Map<String, Object> checkPathAndGetDetails(File path, DataSize threshold, Health.Builder builder) {
long diskFreeInBytes = path.getUsableSpace();
if (diskFreeInBytes < threshold.toBytes()) {
logger.warn(LogMessage.format("Free disk space in %s below threshold. Available: %d bytes (threshold: %s)",
path.getAbsolutePath(), diskFreeInBytes, threshold));
builder.down();
}
builder.withDetail("total", this.path.getTotalSpace()).withDetail("free", diskFreeInBytes)
.withDetail("threshold", this.threshold.toBytes()).withDetail("exists", this.path.exists());
Map<String, Object> pathDetails = new LinkedHashMap<>();
pathDetails.put("total", path.getTotalSpace());
pathDetails.put("free", diskFreeInBytes);
pathDetails.put("threshold", threshold.toBytes());
pathDetails.put("exists", path.exists());
return pathDetails;
}

}
Loading