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