Add support for Statsd metric export
This commit is contained in:
		
							parent
							
								
									0fde04d325
								
							
						
					
					
						commit
						0bd845d183
					
				|  | @ -1,5 +1,6 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||
| 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||
| 	<modelVersion>4.0.0</modelVersion> | ||||
| 	<parent> | ||||
| 		<groupId>org.springframework.boot</groupId> | ||||
|  | @ -62,6 +63,11 @@ | |||
| 			<artifactId>javax.mail</artifactId> | ||||
| 			<optional>true</optional> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>com.timgroup</groupId> | ||||
| 			<artifactId>java-statsd-client</artifactId> | ||||
| 			<optional>true</optional> | ||||
| 		</dependency> | ||||
| 		<dependency> | ||||
| 			<groupId>io.dropwizard.metrics</groupId> | ||||
| 			<artifactId>metrics-core</artifactId> | ||||
|  |  | |||
|  | @ -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()); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -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; | ||||
| 
 | ||||
|  | @ -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<Long>("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<Long>("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<Double>("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<Long>("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<Long>("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<Long>("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<String> messagesReceived = new ArrayList<String>(); | ||||
| 		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<String> messagesReceived() { | ||||
| 			return new ArrayList<String>(this.messagesReceived); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -136,6 +136,7 @@ | |||
| 		<spring-social-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version> | ||||
| 		<spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version> | ||||
| 		<spring-ws.version>2.2.1.RELEASE</spring-ws.version> | ||||
| 		<statsd-client.version>3.0.1</statsd-client.version> | ||||
| 		<sun-mail.version>${javax-mail.version}</sun-mail.version> | ||||
| 		<thymeleaf.version>2.1.4.RELEASE</thymeleaf.version> | ||||
| 		<thymeleaf-extras-springsecurity4.version>2.1.2.RELEASE</thymeleaf-extras-springsecurity4.version> | ||||
|  | @ -552,6 +553,11 @@ | |||
| 				<artifactId>javax.mail</artifactId> | ||||
| 				<version>${sun-mail.version}</version> | ||||
| 			</dependency> | ||||
| 			<dependency> | ||||
| 				<groupId>com.timgroup</groupId> | ||||
| 				<artifactId>java-statsd-client</artifactId> | ||||
| 				<version>${statsd-client.version}</version> | ||||
| 			</dependency> | ||||
| 			<dependency> | ||||
| 				<groupId>com.zaxxer</groupId> | ||||
| 				<artifactId>HikariCP</artifactId> | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue