Rename Service Level Agreement to Service Level Objective

This commit harmonizes our integration of Micrometer's Service Level
objectives.

Closes gh-21076
This commit is contained in:
Stephane Nicoll 2020-04-23 10:13:17 +02:00
parent 489f9b0e58
commit fe90b2a251
7 changed files with 137 additions and 50 deletions

View File

@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty;
/** /**
@ -232,12 +233,12 @@ public class MetricsProperties {
private final Map<String, double[]> percentiles = new LinkedHashMap<>(); private final Map<String, double[]> percentiles = new LinkedHashMap<>();
/** /**
* Specific SLA boundaries for meter IDs starting-with the specified name. The * Specific Service Level Objectives boundaries for meter IDs starting-with the
* longest match wins. Counters will be published for each specified boundary. * specified name. The longest match wins. Counters will be published for each
* Values can be specified as a long or as a Duration value (for timer meters, * specified boundary. Values can be specified as a long or as a Duration value
* defaulting to ms if no unit specified). * (for timer meters, defaulting to ms if no unit specified).
*/ */
private final Map<String, ServiceLevelAgreementBoundary[]> sla = new LinkedHashMap<>(); private final Map<String, ServiceLevelObjectiveBoundary[]> slo = new LinkedHashMap<>();
/** /**
* Minimum value that meter IDs starting-with the specified name are expected to * Minimum value that meter IDs starting-with the specified name are expected to
@ -261,8 +262,14 @@ public class MetricsProperties {
return this.percentiles; return this.percentiles;
} }
public Map<String, ServiceLevelAgreementBoundary[]> getSla() { @Deprecated
return this.sla; @DeprecatedConfigurationProperty(replacement = "management.metrics.distribution.slo")
public Map<String, ServiceLevelObjectiveBoundary[]> getSla() {
return this.slo;
}
public Map<String, ServiceLevelObjectiveBoundary[]> getSlo() {
return this.slo;
} }
public Map<String, String> getMinimumExpectedValue() { public Map<String, String> getMinimumExpectedValue() {

View File

@ -83,7 +83,8 @@ public class PropertiesMeterFilter implements MeterFilter {
return DistributionStatisticConfig.builder() return DistributionStatisticConfig.builder()
.percentilesHistogram(lookupWithFallbackToAll(distribution.getPercentilesHistogram(), id, null)) .percentilesHistogram(lookupWithFallbackToAll(distribution.getPercentilesHistogram(), id, null))
.percentiles(lookupWithFallbackToAll(distribution.getPercentiles(), id, null)) .percentiles(lookupWithFallbackToAll(distribution.getPercentiles(), id, null))
.sla(convertSla(id.getType(), lookup(distribution.getSla(), id, null))) .serviceLevelObjectives(
convertServiceLevelObjectives(id.getType(), lookup(distribution.getSlo(), id, null)))
.minimumExpectedValue( .minimumExpectedValue(
convertMeterValue(id.getType(), lookup(distribution.getMinimumExpectedValue(), id, null))) convertMeterValue(id.getType(), lookup(distribution.getMinimumExpectedValue(), id, null)))
.maximumExpectedValue( .maximumExpectedValue(
@ -91,11 +92,11 @@ public class PropertiesMeterFilter implements MeterFilter {
.build().merge(config); .build().merge(config);
} }
private double[] convertSla(Meter.Type meterType, ServiceLevelAgreementBoundary[] sla) { private double[] convertServiceLevelObjectives(Meter.Type meterType, ServiceLevelObjectiveBoundary[] slo) {
if (sla == null) { if (slo == null) {
return null; return null;
} }
double[] converted = Arrays.stream(sla).map((candidate) -> candidate.getValue(meterType)) double[] converted = Arrays.stream(slo).map((candidate) -> candidate.getValue(meterType))
.filter(Objects::nonNull).mapToDouble(Double::doubleValue).toArray(); .filter(Objects::nonNull).mapToDouble(Double::doubleValue).toArray();
return (converted.length != 0) ? converted : null; return (converted.length != 0) ? converted : null;
} }

View File

@ -22,12 +22,14 @@ import io.micrometer.core.instrument.Meter;
/** /**
* A service level agreement boundary for use when configuring Micrometer. Can be * A service level agreement boundary for use when configuring Micrometer. Can be
* specified as either a {@link Double} (applicable to timers and distribution summaries) * specified as either a {@link Long} (applicable to timers and distribution summaries) or
* or a {@link Duration} (applicable to only timers). * a {@link Duration} (applicable to only timers).
* *
* @author Phillip Webb * @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
* @deprecated as of 2.3.0 in favor of {@link ServiceLevelObjectiveBoundary}
*/ */
@Deprecated
public final class ServiceLevelAgreementBoundary { public final class ServiceLevelAgreementBoundary {
private final MeterValue value; private final MeterValue value;
@ -42,17 +44,18 @@ public final class ServiceLevelAgreementBoundary {
* @param meterType the meter type * @param meterType the meter type
* @return the value or {@code null} if the value cannot be applied * @return the value or {@code null} if the value cannot be applied
*/ */
public Double getValue(Meter.Type meterType) { public Long getValue(Meter.Type meterType) {
return this.value.getValue(meterType); Double value = this.value.getValue(meterType);
return (value != null) ? value.longValue() : null;
} }
/** /**
* Return a new {@link ServiceLevelAgreementBoundary} instance for the given double * Return a new {@link ServiceLevelAgreementBoundary} instance for the given long
* value. * value.
* @param value the source value * @param value the source value
* @return a {@link ServiceLevelAgreementBoundary} instance * @return a {@link ServiceLevelAgreementBoundary} instance
*/ */
public static ServiceLevelAgreementBoundary valueOf(double value) { public static ServiceLevelAgreementBoundary valueOf(long value) {
return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value)); return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value));
} }

View File

@ -0,0 +1,70 @@
/*
* Copyright 2012-2020 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
*
* https://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.autoconfigure.metrics;
import java.time.Duration;
import io.micrometer.core.instrument.Meter;
/**
* A service level objective boundary for use when configuring Micrometer. Can be
* specified as either a {@link Double} (applicable to timers and distribution summaries)
* or a {@link Duration} (applicable to only timers).
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 2.3.0
*/
public final class ServiceLevelObjectiveBoundary {
private final MeterValue value;
ServiceLevelObjectiveBoundary(MeterValue value) {
this.value = value;
}
/**
* Return the underlying value of the SLO in form suitable to apply to the given meter
* type.
* @param meterType the meter type
* @return the value or {@code null} if the value cannot be applied
*/
public Double getValue(Meter.Type meterType) {
return this.value.getValue(meterType);
}
/**
* Return a new {@link ServiceLevelObjectiveBoundary} instance for the given double
* value.
* @param value the source value
* @return a {@link ServiceLevelObjectiveBoundary} instance
*/
public static ServiceLevelObjectiveBoundary valueOf(double value) {
return new ServiceLevelObjectiveBoundary(MeterValue.valueOf(value));
}
/**
* Return a new {@link ServiceLevelObjectiveBoundary} instance for the given String
* value.
* @param value the source value
* @return a {@link ServiceLevelObjectiveBoundary} instance
*/
public static ServiceLevelObjectiveBoundary valueOf(String value) {
return new ServiceLevelObjectiveBoundary(MeterValue.valueOf(value));
}
}

View File

@ -204,29 +204,35 @@ class PropertiesMeterFilterTests {
} }
@Test @Test
void configureWhenHasSlaShouldSetSlaToValue() { @Deprecated
void configureWhenHasDeprecatedSlaShouldSetSlaToValue() {
PropertiesMeterFilter filter = new PropertiesMeterFilter( PropertiesMeterFilter filter = new PropertiesMeterFilter(
createProperties("distribution.sla.spring.boot=1,2,3")); createProperties("distribution.sla.spring.boot=1,2,3"));
assertThat( assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT)
filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries()) .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000);
.containsExactly(1000000, 2000000, 3000000);
} }
@Test @Test
void configureWhenHasHigherSlaShouldSetPercentilesToValue() { void configureWhenHasSloShouldSetSloToValue() {
PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties("distribution.sla.spring=1,2,3"));
assertThat(
filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries())
.containsExactly(1000000, 2000000, 3000000);
}
@Test
void configureWhenHasHigherSlaAndLowerShouldSetSlaToHigher() {
PropertiesMeterFilter filter = new PropertiesMeterFilter( PropertiesMeterFilter filter = new PropertiesMeterFilter(
createProperties("distribution.sla.spring=1,2,3", "distribution.sla.spring.boot=4,5,6")); createProperties("distribution.slo.spring.boot=1,2,3"));
assertThat( assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT)
filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries()) .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000);
.containsExactly(4000000, 5000000, 6000000); }
@Test
void configureWhenHasHigherSloShouldSetPercentilesToValue() {
PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties("distribution.slo.spring=1,2,3"));
assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT)
.getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000);
}
@Test
void configureWhenHasHigherSloAndLowerShouldSetSloToHigher() {
PropertiesMeterFilter filter = new PropertiesMeterFilter(
createProperties("distribution.slo.spring=1,2,3", "distribution.slo.spring.boot=4,5,6"));
assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT)
.getServiceLevelObjectiveBoundaries()).containsExactly(4000000, 5000000, 6000000);
} }
@Test @Test

View File

@ -22,47 +22,47 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for {@link ServiceLevelAgreementBoundary}. * Tests for {@link ServiceLevelObjectiveBoundary}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll * @author Stephane Nicoll
*/ */
class ServiceLevelAgreementBoundaryTests { class ServiceLevelObjectiveBoundaryTests {
@Test @Test
void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf(123L); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf(123L);
assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000);
} }
@Test @Test
void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() { void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123"); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123");
assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000);
} }
@Test @Test
void getValueForTimerWhenFromDurationStringShouldReturnDurationNanos() { void getValueForTimerWhenFromDurationStringShouldReturnDurationNanos() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123ms"); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123ms");
assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000);
} }
@Test @Test
void getValueForDistributionSummaryWhenFromDoubleShouldReturnDoubleValue() { void getValueForDistributionSummaryWhenFromDoubleShouldReturnDoubleValue() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf(123.42); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf(123.42);
assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42);
} }
@Test @Test
void getValueForDistributionSummaryWhenFromStringShouldReturnDoubleValue() { void getValueForDistributionSummaryWhenFromStringShouldReturnDoubleValue() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123.42"); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123.42");
assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42);
} }
@Test @Test
void getValueForDistributionSummaryWhenFromDurationShouldReturnNull() { void getValueForDistributionSummaryWhenFromDurationShouldReturnNull() {
ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123ms"); ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123ms");
assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isNull(); assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isNull();
} }
} }

View File

@ -2131,8 +2131,8 @@ The following properties allow per-meter customization:
| configprop:management.metrics.distribution.percentiles[] | configprop:management.metrics.distribution.percentiles[]
| Publish percentile values computed in your application | Publish percentile values computed in your application
| configprop:management.metrics.distribution.sla[] | configprop:management.metrics.distribution.slo[]
| Publish a cumulative histogram with buckets defined by your SLAs. | Publish a cumulative histogram with buckets defined by your Service Level Objectives.
|=== |===
For more details on concepts behind `percentiles-histogram`, `percentiles` and `sla` refer to the {micrometer-concepts-docs}#_histograms_and_percentiles["Histograms and percentiles" section] of the micrometer documentation. For more details on concepts behind `percentiles-histogram`, `percentiles` and `sla` refer to the {micrometer-concepts-docs}#_histograms_and_percentiles["Histograms and percentiles" section] of the micrometer documentation.