Skip to content

Commit 220c8ba

Browse files
author
Jon Schneider
committed
Service level objective health indicators
1 parent c5b75a7 commit 220c8ba

File tree

8 files changed

+482
-0
lines changed

8 files changed

+482
-0
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ dependencies {
5353
optional("io.micrometer:micrometer-registry-influx")
5454
optional("io.micrometer:micrometer-registry-jmx")
5555
optional("io.micrometer:micrometer-registry-kairos")
56+
optional("io.micrometer:micrometer-registry-health")
5657
optional("io.micrometer:micrometer-registry-new-relic")
5758
optional("io.micrometer:micrometer-registry-prometheus")
5859
optional("io.micrometer:micrometer-registry-stackdriver")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.metrics.export.health;
18+
19+
import io.micrometer.core.instrument.Clock;
20+
import io.micrometer.core.instrument.Meter;
21+
import io.micrometer.core.instrument.Tag;
22+
import io.micrometer.core.instrument.binder.BaseUnits;
23+
import io.micrometer.core.instrument.binder.MeterBinder;
24+
import io.micrometer.core.instrument.config.NamingConvention;
25+
import io.micrometer.core.ipc.http.HttpUrlConnectionSender;
26+
import io.micrometer.health.HealthConfig;
27+
import io.micrometer.health.HealthMeterRegistry;
28+
import io.micrometer.health.ServiceLevelObjective;
29+
import io.micrometer.health.objectives.JvmServiceLevelObjectives;
30+
import io.micrometer.health.objectives.OperatingSystemServiceLevelObjectives;
31+
import org.springframework.beans.factory.ObjectProvider;
32+
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
33+
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
34+
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
35+
import org.springframework.boot.actuate.health.*;
36+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
37+
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
38+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
39+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
40+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
41+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
42+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
43+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
44+
import org.springframework.context.annotation.Bean;
45+
import org.springframework.context.annotation.Configuration;
46+
import org.springframework.context.support.GenericApplicationContext;
47+
48+
import java.util.Arrays;
49+
import java.util.Map;
50+
import java.util.stream.Collectors;
51+
52+
/**
53+
* {@link EnableAutoConfiguration Auto-configuration} for building health indicators based
54+
* on service level objectives.
55+
*
56+
* @author Jon Schneider
57+
* @since 2.4.0
58+
*/
59+
@Configuration(proxyBeanMethods = false)
60+
@AutoConfigureBefore({ CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class })
61+
@AutoConfigureAfter(MetricsAutoConfiguration.class)
62+
@ConditionalOnBean(Clock.class)
63+
@ConditionalOnClass(HealthMeterRegistry.class)
64+
@ConditionalOnProperty(prefix = "management.metrics.export.health", name = "enabled", havingValue = "true",
65+
matchIfMissing = true)
66+
@EnableConfigurationProperties(HealthProperties.class)
67+
public class HealthMetricsExportAutoConfiguration {
68+
69+
private final NamingConvention camelCasedHealthIndicatorNames = NamingConvention.camelCase;
70+
71+
private final HealthProperties properties;
72+
73+
public HealthMetricsExportAutoConfiguration(HealthProperties properties) {
74+
this.properties = properties;
75+
}
76+
77+
@Bean
78+
@ConditionalOnMissingBean
79+
public HealthConfig healthConfig() {
80+
return new HealthPropertiesConfigAdapter(this.properties);
81+
}
82+
83+
@Bean
84+
@ConditionalOnMissingBean
85+
public HealthMeterRegistry healthMeterRegistry(HealthConfig healthConfig, Clock clock,
86+
ObjectProvider<ServiceLevelObjective> serviceLevelObjectives,
87+
GenericApplicationContext applicationContext) {
88+
HealthMeterRegistry registry = HealthMeterRegistry.builder(healthConfig).clock(clock)
89+
.serviceLevelObjectives(serviceLevelObjectives.orderedStream().toArray(ServiceLevelObjective[]::new))
90+
.serviceLevelObjectives(JvmServiceLevelObjectives.MEMORY)
91+
.serviceLevelObjectives(OperatingSystemServiceLevelObjectives.DISK)
92+
.serviceLevelObjectives(properties.getApiErrorBudgets().entrySet().stream().map(apiErrorBudget -> {
93+
String apiEndpoints = '/' + apiErrorBudget.getKey().replace('.', '/');
94+
95+
return ServiceLevelObjective.build("api.error.ratio." + apiErrorBudget.getKey())
96+
.failedMessage("API error ratio exceeded.").baseUnit(BaseUnits.PERCENT)
97+
.tag("uri.matches", apiEndpoints + "/**").tag("error.outcome", "SERVER_ERROR")
98+
.errorRatio(
99+
s -> s.name("http.server.requests").tag("uri", uri -> uri.startsWith(apiEndpoints)),
100+
all -> all.tag("outcome", "SERVER_ERROR"))
101+
.isLessThan(apiErrorBudget.getValue());
102+
}).toArray(ServiceLevelObjective[]::new)).build();
103+
104+
for (ServiceLevelObjective slo : registry.getServiceLevelObjectives()) {
105+
applicationContext.registerBean(camelCasedHealthIndicatorNames.name(slo.getName(), Meter.Type.GAUGE),
106+
HealthContributor.class, () -> toHealthContributor(registry, slo));
107+
}
108+
109+
return registry;
110+
}
111+
112+
private HealthContributor toHealthContributor(HealthMeterRegistry registry, ServiceLevelObjective slo) {
113+
if (slo instanceof ServiceLevelObjective.SingleIndicator) {
114+
return new AbstractHealthIndicator(slo.getFailedMessage()) {
115+
@Override
116+
protected void doHealthCheck(Health.Builder builder) {
117+
ServiceLevelObjective.SingleIndicator singleIndicator = (ServiceLevelObjective.SingleIndicator) slo;
118+
builder.status(slo.healthy(registry) ? Status.UP : Status.OUT_OF_SERVICE)
119+
.withDetail("value", singleIndicator.getValueAsString(registry))
120+
.withDetail("mustBe", singleIndicator.getTestDescription());
121+
122+
for (Tag tag : slo.getTags()) {
123+
builder.withDetail(camelCasedHealthIndicatorNames.tagKey(tag.getKey()), tag.getValue());
124+
}
125+
126+
if (slo.getBaseUnit() != null) {
127+
builder.withDetail("unit", slo.getBaseUnit());
128+
}
129+
}
130+
};
131+
}
132+
else {
133+
ServiceLevelObjective.MultipleIndicator multipleIndicator = (ServiceLevelObjective.MultipleIndicator) slo;
134+
Map<String, HealthContributor> objectiveIndicators = Arrays.stream(multipleIndicator.getObjectives())
135+
.collect(Collectors.toMap(
136+
indicator -> camelCasedHealthIndicatorNames.name(indicator.getName(), Meter.Type.GAUGE),
137+
indicator -> toHealthContributor(registry, indicator)));
138+
return CompositeHealthContributor.fromMap(objectiveIndicators);
139+
}
140+
}
141+
142+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.metrics.export.health;
18+
19+
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties;
20+
import org.springframework.boot.context.properties.ConfigurationProperties;
21+
22+
import java.time.Duration;
23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
25+
26+
/**
27+
* {@link ConfigurationProperties @ConfigurationProperties} for configuring health
28+
* indicators based on service level objectives.
29+
*
30+
* @author Jon Schneider
31+
* @since 2.4.0
32+
*/
33+
@ConfigurationProperties(prefix = "management.metrics.export.health")
34+
public class HealthProperties {
35+
36+
/**
37+
* Step size (i.e. polling frequency for moving window indicators) to use.
38+
*/
39+
private Duration step = Duration.ofSeconds(10);
40+
41+
/**
42+
* Error budgets by API endpoint prefix. The value is a percentage in the range [0,1].
43+
*/
44+
private final Map<String, Double> apiErrorBudgets = new LinkedHashMap<>();
45+
46+
public Duration getStep() {
47+
return step;
48+
}
49+
50+
public void setStep(Duration step) {
51+
this.step = step;
52+
}
53+
54+
public Map<String, Double> getApiErrorBudgets() {
55+
return apiErrorBudgets;
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.metrics.export.health;
18+
19+
import io.micrometer.health.HealthConfig;
20+
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter;
21+
22+
import java.time.Duration;
23+
24+
/**
25+
* Adapter to convert {@link HealthProperties} to a {@link HealthConfig}.
26+
*
27+
* @author Jon Schneider
28+
* @since 2.4.0
29+
*/
30+
class HealthPropertiesConfigAdapter extends PropertiesConfigAdapter<HealthProperties> implements HealthConfig {
31+
32+
HealthPropertiesConfigAdapter(HealthProperties properties) {
33+
super(properties);
34+
}
35+
36+
@Override
37+
public String prefix() {
38+
return "management.metrics.export.health";
39+
}
40+
41+
@Override
42+
public String get(String k) {
43+
return null;
44+
}
45+
46+
@Override
47+
public Duration step() {
48+
return get(HealthProperties::getStep, HealthConfig.super::step);
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Support for building health indicators with service level objectives.
19+
*/
20+
package org.springframework.boot.actuate.autoconfigure.metrics.export.health;

0 commit comments

Comments
 (0)