diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java new file mode 100644 index 0000000000..829a5aa647 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2018 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.autoconfigure.metrics; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Meter.Type; + +import org.springframework.boot.convert.DurationStyle; + +/** + * A meter value that is used when configuring micrometer. Can be a String representation + * of either a {@link Long} (applicable to timers and distribution summaries) or a + * {@link Duration} (applicable to only timers). + * + * @author Phillip Webb + */ +final class MeterValue { + + private final Object value; + + MeterValue(long value) { + this.value = value; + } + + MeterValue(Duration value) { + this.value = value; + } + + /** + * Return the underlying value of the SLA 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 Long getValue(Type meterType) { + if (meterType == Type.DISTRIBUTION_SUMMARY) { + return getDistributionSummaryValue(); + } + if (meterType == Type.TIMER) { + return getTimerValue(); + } + return null; + } + + private Long getDistributionSummaryValue() { + if (this.value instanceof Long) { + return (Long) this.value; + } + return null; + } + + private Long getTimerValue() { + if (this.value instanceof Long) { + return TimeUnit.MILLISECONDS.toNanos((long) this.value); + } + if (this.value instanceof Duration) { + return ((Duration) this.value).toNanos(); + } + return null; + } + + /** + * Return a new {@link MeterValue} instance for the given String value. The value may + * contain a simple number, or a {@link DurationStyle duration style string}. + * @param value the source value + * @return a {@link MeterValue} instance + */ + public static MeterValue valueOf(String value) { + if (isNumber(value)) { + return new MeterValue(Long.parseLong(value)); + } + return new MeterValue(DurationStyle.detectAndParse(value)); + } + + /** + * Return a new {@link MeterValue} instance for the given long value. + * @param value the source value + * @return a {@link MeterValue} instance + */ + public static MeterValue valueOf(long value) { + return new MeterValue(value); + } + + private static boolean isNumber(String value) { + return value.chars().allMatch(Character::isDigit); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index 3bcbd3d0f0..009103f920 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -25,6 +25,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * {@link ConfigurationProperties} for configuring Micrometer-based metrics. * * @author Jon Schneider + * @author Alexander Abramov * @since 2.0.0 */ @ConfigurationProperties("management.metrics") @@ -198,6 +199,20 @@ public class MetricsProperties { */ private final Map sla = new LinkedHashMap<>(); + /** + * Minimum value that meter IDs starting-with the specified name are expected to + * observe. The longest match wins. Values can be specified as a long or as a + * Duration value (for timer meters, defaulting to ms if no unit specified). + */ + private final Map minimumExpectedValue = new LinkedHashMap<>(); + + /** + * Maximum value that meter IDs starting-with the specified name are expected to + * observe. The longest match wins. Values can be specified as a long or as a + * Duration value (for timer meters, defaulting to ms if no unit specified). + */ + private final Map maximumExpectedValue = new LinkedHashMap<>(); + public Map getPercentilesHistogram() { return this.percentilesHistogram; } @@ -210,6 +225,14 @@ public class MetricsProperties { return this.sla; } + public Map getMinimumExpectedValue() { + return this.minimumExpectedValue; + } + + public Map getMaximumExpectedValue() { + return this.maximumExpectedValue; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java index e514c36ffb..86487b3dfb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java @@ -41,6 +41,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Stephane Nicoll * @author Artsiom Yudovin + * @author Alexander Abramov * @since 2.0.0 */ public class PropertiesMeterFilter implements MeterFilter { @@ -87,6 +88,10 @@ public class PropertiesMeterFilter implements MeterFilter { .percentiles( lookupWithFallbackToAll(distribution.getPercentiles(), id, null)) .sla(convertSla(id.getType(), lookup(distribution.getSla(), id, null))) + .minimumExpectedValue(convertMeterValue(id.getType(), + lookup(distribution.getMinimumExpectedValue(), id, null))) + .maximumExpectedValue(convertMeterValue(id.getType(), + lookup(distribution.getMaximumExpectedValue(), id, null))) .build().merge(config); } @@ -100,6 +105,10 @@ public class PropertiesMeterFilter implements MeterFilter { return (converted.length != 0) ? converted : null; } + private Long convertMeterValue(Meter.Type meterType, String value) { + return (value != null) ? MeterValue.valueOf(value).getValue(meterType) : null; + } + private T lookup(Map values, Id id, T defaultValue) { if (values.isEmpty()) { return defaultValue; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java index 707f1116f8..aae8f20f0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java @@ -17,12 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics; import java.time.Duration; -import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Meter.Type; - -import org.springframework.boot.convert.DurationStyle; /** * A service level agreement boundary for use when configuring micrometer. Can be @@ -34,13 +30,9 @@ import org.springframework.boot.convert.DurationStyle; */ public final class ServiceLevelAgreementBoundary { - private final Object value; + private final MeterValue value; - ServiceLevelAgreementBoundary(long value) { - this.value = value; - } - - ServiceLevelAgreementBoundary(Duration value) { + ServiceLevelAgreementBoundary(MeterValue value) { this.value = value; } @@ -51,37 +43,7 @@ public final class ServiceLevelAgreementBoundary { * @return the value or {@code null} if the value cannot be applied */ public Long getValue(Meter.Type meterType) { - if (meterType == Type.DISTRIBUTION_SUMMARY) { - return getDistributionSummaryValue(); - } - if (meterType == Type.TIMER) { - return getTimerValue(); - } - return null; - } - - private Long getDistributionSummaryValue() { - if (this.value instanceof Long) { - return (Long) this.value; - } - return null; - } - - private Long getTimerValue() { - if (this.value instanceof Long) { - return TimeUnit.MILLISECONDS.toNanos((long) this.value); - } - if (this.value instanceof Duration) { - return ((Duration) this.value).toNanos(); - } - return null; - } - - public static ServiceLevelAgreementBoundary valueOf(String value) { - if (isNumber(value)) { - return new ServiceLevelAgreementBoundary(Long.parseLong(value)); - } - return new ServiceLevelAgreementBoundary(DurationStyle.detectAndParse(value)); + return this.value.getValue(meterType); } /** @@ -91,18 +53,17 @@ public final class ServiceLevelAgreementBoundary { * @return a {@link ServiceLevelAgreementBoundary} instance */ public static ServiceLevelAgreementBoundary valueOf(long value) { - return new ServiceLevelAgreementBoundary(value); + return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value)); } /** - * Return a new {@link ServiceLevelAgreementBoundary} instance for the given String - * value. The value may contain a simple number, or a {@link DurationStyle duration - * style string}. + * Return a new {@link ServiceLevelAgreementBoundary} instance for the given long + * value. * @param value the source value * @return a {@link ServiceLevelAgreementBoundary} instance */ - private static boolean isNumber(String value) { - return value.chars().allMatch(Character::isDigit); + public static ServiceLevelAgreementBoundary valueOf(String value) { + return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java new file mode 100644 index 0000000000..b86f350799 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2018 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.autoconfigure.metrics; + +import io.micrometer.core.instrument.Meter.Type; +import org.junit.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MeterValue}. + * + * @author Phillip Webb + */ +public class MeterValueTests { + + @Test + public void getValueForDistributionSummaryWhenFromLongShouldReturnLongValue() { + MeterValue meterValue = MeterValue.valueOf(123L); + assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); + } + + @Test + public void getValueForDistributionSummaryWhenFromNumberStringShouldReturnLongValue() { + MeterValue meterValue = MeterValue.valueOf("123"); + assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); + } + + @Test + public void getValueForDistributionSummaryWhenFromDurationStringShouldReturnNull() { + MeterValue meterValue = MeterValue.valueOf("123ms"); + assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isNull(); + } + + @Test + public void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { + MeterValue meterValue = MeterValue.valueOf(123L); + assertThat(meterValue.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + public void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() { + MeterValue meterValue = MeterValue.valueOf("123"); + assertThat(meterValue.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + public void getValueForTimerWhenFromDurationStringShouldReturnDurationNanos() { + MeterValue meterValue = MeterValue.valueOf("123ms"); + assertThat(meterValue.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + public void getValueForOthersShouldReturnNull() { + MeterValue meterValue = MeterValue.valueOf("123"); + assertThat(meterValue.getValue(Type.COUNTER)).isNull(); + assertThat(meterValue.getValue(Type.GAUGE)).isNull(); + assertThat(meterValue.getValue(Type.LONG_TASK_TIMER)).isNull(); + assertThat(meterValue.getValue(Type.OTHER)).isNull(); + } + + @Test + public void valueOfShouldWorkInBinder() { + MockEnvironment environment = new MockEnvironment(); + TestPropertyValues.of("duration=10ms", "long=20").applyTo(environment); + assertThat(Binder.get(environment).bind("duration", Bindable.of(MeterValue.class)) + .get().getValue(Type.TIMER)).isEqualTo(10000000); + assertThat(Binder.get(environment).bind("long", Bindable.of(MeterValue.class)) + .get().getValue(Type.TIMER)).isEqualTo(20000000); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java index 9be02cc6ac..d3240f4485 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.time.Duration; import java.util.Collections; import io.micrometer.core.instrument.Meter; @@ -251,6 +252,62 @@ public class PropertiesMeterFilterTests { .containsExactly(4000000, 5000000, 6000000); } + @Test + public void configureWhenHasMinimumExpectedValueShouldSetMinimumExpectedToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.minimum-expected-value.spring.boot=10")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMinimumExpectedValue()) + .isEqualTo(Duration.ofMillis(10).toNanos()); + } + + @Test + public void configureWhenHasHigherMinimumExpectedValueShouldSetMinimumExpectedValueToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.minimum-expected-value.spring=10")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMinimumExpectedValue()) + .isEqualTo(Duration.ofMillis(10).toNanos()); + } + + @Test + public void configureWhenHasHigherMinimumExpectedValueAndLowerShouldSetMinimumExpectedValueToHigher() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.minimum-expected-value.spring=10", + "distribution.minimum-expected-value.spring.boot=50")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMinimumExpectedValue()) + .isEqualTo(Duration.ofMillis(50).toNanos()); + } + + @Test + public void configureWhenHasMaximumExpectedValueShouldSetMaximumExpectedToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.maximum-expected-value.spring.boot=5000")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMaximumExpectedValue()) + .isEqualTo(Duration.ofMillis(5000).toNanos()); + } + + @Test + public void configureWhenHasHigherMaximumExpectedValueShouldSetMaximumExpectedValueToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.maximum-expected-value.spring=5000")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMaximumExpectedValue()) + .isEqualTo(Duration.ofMillis(5000).toNanos()); + } + + @Test + public void configureWhenHasHigherMaximumExpectedValueAndLowerShouldSetMaximumExpectedValueToHigher() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.maximum-expected-value.spring=5000", + "distribution.maximum-expected-value.spring.boot=10000")); + assertThat(filter.configure(createMeterId("spring.boot"), + DistributionStatisticConfig.DEFAULT).getMaximumExpectedValue()) + .isEqualTo(Duration.ofMillis(10000).toNanos()); + } + private Id createMeterId(String name) { Meter.Type meterType = Type.TIMER; return createMeterId(name, meterType); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java index 99875e6467..a0df3aa3ff 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java @@ -19,11 +19,6 @@ package org.springframework.boot.actuate.autoconfigure.metrics; import io.micrometer.core.instrument.Meter.Type; import org.junit.Test; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.mock.env.MockEnvironment; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -33,25 +28,6 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class ServiceLevelAgreementBoundaryTests { - @Test - public void getValueForDistributionSummaryWhenFromLongShouldReturnLongValue() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf(123L); - assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); - } - - @Test - public void getValueForDistributionSummaryWhenFromNumberStringShouldReturnLongValue() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123"); - assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); - } - - @Test - public void getValueForDistributionSummaryWhenFromDurationStringShouldReturnNull() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary - .valueOf("123ms"); - assertThat(sla.getValue(Type.DISTRIBUTION_SUMMARY)).isNull(); - } - @Test public void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf(123L); @@ -71,25 +47,4 @@ public class ServiceLevelAgreementBoundaryTests { assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); } - @Test - public void getValueForOthersShouldReturnNull() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123"); - assertThat(sla.getValue(Type.COUNTER)).isNull(); - assertThat(sla.getValue(Type.GAUGE)).isNull(); - assertThat(sla.getValue(Type.LONG_TASK_TIMER)).isNull(); - assertThat(sla.getValue(Type.OTHER)).isNull(); - } - - @Test - public void valueOfShouldWorkInBinder() { - MockEnvironment environment = new MockEnvironment(); - TestPropertyValues.of("duration=10ms", "long=20").applyTo(environment); - assertThat(Binder.get(environment) - .bind("duration", Bindable.of(ServiceLevelAgreementBoundary.class)).get() - .getValue(Type.TIMER)).isEqualTo(10000000); - assertThat(Binder.get(environment) - .bind("long", Bindable.of(ServiceLevelAgreementBoundary.class)).get() - .getValue(Type.TIMER)).isEqualTo(20000000); - } - } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 0e690de9d0..1ece72a682 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -1379,8 +1379,10 @@ content into your application. Rather, pick only the properties that you need. management.info.git.mode=simple # Mode to use to expose git information. # METRICS - management.metrics.distribution.percentiles-histogram.*= # Whether meter IDs starting with the specified name should publish percentile histograms. + management.metrics.distribution.maximum-expected-value.*= # Maximum value that meter IDs starting-with the specified name are expected to observe. + management.metrics.distribution.minimum-expected-value.*= # Minimum value that meter IDs starting-with the specified name are expected to observe. management.metrics.distribution.percentiles.*= # Specific computed non-aggregable percentiles to ship to the backend for meter IDs starting-with the specified name. + management.metrics.distribution.percentiles-histogram.*= # Whether meter IDs starting with the specified name should publish percentile histograms. management.metrics.distribution.sla.*= # Specific SLA boundaries for meter IDs starting-with the specified name. The longest match wins. management.metrics.enable.*= # Whether meter IDs starting-with the specified name should be enabled. The longest match wins, the key `all` can also be used to configure all meters. management.metrics.export.atlas.batch-size=10000 # Number of measurements per request to use for this backend. If more measurements are found, then multiple requests will be made. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index fbef7f0642..343a9181ff 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -2051,6 +2051,10 @@ The following properties allow per-meter customization: | Whether to publish a histogram suitable for computing aggregable (across dimension) percentile approximations. +| `management.metrics.distribution.minimum-expected-value` +| `management.metrics.distribution.maximum-expected-value` +| Publish less histogram buckets by clamping the range of expected values. + | `management.metrics.distribution.percentiles` | Publish percentile values computed in your application