diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 67849c35fa5..5ef4819a9de 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.springframework.boot @@ -62,6 +63,11 @@ javax.mail true + + com.timgroup + java-statsd-client + true + io.dropwizard.metrics metrics-core diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriter.java new file mode 100644 index 00000000000..2724ca9e42b --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2015 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.statsd; + +import java.io.Closeable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.util.StringUtils; + +import com.timgroup.statsd.NonBlockingStatsDClient; +import com.timgroup.statsd.StatsDClientErrorHandler; + +/** + * A {@link MetricWriter} that pushes data to statsd. Statsd has the concept of counters + * and gauges, but only supports gauges with data type Long, so values will be truncated + * towards zero. Metrics whose name contains "timer." (but not "gauge." or "counter.") + * will be treated as execution times (in statsd terms). Anything incremented is treated + * as a counter, and anything with a snapshot value in {@link #set(Metric)} is treated as + * a gauge. + * + * @author Dave Syer + */ +public class StatsdMetricWriter implements MetricWriter, Closeable { + + private static Log logger = LogFactory.getLog(StatsdMetricWriter.class); + + private final NonBlockingStatsDClient client; + + /** + * Create a new writer with the given parameters. + * + * @param host the hostname for the statsd server + * @param port the port for the statsd server + */ + public StatsdMetricWriter(String host, int port) { + this(null, host, port); + } + + /** + * Create a new writer with the given parameters. + * + * @param prefix the prefix to apply to all metric names (can be null) + * @param host the hostname for the statsd server + * @param port the port for the statsd server + */ + public StatsdMetricWriter(String prefix, String host, int port) { + prefix = StringUtils.hasText(prefix) ? prefix : null; + while (prefix != null && prefix.endsWith(".")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + this.client = new NonBlockingStatsDClient(prefix, host, port, + new LoggingStatsdErrorHandler()); + } + + @Override + public void increment(Delta delta) { + this.client.count(delta.getName(), delta.getValue().longValue()); + } + + @Override + public void set(Metric value) { + String name = value.getName(); + if (name.contains("timer.") && !name.contains("gauge.") + && !name.contains("counter.")) { + this.client.recordExecutionTime(name, value.getValue().longValue()); + } + else { + this.client.gauge(name, value.getValue().longValue()); + } + } + + @Override + public void reset(String name) { + if (name.contains("counter.")) { + this.client.gauge(name, 0L); + } + } + + @Override + public void close() { + this.client.stop(); + } + + private static final class LoggingStatsdErrorHandler implements + StatsDClientErrorHandler { + @Override + public void handle(Exception e) { + logger.debug("Failed to write metric. Exception: " + e.getClass() + + ", message: " + e.getMessage()); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/package-info.java new file mode 100644 index 00000000000..237c6794528 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/statsd/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2015 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. + */ + +/** + * Metrics integration with Statsd. + */ +package org.springframework.boot.actuate.metrics.statsd; + diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java new file mode 100644 index 00000000000..c90f6f4a0d5 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/statsd/StatsdMetricWriterTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2012-2015 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.statsd; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.util.SocketUtils; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link StatsdMetricWriter}. + * + * @author Dave Syer + */ +public class StatsdMetricWriterTests { + + private int port = SocketUtils.findAvailableTcpPort(); + + private DummyStatsDServer server = new DummyStatsDServer(this.port); + + private StatsdMetricWriter writer = new StatsdMetricWriter("me", "localhost", + this.port); + + @After + public void close() { + this.server.stop(); + this.writer.close(); + } + + @Test + public void increment() { + this.writer.increment(new Delta("counter.foo", 3L)); + this.server.waitForMessage(); + assertEquals("me.counter.foo:3|c", this.server.messagesReceived().get(0)); + } + + @Test + public void setLongMetric() throws Exception { + this.writer.set(new Metric("gauge.foo", 3L)); + this.server.waitForMessage(); + assertEquals("me.gauge.foo:3|g", this.server.messagesReceived().get(0)); + } + + @Test + public void setDoubleMetric() throws Exception { + this.writer.set(new Metric("gauge.foo", 3.7)); + this.server.waitForMessage(); + // Doubles are truncated + assertEquals("me.gauge.foo:3|g", this.server.messagesReceived().get(0)); + } + + @Test + public void setTimerMetric() throws Exception { + this.writer.set(new Metric("timer.foo", 37L)); + this.server.waitForMessage(); + assertEquals("me.timer.foo:37|ms", this.server.messagesReceived().get(0)); + } + + @Test + public void nullPrefix() throws Exception { + this.writer = new StatsdMetricWriter("localhost", this.port); + this.writer.set(new Metric("gauge.foo", 3L)); + this.server.waitForMessage(); + assertEquals("gauge.foo:3|g", this.server.messagesReceived().get(0)); + } + + @Test + public void perioPrefix() throws Exception { + this.writer = new StatsdMetricWriter("my.", "localhost", this.port); + this.writer.set(new Metric("gauge.foo", 3L)); + this.server.waitForMessage(); + assertEquals("my.gauge.foo:3|g", this.server.messagesReceived().get(0)); + } + + private static final class DummyStatsDServer { + + private final List messagesReceived = new ArrayList(); + private final DatagramSocket server; + + public DummyStatsDServer(int port) { + try { + this.server = new DatagramSocket(port); + } + catch (SocketException e) { + throw new IllegalStateException(e); + } + new Thread(new Runnable() { + @Override + public void run() { + try { + final DatagramPacket packet = new DatagramPacket(new byte[256], + 256); + DummyStatsDServer.this.server.receive(packet); + DummyStatsDServer.this.messagesReceived.add(new String(packet + .getData(), Charset.forName("UTF-8")).trim()); + } + catch (Exception e) { + } + } + }).start(); + } + + public void stop() { + this.server.close(); + } + + public void waitForMessage() { + while (this.messagesReceived.isEmpty()) { + try { + Thread.sleep(50L); + } + catch (InterruptedException e) { + } + } + } + + public List messagesReceived() { + return new ArrayList(this.messagesReceived); + } + } +} diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index f065efc8263..89ff0c6e430 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -136,6 +136,7 @@ 1.0.1.RELEASE 1.1.0.RELEASE 2.2.1.RELEASE + 3.0.1 ${javax-mail.version} 2.1.4.RELEASE 2.1.2.RELEASE @@ -552,6 +553,11 @@ javax.mail ${sun-mail.version} + + com.timgroup + java-statsd-client + ${statsd-client.version} + com.zaxxer HikariCP diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 0c5ea7ea821..f0b9e0c15a7 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -957,6 +957,51 @@ MetricWriter metricWriter() { +[[production-ready-metric-writers-export-to-statsd]] +==== Example: Export to Statsd +If you provide a `@Bean` of type `StatsdMetricWriter` the metrics are exported to a +statsd server: + +[source,java,indent=0] +---- +@Value("${spring.application.name:application}.${random.value:0000}") +private String prefix = "metrics"; + +@Value("${statsd.host:localhost}") +private String host = "localhost"; + +@Value("${statsd.port:8125}") +private int port; + +@Bean +MetricWriter metricWriter() { + return new StatsdMetricWriter(prefix, host, port); +} +---- + + +[[production-ready-metric-writers-export-to-jmx]] +==== Example: Export to JMX +If you provide a `@Bean` of type `JmxMetricWriter` the metrics are exported as MBeans to +the local server (the `MBeanExporter` is provided by Spring Boot JMX autoconfiguration as +long as it is switched on). Metrics can then be inspected, graphed, alerted etc. using any +tool that understands JMX (e.g. JConsole or JVisualVM). Example: + +[source,java,indent=0] +---- +@Bean +MetricWriter metricWriter(MBeanExporter exporter) { + return new JmxMetricWriter(exporter); +} +---- + +Each metric is exported as an individual MBean. The format for the `ObjectNames` is given +by an `ObjectNamingStrategy` which can be injected into the `JmxMetricWriter` (the default +breaks up the metric name and tags the first two period-separated sections in a way that +should make the metrics group nicely in JVisualVM or JConsole). + + + [[production-ready-metric-aggregation]] === Aggregating metrics from multiple sources There is an `AggregateMetricReader` that you can use to consolidate metrics from different