diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml index 9c7e2f9550..4a977c5d43 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml @@ -122,6 +122,11 @@ micrometer-registry-prometheus true + + io.micrometer + micrometer-registry-statsd + true + io.searchbox jest diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterBindersConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterBindersConfiguration.java index 38d709faed..7617ead881 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterBindersConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterBindersConfiguration.java @@ -16,10 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.metrics; -import io.micrometer.core.instrument.binder.JvmMemoryMetrics; -import io.micrometer.core.instrument.binder.LogbackMetrics; import io.micrometer.core.instrument.binder.MeterBinder; -import io.micrometer.core.instrument.binder.UptimeMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.logging.LogbackMetrics; +import io.micrometer.core.instrument.binder.system.UptimeMetrics; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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 1456212799..515a14fe28 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 @@ -36,6 +36,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.influx.Infl import org.springframework.boot.actuate.autoconfigure.metrics.export.jmx.JmxExportConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusExportConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleExportConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdExportConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.reactive.server.WebFluxMetricsConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.client.RestTemplateMetricsConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsConfiguration; @@ -67,7 +68,8 @@ import org.springframework.integration.support.management.IntegrationManagementC AtlasExportConfiguration.class, DatadogExportConfiguration.class, GangliaExportConfiguration.class, GraphiteExportConfiguration.class, InfluxExportConfiguration.class, JmxExportConfiguration.class, - PrometheusExportConfiguration.class, SimpleExportConfiguration.class }) + PrometheusExportConfiguration.class, SimpleExportConfiguration.class, + StatsdExportConfiguration.class }) public class MetricsAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteProperties.java index 06114a9b2f..a5577904cf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteProperties.java @@ -19,6 +19,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.graphite; import java.time.Duration; import java.util.concurrent.TimeUnit; +import io.micrometer.graphite.GraphiteProtocol; + import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -29,6 +31,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; */ @ConfigurationProperties(prefix = "spring.metrics.graphite") public class GraphiteProperties { + /** * Enable publishing to the backend. */ @@ -59,6 +62,11 @@ public class GraphiteProperties { */ private Integer port; + /** + * Protocol to use while shipping data to Graphite. + */ + private GraphiteProtocol protocol = GraphiteProtocol.Pickled; + public Boolean getEnabled() { return this.enabled; } @@ -106,4 +114,13 @@ public class GraphiteProperties { public void setPort(Integer port) { this.port = port; } + + public GraphiteProtocol getProtocol() { + return this.protocol; + } + + public void setProtocol(GraphiteProtocol protocol) { + this.protocol = protocol; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesConfigAdapter.java index 61365e0466..d1e4fac045 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesConfigAdapter.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; import io.micrometer.graphite.GraphiteConfig; +import io.micrometer.graphite.GraphiteProtocol; import org.springframework.boot.actuate.autoconfigure.metrics.export.PropertiesConfigAdapter; @@ -74,4 +75,9 @@ class GraphitePropertiesConfigAdapter return get(GraphiteProperties::getPort, GraphiteConfig::port); } + @Override + public GraphiteProtocol protocol() { + return get(GraphiteProperties::getProtocol, GraphiteConfig::protocol); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdExportConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdExportConfiguration.java new file mode 100644 index 0000000000..1b52307208 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdExportConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2017 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.export.statsd; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.statsd.StatsdConfig; +import io.micrometer.statsd.StatsdMeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.MetricsExporter; +import org.springframework.boot.actuate.autoconfigure.metrics.export.StringToDurationConverter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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; + +/** + * Configuration for exporting metrics to StatsD. + * + * @author Jon Schneider + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass(StatsdMeterRegistry.class) +@Import(StringToDurationConverter.class) +@EnableConfigurationProperties(StatsdProperties.class) +public class StatsdExportConfiguration { + + @Bean + @ConditionalOnMissingBean(StatsdConfig.class) + public StatsdConfig statsdConfig(StatsdProperties statsdProperties) { + return new StatsdPropertiesConfigAdapter(statsdProperties); + } + + @Bean + @ConditionalOnProperty(value = "spring.metrics.statsd.enabled", matchIfMissing = true) + public MetricsExporter statsdExporter(StatsdConfig statsdConfig, Clock clock) { + return () -> new StatsdMeterRegistry(statsdConfig, clock); + } + + @Bean + @ConditionalOnMissingBean + public Clock micrometerClock() { + return Clock.SYSTEM; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java new file mode 100644 index 0000000000..acce7ecdff --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2017 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.export.statsd; + +import java.time.Duration; + +import io.micrometer.statsd.StatsdFlavor; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties} for configuring StatsD metrics export. + * + * @author Jon Schneider + * @since 2.0.0 + */ +@ConfigurationProperties(prefix = "spring.metrics.statsd") +public class StatsdProperties { + + /** + * Enable publishing to the backend. + */ + private Boolean enabled = true; + + /** + * Variant of the StatsD line protocol to use. + */ + private StatsdFlavor flavor = StatsdFlavor.Datadog; + + /** + * Host name of the StatsD agent. + */ + private String host = "localhost"; + + /** + * UDP port of the StatsD agent. + */ + private Integer port = 8125; + + /** + * Total length of a single payload should be kept within your network's MTU. + */ + private Integer maxPacketLength = 1400; + + /** + * Determines how often gauges will be polled. When a gauge is polled, its value is + * recalculated. If the value has changed, it is sent to the StatsD server. + */ + private Duration pollingFrequency = Duration.ofSeconds(10); + + /** + * Governs the maximum size of the queue of items waiting to be sent to a StatsD agent + * over UDP. + */ + private Integer queueSize = Integer.MAX_VALUE; + + /** + * Used to create a bucket filter clamping the bucket domain of timer percentiles + * histograms to some max value. This is used to limit the number of buckets shipped + * to StatsD to save on storage. + */ + private Duration timerPercentilesMax = Duration.ofMinutes(2); + + /** + * Used to create a bucket filter clamping the bucket domain of timer percentiles + * histograms to some min value. This is used to limit the number of buckets shipped + * to StatsD to save on storage. + */ + private Duration timerPercentilesMin = Duration.ofMillis(10); + + public Duration getTimerPercentilesMax() { + return this.timerPercentilesMax; + } + + public void setTimerPercentilesMax(Duration timerPercentilesMax) { + this.timerPercentilesMax = timerPercentilesMax; + } + + public Duration getTimerPercentilesMin() { + return this.timerPercentilesMin; + } + + public void setTimerPercentilesMin(Duration timerPercentilesMin) { + this.timerPercentilesMin = timerPercentilesMin; + } + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public StatsdFlavor getFlavor() { + return this.flavor; + } + + public void setFlavor(StatsdFlavor flavor) { + this.flavor = flavor; + } + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return this.port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Integer getMaxPacketLength() { + return this.maxPacketLength; + } + + public void setMaxPacketLength(Integer maxPacketLength) { + this.maxPacketLength = maxPacketLength; + } + + public Duration getPollingFrequency() { + return this.pollingFrequency; + } + + public void setPollingFrequency(Duration pollingFrequency) { + this.pollingFrequency = pollingFrequency; + } + + public Integer getQueueSize() { + return this.queueSize; + } + + public void setQueueSize(Integer queueSize) { + this.queueSize = queueSize; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java new file mode 100644 index 0000000000..840be63de5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2017 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.export.statsd; + +import java.time.Duration; + +import io.micrometer.statsd.StatsdConfig; +import io.micrometer.statsd.StatsdFlavor; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.PropertiesConfigAdapter; + +/** + * Adapter to convert {@link StatsdProperties} to a {@link StatsdConfig}. + * + * @author Jon Schneider + * @since 2.0.0 + */ +public class StatsdPropertiesConfigAdapter extends + PropertiesConfigAdapter implements StatsdConfig { + + private static final StatsdConfig DEFAULTS = (key) -> null; + + public StatsdPropertiesConfigAdapter(StatsdProperties properties) { + super(properties, DEFAULTS); + } + + @Override + public String get(String s) { + return null; + } + + @Override + public StatsdFlavor flavor() { + return get(StatsdProperties::getFlavor, StatsdConfig::flavor); + } + + @Override + public boolean enabled() { + return get(StatsdProperties::getEnabled, StatsdConfig::enabled); + } + + @Override + public String host() { + return get(StatsdProperties::getHost, StatsdConfig::host); + } + + @Override + public int port() { + return get(StatsdProperties::getPort, StatsdConfig::port); + } + + @Override + public int maxPacketLength() { + return get(StatsdProperties::getMaxPacketLength, StatsdConfig::maxPacketLength); + } + + @Override + public Duration pollingFrequency() { + return get(StatsdProperties::getPollingFrequency, StatsdConfig::pollingFrequency); + } + + @Override + public int queueSize() { + return get(StatsdProperties::getQueueSize, StatsdConfig::queueSize); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/package-info.java new file mode 100644 index 0000000000..b2eec12eb2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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. + */ + +/** + * Support for exporting actuator metrics to StatsD. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.export.statsd; 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 a988cb9cd3..4a83bb1e0e 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 @@ -22,9 +22,9 @@ import java.util.Set; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Statistic; -import io.micrometer.core.instrument.binder.JvmMemoryMetrics; -import io.micrometer.core.instrument.binder.LogbackMetrics; import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.logging.LogbackMetrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleExportConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleExportConfigurationTests.java index 3f6af8de68..4a76b3a69c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleExportConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleExportConfigurationTests.java @@ -45,7 +45,8 @@ public class SimpleExportConfigurationTests { "spring.metrics.graphite.enabled=false", "spring.metrics.influx.enabled=false", "spring.metrics.jmx.enabled=false", - "spring.metrics.prometheus.enabled=false") + "spring.metrics.prometheus.enabled=false", + "spring.metrics.statsd.enabled=false") .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class)) .run((context) -> { CompositeMeterRegistry meterRegistry = context diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java index 7f61ccd272..f41c6cb50b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java @@ -18,22 +18,23 @@ package org.springframework.boot.actuate.metrics; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import io.micrometer.core.instrument.Measurement; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.NamingConvention; import io.micrometer.core.instrument.Statistic; -import io.micrometer.core.instrument.util.HierarchicalNameMapper; +import io.micrometer.core.instrument.Tag; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * An {@link Endpoint} for exposing the metrics held by a {@link MeterRegistry}. @@ -61,57 +62,133 @@ public class MetricsEndpoint { } @ReadOperation - public Map> metric( - @Selector String requiredMetricName) { - return this.registry.find(requiredMetricName).meters().stream() - .collect(Collectors.toMap(this::getHierarchicalName, this::getSamples)); + public Response metric(@Selector String requiredMetricName, + @Nullable List tag) { + Assert.isTrue(tag == null || tag.stream().allMatch((t) -> t.contains(":")), + "Each tag parameter must be in the form key:value"); + List tags = parseTags(tag); + Collection meters = this.registry.find(requiredMetricName).tags(tags) + .meters(); + if (meters.isEmpty()) { + return null; + } + + Map samples = new HashMap<>(); + Map> availableTags = new HashMap<>(); + + for (Meter meter : meters) { + for (Measurement ms : meter.measure()) { + samples.merge(ms.getStatistic(), ms.getValue(), Double::sum); + } + for (Tag availableTag : meter.getId().getTags()) { + availableTags.merge(availableTag.getKey(), + Collections.singletonList(availableTag.getValue()), + (t1, t2) -> Stream.concat(t1.stream(), t2.stream()) + .collect(Collectors.toList())); + } + } + + tags.forEach((t) -> availableTags.remove(t.getKey())); + + return new Response(requiredMetricName, + samples.entrySet().stream() + .map((sample) -> new Response.Sample(sample.getKey(), + sample.getValue())) + .collect( + Collectors.toList()), + availableTags.entrySet().stream() + .map((tagValues) -> new Response.AvailableTag(tagValues.getKey(), + tagValues.getValue())) + .collect(Collectors.toList())); } - private List getSamples(Meter meter) { - return stream(meter.measure()).map(this::getSample).collect(Collectors.toList()); - } - - private MeasurementSample getSample(Measurement measurement) { - return new MeasurementSample(measurement.getStatistic(), measurement.getValue()); - } - - private String getHierarchicalName(Meter meter) { - return HierarchicalNameMapper.DEFAULT.toHierarchicalName(meter.getId(), - NamingConvention.camelCase); - } - - private Stream stream(Iterable measure) { - return StreamSupport.stream(measure.spliterator(), false); + private List parseTags(List tags) { + return tags == null ? Collections.emptyList() : tags.stream().map((t) -> { + String[] tagParts = t.split(":", 2); + return Tag.of(tagParts[0], tagParts[1]); + }).collect(Collectors.toList()); } /** - * A measurement sample combining a {@link Statistic statistic} and a value. + * Response payload. */ - static class MeasurementSample { + static class Response { - private final Statistic statistic; + private final String name; - private final Double value; + private final List measurements; - MeasurementSample(Statistic statistic, Double value) { - this.statistic = statistic; - this.value = value; + private final List availableTags; + + Response(String name, List measurements, + List availableTags) { + this.name = name; + this.measurements = measurements; + this.availableTags = availableTags; } - public Statistic getStatistic() { - return this.statistic; + public String getName() { + return this.name; } - public Double getValue() { - return this.value; + public List getMeasurements() { + return this.measurements; } - @Override - public String toString() { - return "MeasurementSample{" + "statistic=" + this.statistic + ", value=" - + this.value + '}'; + public List getAvailableTags() { + return this.availableTags; } + /** + * A set of tags for further dimensional drilldown and their potential values. + */ + static class AvailableTag { + + private final String tag; + + private final List values; + + AvailableTag(String tag, List values) { + this.tag = tag; + this.values = values; + } + + public String getTag() { + return this.tag; + } + + public List getValues() { + return this.values; + } + } + + /** + * A measurement sample combining a {@link Statistic statistic} and a value. + */ + static class Sample { + + private final Statistic statistic; + + private final Double value; + + Sample(Statistic statistic, Double value) { + this.statistic = statistic; + this.value = value; + } + + public Statistic getStatistic() { + return this.statistic; + } + + public Double getValue() { + return this.value; + } + + @Override + public String toString() { + return "MeasurementSample{" + "statistic=" + this.statistic + ", value=" + + this.value + '}'; + } + } } - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java index d3cf8b95fc..f3e85f7833 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java @@ -16,34 +16,33 @@ package org.springframework.boot.actuate.metrics; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Meter.Id; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleCounter; +import io.micrometer.core.instrument.Statistic; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link MetricsEndpoint}. * * @author Andy Wilkinson + * @author Jon Schneider */ public class MetricsEndpointTests { - private final MeterRegistry registry = mock(MeterRegistry.class); + private final MeterRegistry registry = new SimpleMeterRegistry(); private final MetricsEndpoint endpoint = new MetricsEndpoint(this.registry); @Test public void listNamesHandlesEmptyListOfMeters() { - given(this.registry.getMeters()).willReturn(Arrays.asList()); Map> result = this.endpoint.listNames(); assertThat(result).containsOnlyKeys("names"); assertThat(result.get("names")).isEmpty(); @@ -51,23 +50,56 @@ public class MetricsEndpointTests { @Test public void listNamesProducesListOfUniqueMeterNames() { - List meters = Arrays.asList(createCounter("com.example.foo"), - createCounter("com.example.bar"), createCounter("com.example.foo")); - given(this.registry.getMeters()).willReturn(meters); + this.registry.counter("com.example.foo"); + this.registry.counter("com.example.bar"); + this.registry.counter("com.example.foo"); Map> result = this.endpoint.listNames(); assertThat(result).containsOnlyKeys("names"); assertThat(result.get("names")).containsOnlyOnce("com.example.foo", "com.example.bar"); } - private Meter createCounter(String name) { - return new SimpleCounter(createMeterId(name)); + @Test + public void metricValuesAreTheSumOfAllTimeSeriesMatchingTags() { + this.registry.counter("cache", "result", "hit", "host", "1").increment(2); + this.registry.counter("cache", "result", "miss", "host", "1").increment(2); + this.registry.counter("cache", "result", "hit", "host", "2").increment(2); + MetricsEndpoint.Response response = this.endpoint.metric("cache", + Collections.emptyList()); + assertThat(response.getName()).isEqualTo("cache"); + assertThat(availableTagKeys(response)).containsExactly("result", "host"); + assertThat(getCount(response)).hasValue(6.0); + response = this.endpoint.metric("cache", Collections.singletonList("result:hit")); + assertThat(availableTagKeys(response)).containsExactly("host"); + assertThat(getCount(response)).hasValue(4.0); } - private Id createMeterId(String name) { - Id id = mock(Id.class); - given(id.getName()).willReturn(name); - return id; + @Test + public void metricWithSpaceInTagValue() { + this.registry.counter("counter", "key", "a space").increment(2); + MetricsEndpoint.Response response = this.endpoint.metric("counter", + Collections.singletonList("key:a space")); + assertThat(response.getName()).isEqualTo("counter"); + assertThat(availableTagKeys(response)).isEmpty(); + assertThat(getCount(response)).hasValue(2.0); + } + + @Test + public void nonExistentMetric() { + MetricsEndpoint.Response response = this.endpoint.metric("does.not.exist", + Collections.emptyList()); + assertThat(response).isNull(); + } + + private Optional getCount(MetricsEndpoint.Response response) { + return response.getMeasurements().stream() + .filter((ms) -> ms.getStatistic().equals(Statistic.Count)).findAny() + .map(MetricsEndpoint.Response.Sample::getValue); + } + + private Stream availableTagKeys(MetricsEndpoint.Response response) { + return response.getAvailableTags().stream() + .map(MetricsEndpoint.Response.AvailableTag::getTag); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointWebIntegrationTests.java index 5badb345ac..ed4b1d6f4e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointWebIntegrationTests.java @@ -22,7 +22,7 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.JvmMemoryMetrics; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,9 +61,15 @@ public class MetricsEndpointWebIntegrationTests { public void selectByName() throws IOException { MetricsEndpointWebIntegrationTests.client.get() .uri("/application/metrics/jvm.memory.used").exchange().expectStatus() - .isOk().expectBody() - .jsonPath("['jvmMemoryUsed.area.nonheap.id.Compressed_Class_Space']") - .exists().jsonPath("['jvmMemoryUsed.area.heap.id.PS_Old_Gen']"); + .isOk().expectBody().jsonPath("$.name").isEqualTo("jvm.memory.used"); + } + + @Test + public void selectByTag() { + MetricsEndpointWebIntegrationTests.client.get() + .uri("/application/metrics/jvm.memory.used?tag=id:PS%20Old%20Gen") + .exchange().expectStatus().isOk().expectBody().jsonPath("$.name") + .isEqualTo("jvm.memory.used"); } @Configuration diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 01c8671ab6..15c2c1aa63 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -113,7 +113,7 @@ 1.2.3 1.16.18 2.1.2 - 1.0.0-rc.1 + 1.0.0-SNAPSHOT 6.2.1.jre8 2.10.0 1.6.0 @@ -942,6 +942,11 @@ micrometer-registry-prometheus ${micrometer.version} + + io.micrometer + micrometer-registry-statsd + ${micrometer.version} + io.netty netty-bom