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"?> | <?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> | 	<modelVersion>4.0.0</modelVersion> | ||||||
| 	<parent> | 	<parent> | ||||||
| 		<groupId>org.springframework.boot</groupId> | 		<groupId>org.springframework.boot</groupId> | ||||||
|  | @ -62,6 +63,11 @@ | ||||||
| 			<artifactId>javax.mail</artifactId> | 			<artifactId>javax.mail</artifactId> | ||||||
| 			<optional>true</optional> | 			<optional>true</optional> | ||||||
| 		</dependency> | 		</dependency> | ||||||
|  | 		<dependency> | ||||||
|  | 			<groupId>com.timgroup</groupId> | ||||||
|  | 			<artifactId>java-statsd-client</artifactId> | ||||||
|  | 			<optional>true</optional> | ||||||
|  | 		</dependency> | ||||||
| 		<dependency> | 		<dependency> | ||||||
| 			<groupId>io.dropwizard.metrics</groupId> | 			<groupId>io.dropwizard.metrics</groupId> | ||||||
| 			<artifactId>metrics-core</artifactId> | 			<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-linkedin.version>1.0.1.RELEASE</spring-social-linkedin.version> | ||||||
| 		<spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version> | 		<spring-social-twitter.version>1.1.0.RELEASE</spring-social-twitter.version> | ||||||
| 		<spring-ws.version>2.2.1.RELEASE</spring-ws.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> | 		<sun-mail.version>${javax-mail.version}</sun-mail.version> | ||||||
| 		<thymeleaf.version>2.1.4.RELEASE</thymeleaf.version> | 		<thymeleaf.version>2.1.4.RELEASE</thymeleaf.version> | ||||||
| 		<thymeleaf-extras-springsecurity4.version>2.1.2.RELEASE</thymeleaf-extras-springsecurity4.version> | 		<thymeleaf-extras-springsecurity4.version>2.1.2.RELEASE</thymeleaf-extras-springsecurity4.version> | ||||||
|  | @ -552,6 +553,11 @@ | ||||||
| 				<artifactId>javax.mail</artifactId> | 				<artifactId>javax.mail</artifactId> | ||||||
| 				<version>${sun-mail.version}</version> | 				<version>${sun-mail.version}</version> | ||||||
| 			</dependency> | 			</dependency> | ||||||
|  | 			<dependency> | ||||||
|  | 				<groupId>com.timgroup</groupId> | ||||||
|  | 				<artifactId>java-statsd-client</artifactId> | ||||||
|  | 				<version>${statsd-client.version}</version> | ||||||
|  | 			</dependency> | ||||||
| 			<dependency> | 			<dependency> | ||||||
| 				<groupId>com.zaxxer</groupId> | 				<groupId>com.zaxxer</groupId> | ||||||
| 				<artifactId>HikariCP</artifactId> | 				<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]] | [[production-ready-metric-aggregation]] | ||||||
| === Aggregating metrics from multiple sources | === Aggregating metrics from multiple sources | ||||||
| There is an `AggregateMetricReader` that you can use to consolidate metrics from different | There is an `AggregateMetricReader` that you can use to consolidate metrics from different | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue