diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java index 1a8256c524..07783b95c8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java @@ -49,6 +49,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; import org.springframework.integration.config.EnableIntegrationManagement; import org.springframework.integration.support.management.IntegrationManagementConfigurer; @@ -96,6 +97,12 @@ public class MetricsAutoConfiguration { return Clock.SYSTEM; } + @Bean + @Order(0) + public PropertiesMeterFilter propertiesMeterFilter(MetricsProperties properties) { + return new PropertiesMeterFilter(properties); + } + /** * Binds metrics from Spring Integration. */ 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 0eb5c8508f..54cceb50db 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 @@ -16,7 +16,11 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; /** * {@link ConfigurationProperties} for configuring Micrometer-based metrics. @@ -27,8 +31,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("management.metrics") public class MetricsProperties { - private final Web web = new Web(); - /** * Whether auto-configured MeterRegistry implementations should be bound to the global * static registry on Metrics. For testing, set this to 'false' to maximize test @@ -36,6 +38,16 @@ public class MetricsProperties { */ private boolean useGlobalRegistry = true; + /** + * 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. + */ + private Map enable = new LinkedHashMap<>(); + + private final Web web = new Web(); + + private final Distribution distribution = new Distribution(); + public boolean isUseGlobalRegistry() { return this.useGlobalRegistry; } @@ -44,10 +56,23 @@ public class MetricsProperties { this.useGlobalRegistry = useGlobalRegistry; } + public Map getEnable() { + return this.enable; + } + + public void setEnable(Map enable) { + Assert.notNull(enable, "enable must not be null"); + this.enable = enable; + } + public Web getWeb() { return this.web; } + public Distribution getDistribution() { + return this.distribution; + } + public static class Web { private final Client client = new Client(); @@ -118,13 +143,6 @@ public class MetricsProperties { */ private boolean autoTimeRequests = true; - /** - * Whether or not instrumented requests record percentiles histogram buckets - * by default. Can be overridden by adding '@Timed' to a request endpoint and - * setting 'percentiles' to true. - */ - private boolean recordRequestPercentiles; - /** * Name of the metric for received requests. */ @@ -138,14 +156,6 @@ public class MetricsProperties { this.autoTimeRequests = autoTimeRequests; } - public boolean isRecordRequestPercentiles() { - return this.recordRequestPercentiles; - } - - public void setRecordRequestPercentiles(boolean recordRequestPercentiles) { - this.recordRequestPercentiles = recordRequestPercentiles; - } - public String getRequestsMetricName() { return this.requestsMetricName; } @@ -158,4 +168,59 @@ public class MetricsProperties { } + public static class Distribution { + + /** + * Whether meter IDs starting-with the specified name should be publish percentile + * histograms. Monitoring systems that support aggregable percentile calculation + * based on a histogram be set to true. For other systems, this has no effect. The + * longest match wins, the key `all` can also be used to configure all meters. + */ + private Map percentilesHistogram = new LinkedHashMap<>(); + + /** + * Specific computed non-aggregable percentiles to ship to the backend for meter + * IDs starting-with the specified name. The longest match wins, the key `all` can + * also be used to configure all meters. + */ + private Map percentiles = new LinkedHashMap<>(); + + /** + * Specific SLA boundaries for meter IDs starting-with the specified name. The + * longest match wins, the key `all` can also be used to configure all meters. + * Counters will be published for each sepecified boundary. Values can be + * specified as a long or as a Duration value (for timer meters, defaulting to ms + * if no unit specified). + */ + private Map sla = new LinkedHashMap<>(); + + public Map getPercentilesHistogram() { + return this.percentilesHistogram; + } + + public void setPercentilesHistogram(Map percentilesHistogram) { + Assert.notNull(percentilesHistogram, "PercentilesHistogram must not be null"); + this.percentilesHistogram = percentilesHistogram; + } + + public Map getPercentiles() { + return this.percentiles; + } + + public void setPercentiles(Map percentiles) { + Assert.notNull(percentiles, "Percentiles must not be null"); + this.percentiles = percentiles; + } + + public Map getSla() { + return this.sla; + } + + public void setSla(Map sla) { + Assert.notNull(sla, "SLA must not be null"); + this.sla = sla; + } + + } + } 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 new file mode 100644 index 0000000000..8663263fb8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java @@ -0,0 +1,87 @@ +/* + * 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.util.Arrays; +import java.util.Map; +import java.util.Objects; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Meter.Id; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; +import io.micrometer.core.instrument.histogram.HistogramConfig; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Distribution; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link MeterFilter} to apply settings from {@link MetricsProperties}. + * + * @author Jon Schneider + * @author Phillip Webb + * @since 2.0.0 + */ +public class PropertiesMeterFilter implements MeterFilter { + + private static final ServiceLevelAgreementBoundary[] EMPTY_SLA = {}; + + private MetricsProperties properties; + + public PropertiesMeterFilter(MetricsProperties properties) { + Assert.notNull(properties, "Properties must not be null"); + this.properties = properties; + } + + @Override + public MeterFilterReply accept(Meter.Id id) { + boolean enabled = lookup(this.properties.getEnable(), id, true); + return (enabled ? MeterFilterReply.NEUTRAL : MeterFilterReply.DENY); + } + + @Override + public HistogramConfig configure(Meter.Id id, HistogramConfig config) { + HistogramConfig.Builder builder = HistogramConfig.builder(); + Distribution distribution = this.properties.getDistribution(); + builder.percentilesHistogram(lookup(distribution.getPercentilesHistogram(), id, null)); + builder.percentiles(lookup(distribution.getPercentiles(), id, null)); + builder.sla(convertSla(id.getType(), lookup(distribution.getSla(), id, null))); + return builder.build().merge(config); + } + + private long[] convertSla(Meter.Type meterType, ServiceLevelAgreementBoundary[] sla) { + long[] converted = Arrays.stream(sla == null ? EMPTY_SLA : sla) + .map((candidate) -> candidate.getValue(meterType)) + .filter(Objects::nonNull).mapToLong(Long::longValue).toArray(); + return (converted.length == 0 ? null : converted); + } + + private T lookup(Map values, Id id, T defaultValue) { + String name = id.getName(); + while (StringUtils.hasLength(name)) { + T result = values.get(name); + if (result != null) { + return result; + } + int lastDot = name.lastIndexOf('.'); + name = lastDot == -1 ? "" : name.substring(0, lastDot); + } + return values.getOrDefault("all", 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 new file mode 100644 index 0000000000..fc2a779e69 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java @@ -0,0 +1,109 @@ +/* + * 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; +import io.micrometer.core.instrument.Meter.Type; + +import org.springframework.boot.context.properties.bind.convert.DurationConverter; + +/** + * A service level agreement boundary for use when configuring micrometer. Can be + * specified as either a {@link Long} (applicable to timers and distribution summaries) or + * a {@link Long} (applicable to only timers). + * + * @author Phillip Webb + * @since 2.0.0 + */ +public final class ServiceLevelAgreementBoundary { + + private final Object value; + + ServiceLevelAgreementBoundary(long value) { + this.value = value; + } + + ServiceLevelAgreementBoundary(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(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( + DurationConverter.toDuration(value, null)); + } + + /** + * Return a new {@link ServiceLevelAgreementBoundary} instance for the given long + * value. + * @param value the source value + * @return a {@link ServiceLevelAgreementBoundary} instance + */ + public static ServiceLevelAgreementBoundary valueOf(long value) { + return new ServiceLevelAgreementBoundary(value); + } + + /** + * Return a new {@link ServiceLevelAgreementBoundary} instance for the given String + * value. The value may contain a simple number, or a {@link DurationConverter + * duration formatted value} + * @param value the source value + * @return a {@link ServiceLevelAgreementBoundary} instance + */ + private static boolean isNumber(String value) { + return value.chars().allMatch(Character::isDigit); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index 1fd6271cd2..2806662a55 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -104,6 +104,16 @@ public class MetricsAutoConfigurationTests { }); } + @Test + public void propertyBasedMeterFilter() { + this.contextRunner.withPropertyValues("management.metrics.enable.my.org=false") + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.timer("my.org.timer"); + assertThat(registry.find("my.org.timer").timer()).isNull(); + }); + } + @Configuration static class TwoDataSourcesConfiguration { 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 new file mode 100644 index 0000000000..74812274f5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java @@ -0,0 +1,314 @@ +/* + * 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.util.Collections; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Meter.Id; +import io.micrometer.core.instrument.Meter.Type; +import io.micrometer.core.instrument.config.MeterFilterReply; +import io.micrometer.core.instrument.histogram.HistogramConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +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 fir {@link PropertiesMeterFilter}. + * + * @author Phillip Webb + * @author Jon Schneider + */ +public class PropertiesMeterFilterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Mock + private HistogramConfig config; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void createWhenPropertiesIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Properties must not be null"); + new PropertiesMeterFilter(null); + } + + @Test + public void acceptWhenHasNoEnabledPropertiesShouldReturnNeutral() { + PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties()); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.NEUTRAL); + } + + @Test + public void acceptWhenHasEnableFalseShouldReturnDeny() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring.boot=false")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.DENY); + } + + @Test + public void acceptWhenHasEnableTrueShouldReturnNeutral() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring.boot=true")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.NEUTRAL); + } + + @Test + public void acceptWhenHasHigherEnableFalseShouldReturnDeny() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring=false")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.DENY); + } + + @Test + public void acceptWhenHasHigherEnableTrueShouldReturnNeutral() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring=true")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.NEUTRAL); + } + + @Test + public void acceptWhenHasHigherEnableFalseExactEnableTrueShouldReturnNeutral() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring=false", "enable.spring.boot=true")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.NEUTRAL); + } + + @Test + public void acceptWhenHasHigherEnableTrueExactEnableFalseShouldReturnDeny() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.spring=true", "enable.spring.boot=false")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.DENY); + } + + @Test + public void acceptWhenHasAllEnableFalseShouldReturnDeny() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.all=false")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.DENY); + } + + @Test + public void acceptWhenHasAllEnableFalseButHigherEnableTrueShouldReturnNeutral() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("enable.all=false", "enable.spring=true")); + assertThat(filter.accept(createMeterId("spring.boot"))) + .isEqualTo(MeterFilterReply.NEUTRAL); + } + + @Test + public void configureWhenHasHistogramTrueShouldSetPercentilesHistogramToTrue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring.boot=true")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isTrue(); + } + + @Test + public void configureWhenHasHistogramFalseShouldSetPercentilesHistogramToFalse() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring.boot=false")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isFalse(); + } + + @Test + public void configureWhenHasHigherHistogramTrueShouldSetPercentilesHistogramToTrue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring=true")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isTrue(); + } + + @Test + public void configureWhenHasHigherHistogramFalseShouldSetPercentilesHistogramToFalse() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring=false")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isFalse(); + } + + @Test + public void configureWhenHasHigherHistogramTrueAndLowerFalseShouldSetPercentilesHistogramToFalse() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring=true", + "distribution.percentiles-histogram.spring.boot=false")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isFalse(); + } + + @Test + public void configureWhenHasHigherHistogramFalseAndLowerTrueShouldSetPercentilesHistogramToFalse() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.spring=false", + "distribution.percentiles-histogram.spring.boot=true")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isTrue(); + } + + @Test + public void configureWhenAllHistogramTrueSetPercentilesHistogramToTrue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles-histogram.all=true")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .isPercentileHistogram()).isTrue(); + } + + @Test + public void configureWhenHasPercentilesShouldSetPercentilesToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles.spring.boot=1,1.5,2")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getPercentiles()).containsExactly(1, 1.5, 2); + } + + @Test + public void configureWhenHasHigherPercentilesShouldSetPercentilesToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles.spring=1,1.5,2")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getPercentiles()).containsExactly(1, 1.5, 2); + } + + @Test + public void configureWhenHasHigherPercentilesAndLowerShouldSetPercentilesToHigher() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles.spring=1,1.5,2", + "distribution.percentiles.spring.boot=3,3.5,4")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getPercentiles()).containsExactly(3, 3.5, 4); + } + + @Test + public void configureWhenAllPercentilesSetShouldSetPercentilesToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.percentiles.all=1,1.5,2")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getPercentiles()).containsExactly(1, 1.5, 2); + } + + @Test + public void configureWhenHasSlaShouldSetSlaToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.sla.spring.boot=1,2,3")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getSlaBoundaries()).containsExactly(1000000, 2000000, 3000000); + } + + @Test + public void configureWhenHasHigherSlaShouldSetPercentilesToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.sla.spring=1,2,3")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getSlaBoundaries()).containsExactly(1000000, 2000000, 3000000); + } + + @Test + public void configureWhenHasHigherSlaAndLowerShouldSetSlaToHigher() { + PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties( + "distribution.sla.spring=1,2,3", "distribution.sla.spring.boot=4,5,6")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getSlaBoundaries()).containsExactly(4000000, 5000000, 6000000); + } + + @Test + public void configureWhenAllSlaSetShouldSetSlaToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.sla.all=1,2,3")); + assertThat(filter.configure(createMeterId("spring.boot"), HistogramConfig.DEFAULT) + .getSlaBoundaries()).containsExactly(1000000, 2000000, 3000000); + } + + @Test + public void configureWhenSlaDurationShouldOnlyApplyToTimer() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.sla.all=1ms,2ms,3ms")); + Meter.Id timer = createMeterId("spring.boot", Meter.Type.TIMER); + Meter.Id summary = createMeterId("spring.boot", Meter.Type.DISTRIBUTION_SUMMARY); + Meter.Id counter = createMeterId("spring.boot", Meter.Type.COUNTER); + assertThat(filter.configure(timer, HistogramConfig.DEFAULT).getSlaBoundaries()) + .containsExactly(1000000, 2000000, 3000000); + assertThat(filter.configure(summary, HistogramConfig.DEFAULT).getSlaBoundaries()) + .isEmpty(); + assertThat(filter.configure(counter, HistogramConfig.DEFAULT).getSlaBoundaries()) + .isEmpty(); + } + + @Test + public void configureWhenSlaLongShouldOnlyApplyToTimerAndDistributionSummary() { + PropertiesMeterFilter filter = new PropertiesMeterFilter( + createProperties("distribution.sla.all=1,2,3")); + Meter.Id timer = createMeterId("spring.boot", Meter.Type.TIMER); + Meter.Id summary = createMeterId("spring.boot", Meter.Type.DISTRIBUTION_SUMMARY); + Meter.Id counter = createMeterId("spring.boot", Meter.Type.COUNTER); + assertThat(filter.configure(timer, HistogramConfig.DEFAULT).getSlaBoundaries()) + .containsExactly(1000000, 2000000, 3000000); + assertThat(filter.configure(summary, HistogramConfig.DEFAULT).getSlaBoundaries()) + .containsExactly(1, 2, 3); + assertThat(filter.configure(counter, HistogramConfig.DEFAULT).getSlaBoundaries()) + .isEmpty(); + } + + private Id createMeterId(String name) { + Meter.Type meterType = Type.TIMER; + return createMeterId(name, meterType); + } + + private Id createMeterId(String name, Meter.Type meterType) { + TestMeterRegistry registry = new TestMeterRegistry(); + return Meter.builder(name, meterType, Collections.emptyList()).register(registry) + .getId(); + } + + private MetricsProperties createProperties(String... properties) { + MockEnvironment environment = new MockEnvironment(); + TestPropertyValues.of(properties).applyTo(environment); + Binder binder = Binder.get(environment); + return binder.bind("", Bindable.of(MetricsProperties.class)) + .orElseGet(MetricsProperties::new); + } + + private static class TestMeterRegistry extends SimpleMeterRegistry { + + } + +} 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 new file mode 100644 index 0000000000..eabbde15a0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java @@ -0,0 +1,95 @@ +/* + * 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 ServiceLevelAgreementBoundary}. + * + * @author Phillip Webb + */ +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); + assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + public void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() { + ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123"); + assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + public void getValueForTimerWhenFromDurationStringShouldReturnDrationNanos() { + ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary + .valueOf("123ms"); + 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 9eb3393c74..ee3f5e641d 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 @@ -1378,6 +1378,10 @@ content into your application. Rather, pick only the properties that you need. management.metrics.web.server.auto-time-requests=true # Whether requests handled by Spring MVC or WebFlux should be automatically timed. management.metrics.web.server.record-request-percentiles=false # Whether instrumented requests record percentiles histogram buckets by default. management.metrics.web.server.requests-metric-name=http.server.requests # Name of the metric for received requests. + management.metrics.enabled.= # 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.distribution.percentiles-histogram.= # Whether meter IDs starting-with the specified name should be publish percentile histograms. + 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.sla.= Specific SLA boundaries for meter IDs starting-with the specified name. The longest match wins, the key `all` can also be used to configure all meters. # ---------------------------------------- diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.adoc index e8d2db4c69..117e358fe5 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.adoc @@ -65,6 +65,7 @@ Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; :gradle-user-guide: https://docs.gradle.org/4.2.1/userguide :hibernate-documentation: https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html :jetty-documentation: https://www.eclipse.org/jetty/documentation/9.4.x +:micrometer-concepts-documentation: https://micrometer.io/docs/concepts :tomcat-documentation: https://tomcat.apache.org/tomcat-8.5-doc // ====================================================================================== 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 261f1d3823..a8bd24c8ab 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 @@ -1294,6 +1294,59 @@ Auto-configuration enables binding of a number of Spring Integration-related met +[[production-ready-metrics-per-meter-properties]] +=== Customizing individual meters +If you need to apply customizations to specific `Meter` instances you can use the +`io.micrometer.core.instrument.config.MeterFilter` interface. By default, all +`MeterFilter` beans will be automatically applied to the micrometer +`MeterRegistry.Config`. + +For example, if you want to rename the `mytag.region` tag to `mytag.area` for +all meter IDs beginning with `com.example`, you can do the following: + +[source,java,indent=0] +---- +include::{code-examples}/actuate/metrics/MetricsFilterBeanExample.java[tag=configuration] +---- + +==== Per-meter properties +In addition to `MeterFilter` beans, it's also possible to apply a limited set of +customization on a per-meter basis using properties. Per-meter customizations apply to +any all meter IDs that start with the given name. For example, the following will disable +any meters that have an ID starting with `example.remote` + +[source,properties,indent=0] +---- + management.metrics.enable.example.remote=false +---- + +The following properties allow per-meter customization: + +.Per-meter customizations +|=== +| Property | Description + +| `management.metrics.enable` +| Whether to deny meters from emitting any metrics. + +| `management.metrics.distribution.percentiles-histogram` +| Whether to publish a histogram suitable for computing aggregable (across dimension) +percentile approximations. + +| `management.metrics.distribution.percentiles` +| Publish percentile values computed in your application + +| `management.metrics.distribution.sla` +| Publish a cumulative histogram with buckets defined by your SLAs. + +|=== + +For more details on concepts behind `percentiles-histogram`, `percentiles` and `sla` +refer to the {micrometer-concepts-documentation}#_histograms_and_percentiles["Histograms +and percentiles" section] of the micrometer documentation. + + + [[production-ready-auditing]] == Auditing Once Spring Security is in play, Spring Boot Actuator has a flexible audit framework that diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/actuate/metrics/MetricsFilterBeanExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/actuate/metrics/MetricsFilterBeanExample.java new file mode 100644 index 0000000000..ef21770218 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/actuate/metrics/MetricsFilterBeanExample.java @@ -0,0 +1,43 @@ +/* + * 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.metrics; + +import io.micrometer.core.instrument.config.MeterFilter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Example to show a {@link MeterFilter}. + * + * @author Phillip Webb + */ +public class MetricsFilterBeanExample { + + @Configuration + static class MetricsFilterExampleConfiguration { + + // tag::configuration[] + @Bean + public MeterFilter renameRegionTagMeterFilter() { + return MeterFilter.renameTag("com.example", "mytag.region", "mytag.area"); + } + // end::configuration[] + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java index c2d129ab55..44de020d96 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java @@ -37,8 +37,9 @@ import org.springframework.util.StringUtils; * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. * * @author Phillip Webb + * @since 2.0.0 */ -class DurationConverter implements GenericConverter { +public class DurationConverter implements GenericConverter { private static final Set TYPES; @@ -78,11 +79,20 @@ class DurationConverter implements GenericConverter { if (source == null) { return null; } + DefaultDurationUnit defaultUnit = targetType + .getAnnotation(DefaultDurationUnit.class); return toDuration(source.toString(), - targetType.getAnnotation(DefaultDurationUnit.class)); + (defaultUnit == null ? null : defaultUnit.value())); } - private Duration toDuration(String source, DefaultDurationUnit defaultUnit) { + /** + * Convert the specified source to a {@link Duration}. + * @param source the source to convert + * @param defaultUnit the default unit to use ({@code null} is treated as + * milliseconds) + * @return the duration + */ + public static Duration toDuration(String source, ChronoUnit defaultUnit) { try { if (!StringUtils.hasLength(source)) { return null; @@ -103,9 +113,9 @@ class DurationConverter implements GenericConverter { } } - private ChronoUnit getUnit(String value, DefaultDurationUnit defaultUnit) { + private static ChronoUnit getUnit(String value, ChronoUnit defaultUnit) { if (StringUtils.isEmpty(value)) { - return (defaultUnit != null ? defaultUnit.value() : ChronoUnit.MILLIS); + return (defaultUnit != null ? defaultUnit : ChronoUnit.MILLIS); } ChronoUnit unit = UNITS.get(value.toLowerCase()); Assert.state(unit != null, () -> "Unknown unit '" + value + "'");