diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index caa62a59fe..b2df4ffca0 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.springframework.boot @@ -61,6 +62,11 @@ javax.mail true + + com.timgroup + java-statsd-client + true + io.dropwizard.metrics metrics-core @@ -214,6 +220,11 @@ tomcat-embed-logging-juli test + + org.aspectj + aspectjrt + test + org.crashub crash.connectors.telnet @@ -234,5 +245,10 @@ spring-data-elasticsearch test + + org.springframework.integration + spring-integration-core + test + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ActuatorMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ActuatorMetricRepository.java new file mode 100644 index 0000000000..d18bcf68a5 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ActuatorMetricRepository.java @@ -0,0 +1,42 @@ +/* + * 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.autoconfigure; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Qualifier annotation for a metric repository that is used by the actuator (to + * distinguish it from others that might be installed by the user). + * + * @author Dave Syer + */ +@Qualifier +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, + ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ActuatorMetricRepository { + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java new file mode 100644 index 0000000000..7c6672d7ef --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * 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.autoconfigure; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.export.MetricExportProperties; +import org.springframework.boot.actuate.metrics.export.MetricExporters; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * @author Dave Syer + */ +@Configuration +@EnableScheduling +@ConditionalOnProperty(value = "spring.metrics.export.enabled", matchIfMissing = true) +public class MetricExportAutoConfiguration { + + @Autowired(required = false) + private Map writers = Collections.emptyMap(); + + @Autowired + private MetricExportProperties metrics; + + @Autowired(required = false) + @ActuatorMetricRepository + private MetricWriter actuatorMetricRepository; + + @Autowired(required = false) + @ActuatorMetricRepository + private MetricReader reader; + + @Bean + @ConditionalOnMissingBean + public SchedulingConfigurer metricWritersMetricExporter() { + Map writers = new HashMap(); + if (this.reader != null) { + writers.putAll(this.writers); + if (this.actuatorMetricRepository != null + && writers.containsValue(this.actuatorMetricRepository)) { + for (String name : this.writers.keySet()) { + if (writers.get(name).equals(this.actuatorMetricRepository)) { + writers.remove(name); + } + } + } + MetricExporters exporters = new MetricExporters(this.reader, writers, + this.metrics); + return exporters; + } + return new SchedulingConfigurer() { + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + } + }; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfiguration.java index b3e8a4ab81..1d1e8d148a 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfiguration.java @@ -16,37 +16,31 @@ package org.springframework.boot.actuate.autoconfigure; -import java.util.List; -import java.util.concurrent.Executor; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; -import org.springframework.boot.actuate.endpoint.PublicMetrics; import org.springframework.boot.actuate.metrics.CounterService; import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.buffer.BufferCounterService; +import org.springframework.boot.actuate.metrics.buffer.BufferGaugeService; +import org.springframework.boot.actuate.metrics.buffer.BufferMetricReader; +import org.springframework.boot.actuate.metrics.buffer.CounterBuffers; +import org.springframework.boot.actuate.metrics.buffer.GaugeBuffers; import org.springframework.boot.actuate.metrics.export.Exporter; -import org.springframework.boot.actuate.metrics.reader.MetricRegistryMetricReader; +import org.springframework.boot.actuate.metrics.export.MetricCopyExporter; +import org.springframework.boot.actuate.metrics.export.MetricExportProperties; import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; import org.springframework.boot.actuate.metrics.repository.MetricRepository; -import org.springframework.boot.actuate.metrics.writer.CompositeMetricWriter; import org.springframework.boot.actuate.metrics.writer.DefaultCounterService; import org.springframework.boot.actuate.metrics.writer.DefaultGaugeService; -import org.springframework.boot.actuate.metrics.writer.DropwizardMetricWriter; -import org.springframework.boot.actuate.metrics.writer.MessageChannelMetricWriter; import org.springframework.boot.actuate.metrics.writer.MetricWriter; -import org.springframework.boot.actuate.metrics.writer.MetricWriterMessageHandler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnJava; +import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.JavaVersion; +import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.Range; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ExecutorSubscribableChannel; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import com.codahale.metrics.MetricRegistry; @@ -62,125 +56,106 @@ import com.codahale.metrics.MetricRegistry; * periodic basis) using an {@link Exporter}, most implementations of which have * optimizations for sending data to remote repositories. *

- * If Spring Messaging is on the classpath a {@link MessageChannel} called - * "metricsChannel" is also created (unless one already exists) and all metric update - * events are published additionally as messages on that channel. Additional analysis or - * actions can be taken by clients subscribing to that channel. + * If Spring Messaging is on the classpath and a {@link MessageChannel} called + * "metricsChannel" is also available, all metric update events are published additionally + * as messages on that channel. Additional analysis or actions can be taken by clients + * subscribing to that channel. *

- * In addition if Codahale's metrics library is on the classpath a {@link MetricRegistry} - * will be created and wired up to the counter and gauge services in addition to the basic - * repository. Users can create Codahale metrics by prefixing their metric names with the - * appropriate type (e.g. "histogram.*", "meter.*") and sending them to the standard - * GaugeService or CounterService. + * In addition if Dropwizard's metrics library is on the classpath a + * {@link MetricRegistry} will be created and the default counter and gauge services will + * switch to using it instead of the default repository. Users can create "special" + * Dropwizard metrics by prefixing their metric names with the appropriate type (e.g. + * "histogram.*", "meter.*". "timer.*") and sending them to the GaugeService + * or CounterService. *

* By default all metric updates go to all {@link MetricWriter} instances in the - * application context. To change this behaviour define your own metric writer bean called - * "primaryMetricWriter", mark it @Primary, and this one will receive all - * updates from the default counter and gauge services. Alternatively you can provide your - * own counter and gauge services and wire them to whichever writer you choose. + * application context via a {@link MetricCopyExporter} firing every 5 seconds (disable + * this by setting spring.metrics.export.enabled=false). * * @see GaugeService * @see CounterService * @see MetricWriter * @see InMemoryMetricRepository - * @see DropwizardMetricWriter * @see Exporter * * @author Dave Syer */ @Configuration +@EnableConfigurationProperties(MetricExportProperties.class) public class MetricRepositoryAutoConfiguration { - @Autowired - private MetricWriter writer; + @Configuration + @ConditionalOnJava(value = JavaVersion.EIGHT, range = Range.OLDER_THAN) + @ConditionalOnMissingBean(GaugeService.class) + static class LegacyMetricServicesConfiguration { - @Bean - @ConditionalOnMissingBean - public CounterService counterService() { - return new DefaultCounterService(this.writer); - } + @Autowired + @ActuatorMetricRepository + private MetricWriter writer; + + @Bean + @ConditionalOnMissingBean + public CounterService counterService() { + return new DefaultCounterService(this.writer); + } + + @Bean + @ConditionalOnMissingBean + public GaugeService gaugeService() { + return new DefaultGaugeService(this.writer); + } - @Bean - @ConditionalOnMissingBean - public GaugeService gaugeService() { - return new DefaultGaugeService(this.writer); } @Configuration - @ConditionalOnMissingBean(MetricRepository.class) - static class MetricRepositoryConfiguration { + @ConditionalOnJava(value = JavaVersion.EIGHT) + @ConditionalOnMissingBean(GaugeService.class) + static class FastMetricServicesConfiguration { @Bean + @ConditionalOnMissingBean + public CounterBuffers counterBuffers() { + return new CounterBuffers(); + } + + @Bean + @ConditionalOnMissingBean + public GaugeBuffers gaugeBuffers() { + return new GaugeBuffers(); + } + + @Bean + @ActuatorMetricRepository + @ConditionalOnMissingBean + public BufferMetricReader actuatorMetricReader(CounterBuffers counters, + GaugeBuffers gauges) { + return new BufferMetricReader(counters, gauges); + } + + @Bean + @ConditionalOnMissingBean + public CounterService counterService(CounterBuffers writer) { + return new BufferCounterService(writer); + } + + @Bean + @ConditionalOnMissingBean + public GaugeService gaugeService(GaugeBuffers writer) { + return new BufferGaugeService(writer); + } + } + + @Configuration + @ConditionalOnJava(value = JavaVersion.EIGHT, range = Range.OLDER_THAN) + @ConditionalOnMissingBean(MetricRepository.class) + static class LegacyMetricRepositoryConfiguration { + + @Bean + @ActuatorMetricRepository public InMemoryMetricRepository actuatorMetricRepository() { return new InMemoryMetricRepository(); } } - @Configuration - @ConditionalOnClass(MessageChannel.class) - static class MetricsChannelConfiguration { - - @Autowired - @Qualifier("metricsExecutor") - private Executor executor; - - @Bean - @ConditionalOnMissingBean(name = "metricsChannel") - public SubscribableChannel metricsChannel() { - return new ExecutorSubscribableChannel(this.executor); - } - - @Bean - @ConditionalOnMissingBean(name = "metricsExecutor") - public Executor metricsExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - return executor; - } - - @Bean - @Primary - @ConditionalOnMissingBean(name = "primaryMetricWriter") - public MetricWriter primaryMetricWriter( - @Qualifier("metricsChannel") SubscribableChannel channel, - List writers) { - final MetricWriter observer = new CompositeMetricWriter(writers); - channel.subscribe(new MetricWriterMessageHandler(observer)); - return new MessageChannelMetricWriter(channel); - } - - } - - @Configuration - @ConditionalOnClass(MetricRegistry.class) - static class DropwizardMetricRegistryConfiguration { - - @Bean - @ConditionalOnMissingBean - public MetricRegistry metricRegistry() { - return new MetricRegistry(); - } - - @Bean - public DropwizardMetricWriter dropwizardMetricWriter(MetricRegistry metricRegistry) { - return new DropwizardMetricWriter(metricRegistry); - } - - @Bean - @Primary - @ConditionalOnMissingClass(name = "org.springframework.messaging.MessageChannel") - @ConditionalOnMissingBean(name = "primaryMetricWriter") - public MetricWriter primaryMetricWriter(List writers) { - return new CompositeMetricWriter(writers); - } - - @Bean - public PublicMetrics dropwizardPublicMetrics(MetricRegistry metricRegistry) { - MetricRegistryMetricReader reader = new MetricRegistryMetricReader( - metricRegistry); - return new MetricReaderPublicMetrics(reader); - } - - } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsChannelAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsChannelAutoConfiguration.java new file mode 100644 index 0000000000..154451a4e7 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsChannelAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.autoconfigure; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.metrics.writer.MessageChannelMetricWriter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.MessageChannel; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for writing metrics to a + * {@link MessageChannel}. + * + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass(MessageChannel.class) +@ConditionalOnBean(name = "metricsChannel") +@AutoConfigureBefore(MetricRepositoryAutoConfiguration.class) +public class MetricsChannelAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MessageChannelMetricWriter messageChannelMetricWriter( + @Qualifier("metricsChannel") MessageChannel channel) { + return new MessageChannelMetricWriter(channel); + } + +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsDropwizardAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsDropwizardAutoConfiguration.java new file mode 100644 index 0000000000..14e29f2b44 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricsDropwizardAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; +import org.springframework.boot.actuate.endpoint.PublicMetrics; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.dropwizard.DropwizardMetricServices; +import org.springframework.boot.actuate.metrics.reader.MetricRegistryMetricReader; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.codahale.metrics.MetricRegistry; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Dropwizard-based metrics. + * + * @author Dave Syer + */ +@Configuration +@ConditionalOnClass(MetricRegistry.class) +@AutoConfigureBefore(MetricRepositoryAutoConfiguration.class) +public class MetricsDropwizardAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MetricRegistry metricRegistry() { + return new MetricRegistry(); + } + + @Bean + @ConditionalOnMissingBean({ DropwizardMetricServices.class, CounterService.class, + GaugeService.class }) + public DropwizardMetricServices dropwizardMetricServices(MetricRegistry metricRegistry) { + return new DropwizardMetricServices(metricRegistry); + } + + @Bean + public PublicMetrics dropwizardPublicMetrics(MetricRegistry metricRegistry) { + MetricRegistryMetricReader reader = new MetricRegistryMetricReader(metricRegistry); + return new MetricReaderPublicMetrics(reader); + } + +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java index ae5f0adf2e..4580e197c5 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java @@ -60,6 +60,7 @@ import org.springframework.context.annotation.Configuration; public class PublicMetricsAutoConfiguration { @Autowired(required = false) + @ActuatorMetricRepository private MetricReader metricReader = new InMemoryMetricRepository(); @Bean diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java index 1d20bc228c..2c11bc056c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/MetricsEndpoint.java @@ -62,8 +62,13 @@ public class MetricsEndpoint extends AbstractEndpoint> { public Map invoke() { Map result = new LinkedHashMap(); for (PublicMetrics publicMetric : this.publicMetrics) { - for (Metric metric : publicMetric.metrics()) { - result.put(metric.getName(), metric.getValue()); + try { + for (Metric metric : publicMetric.metrics()) { + result.put(metric.getName(), metric.getValue()); + } + } + catch (Exception e) { + // Could not evaluate metrics } } return result; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java index 5d1b4c054a..ae54c50ca9 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java @@ -56,21 +56,21 @@ public class MetricsMvcEndpoint extends EndpointMvcAdapter { /** * {@link NamePatternFilter} for the Map source. */ - private class NamePatternMapFilter extends NamePatternFilter> { + private class NamePatternMapFilter extends NamePatternFilter> { - public NamePatternMapFilter(Map source) { + public NamePatternMapFilter(Map source) { super(source); } @Override - protected void getNames(Map source, NameCallback callback) { + protected void getNames(Map source, NameCallback callback) { for (String name : source.keySet()) { callback.addName(name); } } @Override - protected Object getValue(Map source, String name) { + protected Object getValue(Map source, String name) { Object value = source.get(name); if (value == null) { throw new NoSuchMetricException("No such metric: " + name); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java new file mode 100644 index 0000000000..2c8513503e --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java @@ -0,0 +1,142 @@ +/* + * Copyright 2014-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.aggregate; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.util.StringUtils; + +/** + * A metric reader that aggregates values from a source reader, normally one that has been + * collecting data from many sources in the same form (like a scaled-out application). The + * source has metrics with names in the form *.*.counter.** and + * *.*.[anything].** (the length of the prefix is controlled by the + * {@link #setTruncateKeyLength(int) truncateKeyLength} property, and defaults to 2, + * meaning 2 period separated fields), and the result has metric names in the form + * aggregate.count.** and aggregate.[anything].**. Counters are + * summed and anything else (i.e. gauges) are aggregated by choosing the most recent + * value. + * + * @author Dave Syer + * + */ +public class AggregateMetricReader implements MetricReader { + + private MetricReader source; + + private int truncate = 2; + + private String prefix = "aggregate."; + + public AggregateMetricReader(MetricReader source) { + this.source = source; + } + + /** + * The number of period-separated keys to remove from the start of the input metric + * names before aggregating. + * + * @param truncate length of source metric prefixes + */ + public void setTruncateKeyLength(int truncate) { + this.truncate = truncate; + } + + /** + * Prefix to apply to all output metrics. A period will be appended if no present in + * the provided value. + * + * @param prefix the prefix to use default "aggregator.") + */ + public void setPrefix(String prefix) { + this.prefix = prefix.endsWith(".") ? prefix : prefix + "."; + } + + @Override + public Metric findOne(String metricName) { + if (!metricName.startsWith(this.prefix)) { + return null; + } + InMemoryMetricRepository result = new InMemoryMetricRepository(); + String baseName = metricName.substring(this.prefix.length()); + for (Metric metric : this.source.findAll()) { + String name = getSourceKey(metric.getName()); + if (baseName.equals(name)) { + update(result, name, metric); + } + } + return result.findOne(metricName); + } + + @Override + public Iterable> findAll() { + InMemoryMetricRepository result = new InMemoryMetricRepository(); + for (Metric metric : this.source.findAll()) { + String key = getSourceKey(metric.getName()); + if (key != null) { + update(result, key, metric); + } + } + return result.findAll(); + } + + @Override + public long count() { + Set names = new HashSet(); + for (Metric metric : this.source.findAll()) { + String name = getSourceKey(metric.getName()); + if (name != null) { + names.add(name); + } + } + return names.size(); + } + + private void update(InMemoryMetricRepository result, String key, Metric metric) { + String name = this.prefix + key; + Metric aggregate = result.findOne(name); + if (aggregate == null) { + aggregate = new Metric(name, metric.getValue(), metric.getTimestamp()); + } + else if (key.contains("counter.")) { + // accumulate all values + aggregate = new Metric(name, metric.increment( + aggregate.getValue().intValue()).getValue(), metric.getTimestamp()); + } + else if (aggregate.getTimestamp().before(metric.getTimestamp())) { + // sort by timestamp and only take the latest + aggregate = new Metric(name, metric.getValue(), metric.getTimestamp()); + } + result.set(aggregate); + } + + private String getSourceKey(String name) { + String[] keys = StringUtils.delimitedListToStringArray(name, "."); + if (keys.length <= this.truncate) { + return null; + } + StringBuilder builder = new StringBuilder(keys[this.truncate]); + for (int i = this.truncate + 1; i < keys.length; i++) { + builder.append(".").append(keys[i]); + } + return builder.toString(); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferCounterService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferCounterService.java new file mode 100644 index 0000000000..34b4862b32 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferCounterService.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2014 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.buffer; + +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.lang.UsesJava8; + +/** + * Fast implementation of {@link CounterService} using {@link CounterBuffers}. + * + * @author Dave Syer + */ +@UsesJava8 +public class BufferCounterService implements CounterService { + + private final ConcurrentHashMap names = new ConcurrentHashMap(); + + private final CounterBuffers writer; + + /** + * Create a {@link BufferCounterService} instance. + * @param writer the underlying writer used to manage metrics + */ + public BufferCounterService(CounterBuffers writer) { + this.writer = writer; + } + + @Override + public void increment(String metricName) { + this.writer.increment(wrap(metricName), 1L); + } + + @Override + public void decrement(String metricName) { + this.writer.increment(wrap(metricName), -1L); + } + + @Override + public void reset(String metricName) { + this.writer.reset(wrap(metricName)); + } + + private String wrap(String metricName) { + if (this.names.containsKey(metricName)) { + return this.names.get(metricName); + } + if (metricName.startsWith("counter") || metricName.startsWith("meter")) { + return metricName; + } + String name = "counter." + metricName; + this.names.put(metricName, name); + return name; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeService.java new file mode 100644 index 0000000000..3ef6d697ac --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeService.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2014 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.buffer; + +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.lang.UsesJava8; + +/** + * Fast implementation of {@link GaugeService} using {@link GaugeBuffers}. + * + * @author Dave Syer + */ +@UsesJava8 +public class BufferGaugeService implements GaugeService { + + private final ConcurrentHashMap names = new ConcurrentHashMap(); + + private final GaugeBuffers writer; + + /** + * Create a {@link BufferGaugeService} instance. + * @param writer the underlying writer used to manage metrics + */ + public BufferGaugeService(GaugeBuffers writer) { + this.writer = writer; + } + + @Override + public void submit(String metricName, double value) { + this.writer.set(wrap(metricName), value); + } + + private String wrap(String metricName) { + if (this.names.containsKey(metricName)) { + return this.names.get(metricName); + } + if (metricName.startsWith("gauge") || metricName.startsWith("histogram") + || metricName.startsWith("timer")) { + return metricName; + } + String name = "gauge." + metricName; + this.names.put(metricName, name); + return name; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReader.java new file mode 100644 index 0000000000..b3856bd15e --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReader.java @@ -0,0 +1,113 @@ +/* + * 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.buffer; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.reader.PrefixMetricReader; +import org.springframework.lang.UsesJava8; + +/** + * {@link MetricReader} implementation using {@link CounterBuffers} and + * {@link GaugeBuffers}. + * + * @author Dave Syer + */ +@UsesJava8 +public class BufferMetricReader implements MetricReader, PrefixMetricReader { + + private final CounterBuffers counters; + + private final GaugeBuffers gauges; + + private final Predicate all = Pattern.compile(".*").asPredicate(); + + public BufferMetricReader(CounterBuffers counters, GaugeBuffers gauges) { + this.counters = counters; + this.gauges = gauges; + } + + @Override + public Iterable> findAll(String prefix) { + final List> metrics = new ArrayList>(); + this.counters.forEach(Pattern.compile(prefix + ".*").asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, LongBuffer value) { + metrics.add(new Metric(name, value.getValue(), new Date( + value.getTimestamp()))); + } + }); + this.gauges.forEach(Pattern.compile(prefix + ".*").asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, DoubleBuffer value) { + metrics.add(new Metric(name, value.getValue(), new Date( + value.getTimestamp()))); + } + }); + return metrics; + } + + @Override + public Metric findOne(final String name) { + LongBuffer buffer = this.counters.find(name); + if (buffer != null) { + return new Metric(name, buffer.getValue(), new Date( + buffer.getTimestamp())); + } + DoubleBuffer doubleValue = this.gauges.find(name); + if (doubleValue != null) { + return new Metric(name, doubleValue.getValue(), new Date( + doubleValue.getTimestamp())); + } + return null; + } + + @Override + public Iterable> findAll() { + final List> metrics = new ArrayList>(); + this.counters.forEach(this.all, new BiConsumer() { + @Override + public void accept(String name, LongBuffer value) { + metrics.add(new Metric(name, value.getValue(), new Date(value + .getTimestamp()))); + } + }); + this.gauges.forEach(this.all, new BiConsumer() { + @Override + public void accept(String name, DoubleBuffer value) { + metrics.add(new Metric(name, value.getValue(), new Date(value + .getTimestamp()))); + } + }); + return metrics; + } + + @Override + public long count() { + return this.counters.count() + this.gauges.count(); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffers.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffers.java new file mode 100644 index 0000000000..96878be4c3 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffers.java @@ -0,0 +1,116 @@ +/* + * 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.buffer; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.UsesJava8; + +/** + * Fast writes to in-memory metrics store using {@link LongBuffer}. + * + * @author Dave Syer + */ +@UsesJava8 +public class CounterBuffers { + + private final ConcurrentHashMap metrics = new ConcurrentHashMap(); + + public void forEach(final Predicate predicate, + final BiConsumer consumer) { + this.metrics.forEach(new BiConsumer() { + @Override + public void accept(String name, LongBuffer value) { + if (predicate.test(name)) { + consumer.accept(name, value); + } + } + }); + } + + public LongBuffer find(final String name) { + return this.metrics.get(name); + } + + public void get(final String name, final Consumer consumer) { + read(name, new Consumer() { + @Override + public void accept(LongBuffer adder) { + consumer.accept(adder); + } + }); + } + + public void increment(final String name, final long delta) { + write(name, new Consumer() { + @Override + public void accept(LongBuffer adder) { + adder.add(delta); + } + }); + } + + public void reset(final String name) { + write(name, new Consumer() { + @Override + public void accept(LongBuffer adder) { + adder.reset(); + } + }); + } + + public int count() { + return this.metrics.size(); + } + + private void read(final String name, final Consumer consumer) { + acceptInternal(name, new Consumer() { + @Override + public void accept(LongBuffer adder) { + consumer.accept(adder); + } + }); + } + + private void write(final String name, final Consumer consumer) { + acceptInternal(name, new Consumer() { + @Override + public void accept(LongBuffer buffer) { + buffer.setTimestamp(System.currentTimeMillis()); + consumer.accept(buffer); + } + }); + } + + private void acceptInternal(final String name, final Consumer consumer) { + LongBuffer adder; + if (null == (adder = this.metrics.get(name))) { + adder = this.metrics.computeIfAbsent(name, + new Function() { + @Override + public LongBuffer apply(String name) { + return new LongBuffer(0L); + } + }); + } + consumer.accept(adder); + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/DoubleBuffer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/DoubleBuffer.java new file mode 100644 index 0000000000..ec2d139abf --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/DoubleBuffer.java @@ -0,0 +1,50 @@ +/* + * 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.buffer; + +/** + * Mutable buffer containing a double value and a timestamp. + * + * @author Dave Syer + */ +public class DoubleBuffer { + + private volatile double value; + + private volatile long timestamp; + + public DoubleBuffer(long timestamp) { + this.value = 0; + this.timestamp = timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public double getValue() { + return this.value; + } + + public void setValue(double value) { + this.value = value; + } + + public long getTimestamp() { + return this.timestamp; + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/GaugeBuffers.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/GaugeBuffers.java new file mode 100644 index 0000000000..80cb065d91 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/GaugeBuffers.java @@ -0,0 +1,97 @@ +/* + * 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.buffer; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.UsesJava8; + +/** + * Fast writes to in-memory metrics store using {@link DoubleBuffer}. + * + * @author Dave Syer + */ +@UsesJava8 +public class GaugeBuffers { + + private final ConcurrentHashMap metrics = new ConcurrentHashMap(); + + public void forEach(final Predicate predicate, + final BiConsumer consumer) { + this.metrics.forEach(new BiConsumer() { + @Override + public void accept(String name, DoubleBuffer value) { + if (predicate.test(name)) { + consumer.accept(name, value); + } + } + }); + } + + public DoubleBuffer find(final String name) { + return this.metrics.get(name); + } + + public void get(final String name, final Consumer consumer) { + acceptInternal(name, new Consumer() { + @Override + public void accept(DoubleBuffer value) { + consumer.accept(value); + } + }); + } + + public void set(final String name, final double value) { + write(name, value); + } + + public int count() { + return this.metrics.size(); + } + + private void write(final String name, final double value) { + acceptInternal(name, new Consumer() { + @Override + public void accept(DoubleBuffer buffer) { + buffer.setTimestamp(System.currentTimeMillis()); + buffer.setValue(value); + } + }); + } + + public void reset(String name) { + this.metrics.remove(name, this.metrics.get(name)); + } + + private void acceptInternal(final String name, final Consumer consumer) { + DoubleBuffer value; + if (null == (value = this.metrics.get(name))) { + value = this.metrics.computeIfAbsent(name, + new Function() { + @Override + public DoubleBuffer apply(String tag) { + return new DoubleBuffer(0L); + } + }); + } + consumer.accept(value); + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/LongBuffer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/LongBuffer.java new file mode 100644 index 0000000000..08c2e97ef0 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/LongBuffer.java @@ -0,0 +1,59 @@ +/* + * 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.buffer; + +import java.util.concurrent.atomic.LongAdder; + +import org.springframework.lang.UsesJava8; + +/** + * Mutable buffer containing a long adder (Java 8) and a timestamp. + * + * @author Dave Syer + */ +@UsesJava8 +public class LongBuffer { + + private final LongAdder adder; + + private volatile long timestamp; + + public LongBuffer(long timestamp) { + this.adder = new LongAdder(); + this.timestamp = timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getValue() { + return this.adder.sum(); + } + + public long getTimestamp() { + return this.timestamp; + } + + public void reset() { + this.adder.reset(); + } + + public void add(long delta) { + this.adder.add(delta); + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/package-info.java new file mode 100644 index 0000000000..4f57bc24d0 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/buffer/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 buffering support. + */ +package org.springframework.boot.actuate.metrics.buffer; + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServices.java similarity index 53% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriter.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServices.java index e3045976ac..d9491c714e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServices.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -14,13 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics.writer; +package org.springframework.boot.actuate.metrics.dropwizard; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; -import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; @@ -30,77 +31,107 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; /** - * A {@link MetricWriter} that send data to a Codahale {@link MetricRegistry} based on a - * naming convention: + * A {@link GaugeService} and {@link CounterService} that sends data to a Dropwizard + * {@link MetricRegistry} based on a naming convention: * *

    - *
  • Updates to {@link #increment(Delta)} with names in "meter.*" are treated as + *
  • Updates to {@link #increment(String)} with names in "meter.*" are treated as * {@link Meter} events
  • *
  • Other deltas are treated as simple {@link Counter} values
  • - *
  • Inputs to {@link #set(Metric)} with names in "histogram.*" are treated as - * {@link Histogram} updates
  • - *
  • Inputs to {@link #set(Metric)} with names in "timer.*" are treated as {@link Timer} - * updates
  • + *
  • Inputs to {@link #submit(String, double)} with names in "histogram.*" are treated + * as {@link Histogram} updates
  • + *
  • Inputs to {@link #submit(String, double)} with names in "timer.*" are treated as + * {@link Timer} updates
  • *
  • Other metrics are treated as simple {@link Gauge} values (single valued * measurements of type double)
  • *
* * @author Dave Syer */ -public class DropwizardMetricWriter implements MetricWriter { +public class DropwizardMetricServices implements CounterService, GaugeService { private final MetricRegistry registry; private final ConcurrentMap gaugeLocks = new ConcurrentHashMap(); + private final ConcurrentHashMap names = new ConcurrentHashMap(); + /** - * Create a new {@link DropwizardMetricWriter} instance. + * Create a new {@link DropwizardMetricServices} instance. * @param registry the underlying metric registry */ - public DropwizardMetricWriter(MetricRegistry registry) { + public DropwizardMetricServices(MetricRegistry registry) { this.registry = registry; } @Override - public void increment(Delta delta) { - String name = delta.getName(); - long value = delta.getValue().longValue(); + public void increment(String name) { + incrementInternal(name, 1L); + } + + @Override + public void decrement(String name) { + incrementInternal(name, -1L); + } + + private void incrementInternal(String name, long value) { if (name.startsWith("meter")) { Meter meter = this.registry.meter(name); meter.mark(value); } else { + name = wrapCounterName(name); Counter counter = this.registry.counter(name); counter.inc(value); } } @Override - public void set(Metric value) { - String name = value.getName(); + public void submit(String name, double value) { if (name.startsWith("histogram")) { - long longValue = value.getValue().longValue(); + long longValue = (long) value; Histogram metric = this.registry.histogram(name); metric.update(longValue); } else if (name.startsWith("timer")) { - long longValue = value.getValue().longValue(); + long longValue = (long) value; Timer metric = this.registry.timer(name); metric.update(longValue, TimeUnit.MILLISECONDS); } else { - final double gauge = value.getValue().doubleValue(); + name = wrapGaugeName(name); + final double gauge = value; // Ensure we synchronize to avoid another thread pre-empting this thread after - // remove causing an error in CodaHale metrics - // NOTE: CodaHale provides no way to do this atomically - synchronized (getGuageLock(name)) { + // remove causing an error in Dropwizard metrics + // NOTE: Dropwizard provides no way to do this atomically + synchronized (getGaugeLock(name)) { this.registry.remove(name); this.registry.register(name, new SimpleGauge(gauge)); } } } - private Object getGuageLock(String name) { + private String wrapGaugeName(String metricName) { + return wrapName(metricName, "gauge."); + } + + private String wrapCounterName(String metricName) { + return wrapName(metricName, "counter."); + } + + private String wrapName(String metricName, String prefix) { + if (this.names.containsKey(metricName)) { + return this.names.get(metricName); + } + if (metricName.startsWith(prefix)) { + return metricName; + } + String name = prefix + metricName; + this.names.put(metricName, name); + return name; + } + + private Object getGaugeLock(String name) { Object lock = this.gaugeLocks.get(name); if (lock == null) { Object newLock = new Object(); @@ -111,8 +142,11 @@ public class DropwizardMetricWriter implements MetricWriter { } @Override - public void reset(String metricName) { - this.registry.remove(metricName); + public void reset(String name) { + if (!name.startsWith("meter")) { + name = wrapCounterName(name); + } + this.registry.remove(name); } /** diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/package-info.java new file mode 100644 index 0000000000..bab7b17850 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/dropwizard/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 Dropwizard Metrics. + */ +package org.springframework.boot.actuate.metrics.dropwizard; + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java index e030657b29..ebabd48425 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.metrics.Metric; import org.springframework.util.StringUtils; @@ -34,6 +36,8 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractMetricExporter implements Exporter { + private static final Log logger = LogFactory.getLog(AbstractMetricExporter.class); + private volatile AtomicBoolean processing = new AtomicBoolean(false); private Date earliestTimestamp = new Date(); @@ -42,6 +46,10 @@ public abstract class AbstractMetricExporter implements Exporter { private final String prefix; + private Date latestTimestamp = new Date(0L); + + private boolean sendLatest = true; + public AbstractMetricExporter(String prefix) { this.prefix = !StringUtils.hasText(prefix) ? "" : (prefix.endsWith(".") ? prefix : prefix + "."); @@ -63,13 +71,24 @@ public abstract class AbstractMetricExporter implements Exporter { this.ignoreTimestamps = ignoreTimestamps; } + /** + * Send only the data that changed since the last export. + * + * @param sendLatest the flag to set + */ + public void setSendLatest(boolean sendLatest) { + this.sendLatest = sendLatest; + } + @Override public void export() { if (!this.processing.compareAndSet(false, true)) { // skip a tick return; } + long latestTimestamp = 0; try { + latestTimestamp = System.currentTimeMillis(); for (String group : groups()) { Collection> values = new ArrayList>(); for (Metric metric : next(group)) { @@ -79,6 +98,10 @@ public abstract class AbstractMetricExporter implements Exporter { if (!this.ignoreTimestamps && this.earliestTimestamp.after(timestamp)) { continue; } + if (!this.ignoreTimestamps && this.sendLatest + && this.latestTimestamp.after(timestamp)) { + continue; + } values.add(value); } if (!values.isEmpty()) { @@ -86,11 +109,26 @@ public abstract class AbstractMetricExporter implements Exporter { } } } + catch (Exception e) { + logger.warn("Could not write to MetricWriter: " + e.getClass() + ": " + + e.getMessage()); + } finally { + try { + this.latestTimestamp = new Date(latestTimestamp); + flush(); + } + catch (Exception e) { + logger.warn("Could not flush MetricWriter: " + e.getClass() + ": " + + e.getMessage()); + } this.processing.set(false); } } + public void flush() { + } + /** * Generate a group of metrics to iterate over in the form of a set of Strings (e.g. * prefixes). If the metrics to be exported partition into groups identified by a diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java index cd4f7b9fa2..d0c92a8b49 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java @@ -20,7 +20,7 @@ package org.springframework.boot.actuate.metrics.export; * Generic interface for metric exports. As you scale up metric collection you will often * need to buffer metric data locally and export it periodically (e.g. for aggregation * across a cluster), so this is the marker interface for those operations. The trigger of - * an export operation might be periodic or even driven, but it remains outside the scope + * an export operation might be periodic or event driven, but it remains outside the scope * of this interface. You might for instance create an instance of an Exporter and trigger * it using a {@code @Scheduled} annotation in a Spring ApplicationContext. * diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java index 9d5fb729ff..69dbbe706d 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * 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. @@ -17,10 +17,13 @@ package org.springframework.boot.actuate.metrics.export; import java.util.Collection; +import java.util.Iterator; import org.springframework.boot.actuate.metrics.Metric; import org.springframework.boot.actuate.metrics.reader.MetricReader; import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.boot.actuate.metrics.writer.WriterUtils; +import org.springframework.util.PatternMatchUtils; /** * {@link Exporter} that "exports" by copying metric data from a source @@ -34,10 +37,26 @@ public class MetricCopyExporter extends AbstractMetricExporter { private final MetricWriter writer; + private String[] includes = new String[0]; + + private String[] excludes = new String[0]; + public MetricCopyExporter(MetricReader reader, MetricWriter writer) { this(reader, writer, ""); } + public void setIncludes(String... includes) { + if (includes != null) { + this.includes = includes; + } + } + + public void setExcludes(String... excludes) { + if (excludes != null) { + this.excludes = excludes; + } + } + public MetricCopyExporter(MetricReader reader, MetricWriter writer, String prefix) { super(prefix); this.reader = reader; @@ -46,7 +65,17 @@ public class MetricCopyExporter extends AbstractMetricExporter { @Override protected Iterable> next(String group) { - return this.reader.findAll(); + if ((this.includes == null || this.includes.length == 0) + && (this.excludes == null || this.excludes.length == 0)) { + return this.reader.findAll(); + } + return new Iterable>() { + @Override + public Iterator> iterator() { + return new PatternMatchingIterator(MetricCopyExporter.this.reader + .findAll().iterator()); + } + }; } @Override @@ -56,4 +85,64 @@ public class MetricCopyExporter extends AbstractMetricExporter { } } + @Override + public void flush() { + WriterUtils.flush(this.writer); + } + + private class PatternMatchingIterator implements Iterator> { + + private Metric buffer = null; + private Iterator> iterator; + + public PatternMatchingIterator(Iterator> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + if (this.buffer != null) { + return true; + } + this.buffer = findNext(); + return this.buffer != null; + } + + private Metric findNext() { + Metric metric = null; + boolean matched = false; + while (this.iterator.hasNext() && !matched) { + metric = this.iterator.next(); + if (MetricCopyExporter.this.includes == null + || MetricCopyExporter.this.includes.length == 0) { + matched = true; + } + else { + for (String pattern : MetricCopyExporter.this.includes) { + if (PatternMatchUtils.simpleMatch(pattern, metric.getName())) { + matched = true; + break; + } + } + } + if (MetricCopyExporter.this.excludes != null) { + for (String pattern : MetricCopyExporter.this.excludes) { + if (PatternMatchUtils.simpleMatch(pattern, metric.getName())) { + matched = false; + break; + } + } + } + } + return matched ? metric : null; + + } + + @Override + public Metric next() { + Metric metric = this.buffer; + this.buffer = null; + return metric; + } + }; } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java new file mode 100644 index 0000000000..bd399c9cdf --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java @@ -0,0 +1,215 @@ +/* + * 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.export; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.annotation.PostConstruct; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.PatternMatchUtils; + +/** + * @author Dave Syer + */ +@ConfigurationProperties("spring.metrics.export") +public class MetricExportProperties { + + /** + * Flag to disable all metric exports (assuming any MetricWriters are available). + */ + private boolean enabled = true; + + private Export export = new Export(); + + private Map writers = new LinkedHashMap(); + + /** + * Default values for trigger configuration for all writers. Can also be set by + * including a writer with name="*". + * + * @return the default trigger configuration + */ + public Export getDefault() { + return this.export; + } + + /** + * Configuration for triggers on individual named writers. Each value can individually + * specify a name pattern explicitly, or else the map key will be used if the name is + * not set. + * + * @return the writers + */ + public Map getWriters() { + return this.writers; + } + + @PostConstruct + public void setUpDefaults() { + Export defaults = null; + for (Entry entry : this.writers.entrySet()) { + String key = entry.getKey(); + Export value = entry.getValue(); + if (value.getNames() == null || value.getNames().length == 0) { + value.setNames(new String[] { key }); + } + if (Arrays.asList(value.getNames()).contains("*")) { + defaults = value; + } + } + if (defaults == null) { + this.export.setNames(new String[] { "*" }); + this.writers.put("*", this.export); + defaults = this.export; + } + if (defaults.isIgnoreTimestamps() == null) { + defaults.setIgnoreTimestamps(false); + } + if (defaults.isSendLatest() == null) { + defaults.setSendLatest(true); + } + if (defaults.getDelayMillis() == null) { + defaults.setDelayMillis(5000); + } + for (Export value : this.writers.values()) { + if (value.isIgnoreTimestamps() == null) { + value.setIgnoreTimestamps(defaults.isIgnoreTimestamps()); + } + if (value.isSendLatest() == null) { + value.setSendLatest(defaults.isSendLatest()); + } + if (value.getDelayMillis() == null) { + value.setDelayMillis(defaults.getDelayMillis()); + } + } + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public static class Export { + /** + * Names (or patterns) for bean names that this configuration applies to. + */ + private String[] names; + /** + * Delay in milliseconds between export ticks. Metrics are exported to external + * sources on a schedule with this delay. + */ + private Long delayMillis; + + /** + * Flag to enable metric export (assuming a MetricWriter is available). + */ + private boolean enabled = true; + + /** + * Flag to switch off any available optimizations based on not exporting unchanged + * metric values. + */ + private Boolean sendLatest; + + /** + * Flag to ignore timestamps completely. If true, send all metrics all the time, + * including ones that haven't changed since startup. + */ + private Boolean ignoreTimestamps; + + private String[] includes; + + private String[] excludes; + + public String[] getNames() { + return this.names; + } + + public void setNames(String[] names) { + this.names = names; + } + + public String[] getIncludes() { + return this.includes; + } + + public void setIncludes(String[] includes) { + this.includes = includes; + } + + public void setExcludes(String[] excludes) { + this.excludes = excludes; + } + + public String[] getExcludes() { + return this.excludes; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Long getDelayMillis() { + return this.delayMillis; + } + + public void setDelayMillis(long delayMillis) { + this.delayMillis = delayMillis; + } + + public Boolean isIgnoreTimestamps() { + return this.ignoreTimestamps; + } + + public void setIgnoreTimestamps(boolean ignoreTimestamps) { + this.ignoreTimestamps = ignoreTimestamps; + } + + public Boolean isSendLatest() { + return this.sendLatest; + } + + public void setSendLatest(boolean sendLatest) { + this.sendLatest = sendLatest; + } + } + + /** + * Find a matching trigger configuration. + * @param name the bean name to match + * @return a matching configuration if there is one + */ + public Export findExport(String name) { + for (Export value : this.writers.values()) { + if (PatternMatchUtils.simpleMatch(value.getNames(), name)) { + return value; + } + } + return this.export; + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExporters.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExporters.java new file mode 100644 index 0000000000..bd2b36928e --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExporters.java @@ -0,0 +1,88 @@ +/* + * 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.export; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.boot.actuate.metrics.export.MetricExportProperties.Export; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * @author Dave Syer + */ +public class MetricExporters implements SchedulingConfigurer { + + private MetricExportProperties export; + + private Map writers; + + private Map exporters = new HashMap(); + + private MetricReader reader; + + public MetricExporters(MetricReader reader, Map writers, + MetricExportProperties export) { + this.reader = reader; + this.export = export; + this.writers = writers; + } + + public Map getExporters() { + return this.exporters; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + + for (Entry entry : this.writers.entrySet()) { + String name = entry.getKey(); + Export trigger = this.export.findExport(name); + + if (trigger != null) { + + MetricWriter writer = entry.getValue(); + final MetricCopyExporter exporter = new MetricCopyExporter(this.reader, + writer); + if (trigger.getIncludes() != null || trigger.getExcludes() != null) { + exporter.setIncludes(trigger.getIncludes()); + exporter.setExcludes(trigger.getExcludes()); + } + exporter.setIgnoreTimestamps(trigger.isIgnoreTimestamps()); + exporter.setSendLatest(trigger.isSendLatest()); + + this.exporters.put(name, exporter); + + taskRegistrar.addFixedDelayTask(new IntervalTask(new Runnable() { + @Override + public void run() { + exporter.export(); + } + }, trigger.getDelayMillis(), trigger.getDelayMillis())); + + } + + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java new file mode 100644 index 0000000000..b5d3f31228 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategy.java @@ -0,0 +1,64 @@ +/* + * 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.jmx; + +import java.util.Hashtable; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.springframework.jmx.export.naming.KeyNamingStrategy; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; +import org.springframework.util.StringUtils; + +/** + * MBean naming strategy for metric keys. A metric name of + * counter.foo.bar.spam translates to an object name with + * type=counter, name=foo and value=bar.spam. This + * results in a more or less pleasing view with no tweaks in jconsole or jvisualvm. The + * domain is copied from the input key and the type in the input key is discarded. + * + * @author Dave Syer + */ +public class DefaultMetricNamingStrategy implements ObjectNamingStrategy { + + private ObjectNamingStrategy namingStrategy = new KeyNamingStrategy(); + + @Override + public ObjectName getObjectName(Object managedBean, String beanKey) + throws MalformedObjectNameException { + ObjectName objectName = this.namingStrategy.getObjectName(managedBean, beanKey); + String domain = objectName.getDomain(); + Hashtable table = new Hashtable( + objectName.getKeyPropertyList()); + String name = objectName.getKeyProperty("name"); + if (name != null) { + table.remove("name"); + String[] parts = StringUtils.delimitedListToStringArray(name, "."); + table.put("type", parts[0]); + if (parts.length > 1) { + table.put(parts.length > 2 ? "name" : "value", parts[1]); + } + if (parts.length > 2) { + table.put("value", + name.substring(parts[0].length() + parts[1].length() + 2)); + } + } + return new ObjectName(domain, table); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java new file mode 100644 index 0000000000..db345b7833 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/JmxMetricWriter.java @@ -0,0 +1,159 @@ +/* + * 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.jmx; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.management.MalformedObjectNameException; + +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.jmx.export.MBeanExporter; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.jmx.export.naming.ObjectNamingStrategy; + +/** + * A {@link MetricWriter} for MBeans. Each metric is registered as an individual MBean, so + * (for instance) it can be graphed and monitored. The object names are provided by an + * {@link ObjectNamingStrategy}, where the default is a + * {@link DefaultMetricNamingStrategy} which provides type, name + * and value keys by splitting up the metric name on periods. + * + * @author Dave Syer + */ +@ManagedResource(description = "MetricWriter for pushing metrics to JMX MBeans.") +public class JmxMetricWriter implements MetricWriter { + + private static Log logger = LogFactory.getLog(JmxMetricWriter.class); + + private final ConcurrentMap values = new ConcurrentHashMap(); + + private final MBeanExporter exporter; + + private ObjectNamingStrategy namingStrategy = new DefaultMetricNamingStrategy(); + + private String domain = "org.springframework.metrics"; + + public JmxMetricWriter(MBeanExporter exporter) { + this.exporter = exporter; + } + + public void setNamingStrategy(ObjectNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + @ManagedOperation + public void increment(String name, long value) { + increment(new Delta(name, value)); + } + + @Override + public void increment(Delta delta) { + MetricValue counter = getValue(delta.getName()); + counter.increment(delta.getValue().longValue()); + } + + @ManagedOperation + public void set(String name, double value) { + set(new Metric(name, value)); + } + + @Override + public void set(Metric value) { + MetricValue metric = getValue(value.getName()); + metric.setValue(value.getValue().doubleValue()); + } + + @Override + @ManagedOperation + public void reset(String name) { + MetricValue value = this.values.remove(name); + if (value != null) { + try { + // We can unregister the MBean, but if this writer is on the end of an + // Exporter the chances are it will be re-registered almost immediately. + this.exporter.unregisterManagedResource(this.namingStrategy + .getObjectName(value, getKey(name))); + } + catch (MalformedObjectNameException e) { + logger.warn("Could not unregister MBean for " + name); + } + } + } + + private MetricValue getValue(String name) { + if (!this.values.containsKey(name)) { + this.values.putIfAbsent(name, new MetricValue()); + MetricValue value = this.values.get(name); + try { + this.exporter.registerManagedResource(value, + this.namingStrategy.getObjectName(value, getKey(name))); + } + catch (Exception e) { + // Could not register mbean, maybe just a race condition + } + } + return this.values.get(name); + } + + private String getKey(String name) { + return String.format(this.domain + ":type=MetricValue,name=%s", name); + } + + @ManagedResource + public static class MetricValue { + + private double value; + + private long lastUpdated = 0; + + public void setValue(double value) { + if (this.value != value) { + this.lastUpdated = System.currentTimeMillis(); + } + this.value = value; + } + + public void increment(long value) { + this.lastUpdated = System.currentTimeMillis(); + this.value += value; + } + + @ManagedAttribute + public double getValue() { + return this.value; + } + + @ManagedAttribute + public Date getLastUpdated() { + return new Date(this.lastUpdated); + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/package-info.java new file mode 100644 index 0000000000..9550f9a4c8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jmx/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 JMX. + */ +package org.springframework.boot.actuate.metrics.jmx; + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/DefaultOpenTsdbNamingStrategy.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/DefaultOpenTsdbNamingStrategy.java new file mode 100644 index 0000000000..50ec9d0831 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/DefaultOpenTsdbNamingStrategy.java @@ -0,0 +1,72 @@ +/* + * 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.opentsdb; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.ObjectUtils; + +/** + * A naming strategy that just passes through the metric name, together with tags from a + * set of static values. Open TSDB requires at least one tag, so tags are always added for + * you: the {@value #DOMAIN_KEY} key is added with a value "spring", and the + * {@value #PROCESS_KEY} key is added with a value equal to the object hash of "this" (the + * naming strategy). The "domain" value is a system identifier - it would be common to all + * processes in the same distributed system. In most cases this will be unique enough to + * allow aggregation of the underlying metrics in Open TSDB, but normally it is best to + * provide your own tags, including a prefix and process identifier if you know one + * (overwriting the default). + * + * @author Dave Syer + */ +public class DefaultOpenTsdbNamingStrategy implements OpenTsdbNamingStrategy { + + public static final String DOMAIN_KEY = "domain"; + + public static final String PROCESS_KEY = "process"; + + /** + * Tags to apply to every metric. Open TSDB requires at least one tag, so a "prefix" + * tag is added for you by default. + */ + private Map tags = new LinkedHashMap(); + + private Map cache = new HashMap(); + + public DefaultOpenTsdbNamingStrategy() { + this.tags.put(DOMAIN_KEY, "org.springframework.metrics"); + this.tags.put(PROCESS_KEY, ObjectUtils.getIdentityHexString(this)); + } + + public void setTags(Map staticTags) { + this.tags.putAll(staticTags); + } + + @Override + public OpenTsdbName getName(String name) { + if (this.cache.containsKey(name)) { + return this.cache.get(name); + } + OpenTsdbName value = new OpenTsdbName(name); + value.setTags(this.tags); + this.cache.put(name, value); + return value; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbData.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbData.java new file mode 100644 index 0000000000..8e660f8300 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbData.java @@ -0,0 +1,82 @@ +/* + * 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.opentsdb; + +import java.util.Map; + +/** + * @author Dave Syer + */ +public class OpenTsdbData { + + private OpenTsdbName name; + + private Long timestamp; + + private Number value; + + protected OpenTsdbData() { + this.name = new OpenTsdbName(); + } + + public OpenTsdbData(String metric, Number value) { + this(metric, value, System.currentTimeMillis()); + } + + public OpenTsdbData(String metric, Number value, Long timestamp) { + this(new OpenTsdbName(metric), value, timestamp); + } + + public OpenTsdbData(OpenTsdbName name, Number value, Long timestamp) { + this.name = name; + this.value = value; + this.timestamp = timestamp; + } + + public String getMetric() { + return this.name.getMetric(); + } + + public void setMetric(String metric) { + this.name.setMetric(metric); + } + + public Long getTimestamp() { + return this.timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Number getValue() { + return this.value; + } + + public void setValue(Number value) { + this.value = value; + } + + public Map getTags() { + return this.name.getTags(); + } + + public void setTags(Map tags) { + this.name.setTags(tags); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriter.java new file mode 100644 index 0000000000..13ae1ad39f --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriter.java @@ -0,0 +1,144 @@ +/* + * 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.opentsdb; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +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.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +/** + * A {@link MetricWriter} for the Open TSDB database (version 2.0), writing metrics to the + * HTTP endpoint provided by the server. Data are buffered according to the + * {@link #setBufferSize(int) bufferSize} property, and only flushed automatically when + * the buffer size is reached. Users should either manually {@link #flush()} after writing + * a batch of data if that makes sense, or consider adding a {@link Scheduled + * @Scheduled} task to flush periodically. + * + * @author Dave Syer + */ +public class OpenTsdbMetricWriter implements MetricWriter { + + private static final Log logger = LogFactory.getLog(OpenTsdbMetricWriter.class); + + private RestOperations restTemplate = new RestTemplate(); + + /** + * URL for POSTing data. Defaults to http://localhost:4242/api/put. + */ + private String url = "http://localhost:4242/api/put"; + + /** + * Buffer size to fill before posting data to server. + */ + private int bufferSize = 64; + + /** + * The media type to use to serialize and accept responses from the server. Defaults + * to "application/json". + */ + private MediaType mediaType = MediaType.APPLICATION_JSON; + + private List buffer = new ArrayList(this.bufferSize); + + private OpenTsdbNamingStrategy namingStrategy = new DefaultOpenTsdbNamingStrategy(); + + public RestOperations getRestTemplate() { + return this.restTemplate; + } + + public void setRestTemplate(RestOperations restTemplate) { + this.restTemplate = restTemplate; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + public void setMediaType(MediaType mediaType) { + this.mediaType = mediaType; + } + + public void setNamingStrategy(OpenTsdbNamingStrategy namingStrategy) { + this.namingStrategy = namingStrategy; + } + + @Override + public void increment(Delta delta) { + throw new UnsupportedOperationException("Counters not supported via increment"); + } + + @Override + public void set(Metric value) { + OpenTsdbData data = new OpenTsdbData( + this.namingStrategy.getName(value.getName()), value.getValue(), value + .getTimestamp().getTime()); + this.buffer.add(data); + if (this.buffer.size() >= this.bufferSize) { + flush(); + } + } + + /** + * Flush the buffer without waiting for it to fill any further. + */ + public void flush() { + if (this.buffer.isEmpty()) { + return; + } + List temp = new ArrayList(); + synchronized (this.buffer) { + temp.addAll(this.buffer); + this.buffer.clear(); + } + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(this.mediaType)); + headers.setContentType(this.mediaType); + HttpEntity> request = new HttpEntity>(temp, + headers); + @SuppressWarnings("rawtypes") + ResponseEntity response = this.restTemplate.postForEntity(this.url, request, + Map.class); + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("Cannot write metrics (discarded " + temp.size() + " values): " + + response.getBody()); + } + } + + @Override + public void reset(String metricName) { + set(new Metric(metricName, 0L)); + } + +} \ No newline at end of file diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbName.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbName.java new file mode 100644 index 0000000000..228c58651b --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbName.java @@ -0,0 +1,58 @@ +/* + * 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.opentsdb; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author Dave Syer + */ +public class OpenTsdbName { + + private String metric; + + private Map tags = new LinkedHashMap(); + + protected OpenTsdbName() { + } + + public OpenTsdbName(String metric) { + this.metric = metric; + } + + public String getMetric() { + return this.metric; + } + + public void setMetric(String metric) { + this.metric = metric; + } + + public Map getTags() { + return this.tags; + } + + public void setTags(Map tags) { + this.tags.putAll(tags); + } + + public void tag(String name, String value) { + this.tags.put(name, value); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbNamingStrategy.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbNamingStrategy.java new file mode 100644 index 0000000000..c3bebf7eb4 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbNamingStrategy.java @@ -0,0 +1,26 @@ +/* + * 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.opentsdb; + +/** + * @author Dave Syer + */ +public interface OpenTsdbNamingStrategy { + + OpenTsdbName getName(String metricName); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/package-info.java new file mode 100644 index 0000000000..defed376b0 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/opentsdb/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 OpenTSDB. + */ +package org.springframework.boot.actuate.metrics.opentsdb; + diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java index fb9dca99df..5155798778 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java @@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A {@link MetricRepository} implementation for a redis backend. Metric values are stored @@ -92,6 +93,8 @@ public class RedisMetricRepository implements MetricRepository { public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory, String prefix, String key) { Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null"); + Assert.state(StringUtils.hasText(prefix), "Prefix must be non-empty"); + Assert.state(StringUtils.hasText(key), "Key must be non-empty"); this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory); if (!prefix.endsWith(".")) { prefix = prefix + "."; 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 0000000000..2724ca9e42 --- /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 0000000000..237c679452 --- /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/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java index e256884f4e..117bbf0120 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java @@ -48,13 +48,8 @@ public class SimpleInMemoryRepository { synchronized (lock) { T current = this.values.get(name); T value = callback.modify(current); - if (current != null) { - this.values.replace(name, current, value); - } - else { - this.values.putIfAbsent(name, value); - } - return this.values.get(name); + this.values.put(name, value); + return value; } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriter.java deleted file mode 100644 index f93c29d4f3..0000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.writer; - -import org.springframework.boot.actuate.metrics.Metric; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Gauge; -import com.codahale.metrics.Histogram; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; - -/** - * A {@link MetricWriter} that send data to a Codahale {@link MetricRegistry} based on a - * naming convention: - * - *
    - *
  • Updates to {@link #increment(Delta)} with names in "meter.*" are treated as - * {@link Meter} events
  • - *
  • Other deltas are treated as simple {@link Counter} values
  • - *
  • Inputs to {@link #set(Metric)} with names in "histogram.*" are treated as - * {@link Histogram} updates
  • - *
  • Inputs to {@link #set(Metric)} with names in "timer.*" are treated as {@link Timer} - * updates
  • - *
  • Other metrics are treated as simple {@link Gauge} values (single valued - * measurements of type double)
  • - *
- * - * @author Dave Syer - * @deprecated since 1.2.2 in favor of {@link DropwizardMetricWriter} - */ -@Deprecated -public class CodahaleMetricWriter extends DropwizardMetricWriter { - - /** - * Create a new {@link DropwizardMetricWriter} instance. - * @param registry the underlying metric registry - */ - public CodahaleMetricWriter(MetricRegistry registry) { - super(registry); - } - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java index a34b02a3f0..b84115fd2e 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.metrics.writer; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import org.springframework.boot.actuate.metrics.Metric; @@ -28,7 +29,7 @@ import org.springframework.boot.actuate.metrics.Metric; * * @author Dave Syer */ -public class CompositeMetricWriter implements MetricWriter { +public class CompositeMetricWriter implements MetricWriter, Iterable { private final List writers = new ArrayList(); @@ -40,6 +41,11 @@ public class CompositeMetricWriter implements MetricWriter { this.writers.addAll(writers); } + @Override + public Iterator iterator() { + return this.writers.iterator(); + } + @Override public void increment(Delta delta) { for (MetricWriter writer : this.writers) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java index f0ad86fd4b..28fe6ac479 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.metrics.writer; +import java.util.concurrent.ConcurrentHashMap; + import org.springframework.boot.actuate.metrics.CounterService; /** @@ -27,6 +29,8 @@ public class DefaultCounterService implements CounterService { private final MetricWriter writer; + private final ConcurrentHashMap names = new ConcurrentHashMap(); + /** * Create a {@link DefaultCounterService} instance. * @param writer the underlying writer used to manage metrics @@ -51,10 +55,14 @@ public class DefaultCounterService implements CounterService { } private String wrap(String metricName) { + if (this.names.containsKey(metricName)) { + return this.names.get(metricName); + } if (metricName.startsWith("counter") || metricName.startsWith("meter")) { return metricName; } - return "counter." + metricName; + String name = "counter." + metricName; + this.names.put(metricName, name); + return name; } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java index 8ab448dc91..f11b6d0f23 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.metrics.writer; +import java.util.concurrent.ConcurrentHashMap; + import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.boot.actuate.metrics.Metric; @@ -28,6 +30,8 @@ public class DefaultGaugeService implements GaugeService { private final MetricWriter writer; + private final ConcurrentHashMap names = new ConcurrentHashMap(); + /** * Create a {@link DefaultGaugeService} instance. * @param writer the underlying writer used to manage metrics @@ -42,11 +46,15 @@ public class DefaultGaugeService implements GaugeService { } private String wrap(String metricName) { + if (this.names.containsKey(metricName)) { + return this.names.get(metricName); + } if (metricName.startsWith("gauge") || metricName.startsWith("histogram") || metricName.startsWith("timer")) { return metricName; } - return "gauge." + metricName; + String name = "gauge." + metricName; + this.names.put(metricName, name); + return name; } - } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/WriterUtils.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/WriterUtils.java new file mode 100644 index 0000000000..8ae662387c --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/WriterUtils.java @@ -0,0 +1,59 @@ +/* + * 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.writer; + +import java.io.Flushable; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.actuate.metrics.export.MetricCopyExporter; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * @author Dave Syer + */ +public class WriterUtils { + + private static final Log logger = LogFactory.getLog(MetricCopyExporter.class); + + public static void flush(MetricWriter writer) { + if (writer instanceof CompositeMetricWriter) { + for (MetricWriter element : (CompositeMetricWriter) writer) { + flush(element); + } + } + try { + if (ClassUtils.isPresent("java.io.Flushable", null)) { + if (writer instanceof Flushable) { + ((Flushable) writer).flush(); + return; + } + } + Method method = ReflectionUtils.findMethod(writer.getClass(), "flush"); + if (method != null) { + ReflectionUtils.invokeMethod(method, writer); + } + } + catch (Exception e) { + logger.warn("Could not flush MetricWriter: " + e.getClass() + ": " + + e.getMessage()); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/package-info.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/package-info.java index 714677991c..0f34887c2a 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/package-info.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/package-info.java @@ -15,7 +15,7 @@ */ /** - * Metrics integration with Dropwizard Metrics. + * Support for writing metrics */ package org.springframework.boot.actuate.metrics.writer; diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index 01f62773c1..fea96720c6 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -11,6 +11,9 @@ org.springframework.boot.actuate.autoconfigure.ManagementSecurityAutoConfigurati org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.MetricsDropwizardAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.MetricsChannelAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/AdhocTestSuite.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/AdhocTestSuite.java new file mode 100644 index 0000000000..ce93990c99 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/AdhocTestSuite.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2013 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; + +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfigurationTests; +import org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfigurationTests; + +/** + * A test suite for probing weird ordering problems in the tests. + * + * @author Dave Syer + */ +@RunWith(Suite.class) +@SuiteClasses({ PublicMetricsAutoConfigurationTests.class, + MetricRepositoryAutoConfigurationTests.class }) +@Ignore +public class AdhocTestSuite { + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfigurationTests.java index 78fbf1f4b8..12a1d47aff 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricRepositoryAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * 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. @@ -16,22 +16,31 @@ package org.springframework.boot.actuate.autoconfigure; -import java.util.concurrent.Executor; - +import org.junit.After; import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mockito; import org.springframework.boot.actuate.metrics.CounterService; import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.buffer.BufferCounterService; +import org.springframework.boot.actuate.metrics.buffer.BufferGaugeService; +import org.springframework.boot.actuate.metrics.dropwizard.DropwizardMetricServices; +import org.springframework.boot.actuate.metrics.export.MetricCopyExporter; +import org.springframework.boot.actuate.metrics.export.MetricExporters; import org.springframework.boot.actuate.metrics.reader.MetricReader; -import org.springframework.boot.actuate.metrics.writer.DefaultCounterService; -import org.springframework.boot.actuate.metrics.writer.DefaultGaugeService; +import org.springframework.boot.actuate.metrics.reader.PrefixMetricReader; import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SyncTaskExecutor; -import org.springframework.messaging.support.ExecutorSubscribableChannel; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.integration.channel.FixedSubscriberChannel; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.SubscribableChannel; import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; @@ -40,10 +49,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link MetricRepositoryAutoConfiguration}. @@ -53,76 +59,95 @@ import static org.mockito.Mockito.verify; */ public class MetricRepositoryAutoConfigurationTests { - @Test - public void defaultExecutor() throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - MetricRepositoryAutoConfiguration.class); - ExecutorSubscribableChannel channel = context - .getBean(ExecutorSubscribableChannel.class); - ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) channel.getExecutor(); - context.close(); - assertTrue(executor.getThreadPoolExecutor().isShutdown()); + private AnnotationConfigApplicationContext context; + + @After + public void after() { + if (this.context != null) { + this.context.close(); + } } @Test public void createServices() throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - SyncTaskExecutorConfiguration.class, + this.context = new AnnotationConfigApplicationContext( MetricRepositoryAutoConfiguration.class); - DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + GaugeService gaugeService = this.context.getBean(BufferGaugeService.class); assertNotNull(gaugeService); - assertNotNull(context.getBean(DefaultCounterService.class)); + assertNotNull(this.context.getBean(BufferCounterService.class)); + assertNotNull(this.context.getBean(PrefixMetricReader.class)); gaugeService.submit("foo", 2.7); - assertEquals(2.7, context.getBean(MetricReader.class).findOne("gauge.foo") + assertEquals(2.7, this.context.getBean(MetricReader.class).findOne("gauge.foo") .getValue()); - context.close(); + } + + @Test + public void defaultExporterWhenMessageChannelAvailable() throws Exception { + this.context = new AnnotationConfigApplicationContext( + MessageChannelConfiguration.class, + MetricRepositoryAutoConfiguration.class, + MetricsChannelAutoConfiguration.class, + MetricExportAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + MetricExporters exporter = this.context.getBean(MetricExporters.class); + assertNotNull(exporter); } @Test public void provideAdditionalWriter() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - SyncTaskExecutorConfiguration.class, WriterConfig.class, - MetricRepositoryAutoConfiguration.class); - DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + this.context = new AnnotationConfigApplicationContext(WriterConfig.class, + MetricRepositoryAutoConfiguration.class, + MetricExportAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class); + GaugeService gaugeService = this.context.getBean(GaugeService.class); assertNotNull(gaugeService); gaugeService.submit("foo", 2.7); - MetricWriter writer = context.getBean("writer", MetricWriter.class); - verify(writer).set(any(Metric.class)); - context.close(); + MetricExporters exporters = this.context.getBean(MetricExporters.class); + MetricCopyExporter exporter = (MetricCopyExporter) exporters.getExporters().get( + "writer"); + exporter.setIgnoreTimestamps(true); + exporter.export(); + MetricWriter writer = this.context.getBean("writer", MetricWriter.class); + Mockito.verify(writer, Mockito.atLeastOnce()).set(Matchers.any(Metric.class)); } @Test - public void codahaleInstalledIfPresent() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - SyncTaskExecutorConfiguration.class, WriterConfig.class, - MetricRepositoryAutoConfiguration.class); - DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + public void dropwizardInstalledIfPresent() { + this.context = new AnnotationConfigApplicationContext( + MetricsDropwizardAutoConfiguration.class, + MetricRepositoryAutoConfiguration.class, AopAutoConfiguration.class); + GaugeService gaugeService = this.context.getBean(GaugeService.class); assertNotNull(gaugeService); gaugeService.submit("foo", 2.7); - MetricRegistry registry = context.getBean(MetricRegistry.class); + DropwizardMetricServices exporter = this.context + .getBean(DropwizardMetricServices.class); + assertEquals(gaugeService, exporter); + MetricRegistry registry = this.context.getBean(MetricRegistry.class); @SuppressWarnings("unchecked") Gauge gauge = (Gauge) registry.getMetrics().get("gauge.foo"); assertEquals(new Double(2.7), gauge.getValue()); - context.close(); } @Test public void skipsIfBeansExist() throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - Config.class, MetricRepositoryAutoConfiguration.class); - assertThat(context.getBeansOfType(DefaultGaugeService.class).size(), equalTo(0)); - assertThat(context.getBeansOfType(DefaultCounterService.class).size(), equalTo(0)); - context.close(); + this.context = new AnnotationConfigApplicationContext(Config.class, + MetricRepositoryAutoConfiguration.class); + assertThat(this.context.getBeansOfType(BufferGaugeService.class).size(), + equalTo(0)); + assertThat(this.context.getBeansOfType(BufferCounterService.class).size(), + equalTo(0)); } @Configuration - public static class SyncTaskExecutorConfiguration { - + public static class MessageChannelConfiguration { @Bean - public Executor metricsExecutor() { - return new SyncTaskExecutor(); + public SubscribableChannel metricsChannel() { + return new FixedSubscriberChannel(new MessageHandler() { + @Override + public void handleMessage(Message message) throws MessagingException { + } + }); } - } @Configuration @@ -130,7 +155,7 @@ public class MetricRepositoryAutoConfigurationTests { @Bean public MetricWriter writer() { - return mock(MetricWriter.class); + return Mockito.mock(MetricWriter.class); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java new file mode 100644 index 0000000000..5e3ee336aa --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java @@ -0,0 +1,87 @@ +/* + * 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.aggregate; + +import java.util.Date; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.boot.actuate.metrics.writer.Delta; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Dave Syer + */ +public class AggregateMetricReaderTests { + + private InMemoryMetricRepository source = new InMemoryMetricRepository(); + + private AggregateMetricReader reader = new AggregateMetricReader(this.source); + + @Test + public void writeAndReadDefaults() { + this.source.set(new Metric("foo.bar.spam", 2.3)); + assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue()); + } + + @Test + public void writeAndReadLatestValue() { + this.source.set(new Metric("foo.bar.spam", 2.3, new Date(100L))); + this.source.set(new Metric("oof.rab.spam", 2.4, new Date(0L))); + assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue()); + } + + @Test + public void writeAndReadExtraLong() { + this.source.set(new Metric("blee.foo.bar.spam", 2.3)); + this.reader.setTruncateKeyLength(3); + assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue()); + } + + @Test + public void onlyPrefixed() { + this.source.set(new Metric("foo.bar.spam", 2.3)); + assertNull(this.reader.findOne("spam")); + } + + @Test + public void incrementCounter() { + this.source.increment(new Delta("foo.bar.counter.spam", 2L)); + this.source.increment(new Delta("oof.rab.counter.spam", 3L)); + assertEquals(5L, this.reader.findOne("aggregate.counter.spam").getValue()); + } + + @Test + public void countGauges() { + this.source.set(new Metric("foo.bar.spam", 2.3)); + this.source.set(new Metric("oof.rab.spam", 2.4)); + assertEquals(1, this.reader.count()); + } + + @Test + public void countGaugesAndCounters() { + this.source.set(new Metric("foo.bar.spam", 2.3)); + this.source.set(new Metric("oof.rab.spam", 2.4)); + this.source.increment(new Delta("foo.bar.counter.spam", 2L)); + this.source.increment(new Delta("oof.rab.counter.spam", 3L)); + assertEquals(2, this.reader.count()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeServiceSpeedTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeServiceSpeedTests.java new file mode 100644 index 0000000000..3af6ef06b5 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferGaugeServiceSpeedTests.java @@ -0,0 +1,167 @@ +/* + * 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.buffer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.DoubleAdder; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.lang.UsesJava8; +import org.springframework.util.StopWatch; + +import static org.junit.Assert.assertTrue; + +/** + * Speed tests for {@link BufferGaugeService}. + * + * @author Dave Syer + */ +@RunWith(Theories.class) +@UsesJava8 +public class BufferGaugeServiceSpeedTests { + + @DataPoints + public static String[] values = new String[10]; + + public static String[] names = new String[] { "foo", "bar", "spam", "bucket" }; + + public static String[] sample = new String[1000]; + + private GaugeBuffers gauges = new GaugeBuffers(); + + private GaugeService service = new BufferGaugeService(this.gauges); + + private BufferMetricReader reader = new BufferMetricReader(new CounterBuffers(), + this.gauges); + + private static int threadCount = 2; + + private static final int number = Boolean.getBoolean("performance.test") ? 10000000 + : 100000; + + private static StopWatch watch = new StopWatch("count"); + + private static int count; + + private static PrintWriter err; + + @BeforeClass + public static void prime() throws FileNotFoundException { + err = new PrintWriter("/dev/null"); + final Random random = new Random(); + for (int i = 0; i < 1000; i++) { + sample[i] = names[random.nextInt(names.length)]; + } + } + + @AfterClass + public static void washup() { + System.err.println(watch); + } + + @Theory + public void raw(String input) throws Exception { + iterate("writeRaw"); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Rate(" + count + ")=" + rate + ", " + watch); + watch.start("readRaw" + count); + for (String name : names) { + this.gauges.forEach(Pattern.compile(name).asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, DoubleBuffer value) { + err.println(name + "=" + value); + } + }); + } + final DoubleAdder total = new DoubleAdder(); + this.gauges.forEach(Pattern.compile(".*").asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, DoubleBuffer value) { + total.add(value.getValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertTrue(number * threadCount < total.longValue()); + } + + @Theory + public void reader(String input) throws Exception { + iterate("writeReader"); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Rate(" + count + ")=" + rate + ", " + watch); + watch.start("readReader" + count); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric metric) { + err.println(metric); + } + }); + final LongAdder total = new LongAdder(); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric value) { + total.add(value.getValue().intValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertTrue(0 < total.longValue()); + } + + private void iterate(String taskName) throws Exception { + watch.start(taskName + count++); + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Runnable task = new Runnable() { + @Override + public void run() { + for (int i = 0; i < number; i++) { + String name = sample[i % sample.length]; + BufferGaugeServiceSpeedTests.this.service.submit(name, count + i); + } + } + }; + Collection> futures = new HashSet>(); + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(task)); + } + for (Future future : futures) { + future.get(); + } + watch.stop(); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReaderTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReaderTests.java new file mode 100644 index 0000000000..be9be3cb5e --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/BufferMetricReaderTests.java @@ -0,0 +1,58 @@ +/* + * 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.buffer; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests for {@link BufferMetricReader}. + * + * @author Dave Syer + */ +public class BufferMetricReaderTests { + + private CounterBuffers counters = new CounterBuffers(); + + private GaugeBuffers gauges = new GaugeBuffers(); + + private BufferMetricReader reader = new BufferMetricReader(this.counters, this.gauges); + + @Test + public void countReflectsNumberOfMetrics() { + this.gauges.set("foo", 1); + this.counters.increment("bar", 2); + assertEquals(2, this.reader.count()); + } + + @Test + public void findGauge() { + this.gauges.set("foo", 1); + assertNotNull(this.reader.findOne("foo")); + assertEquals(1, this.reader.count()); + } + + @Test + public void findCounter() { + this.counters.increment("foo", 1); + assertNotNull(this.reader.findOne("foo")); + assertEquals(1, this.reader.count()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffersTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffersTests.java new file mode 100644 index 0000000000..4a781ecde3 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterBuffersTests.java @@ -0,0 +1,64 @@ +/* + * 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.buffer; + +import java.util.function.Consumer; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests for {@link CounterBuffers}. + * + * @author Dave Syer + */ +public class CounterBuffersTests { + + private CounterBuffers buffers = new CounterBuffers(); + + private long value; + + @Test + public void inAndOut() { + this.buffers.increment("foo", 2); + this.buffers.get("foo", new Consumer() { + @Override + public void accept(LongBuffer buffer) { + CounterBuffersTests.this.value = buffer.getValue(); + } + }); + assertEquals(2, this.value); + } + + @Test + public void getNonExistent() { + this.buffers.get("foo", new Consumer() { + @Override + public void accept(LongBuffer buffer) { + CounterBuffersTests.this.value = buffer.getValue(); + } + }); + assertEquals(0, this.value); + } + + @Test + public void findNonExistent() { + assertNull(this.buffers.find("foo")); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterServiceSpeedTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterServiceSpeedTests.java new file mode 100644 index 0000000000..ecaae90f98 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/CounterServiceSpeedTests.java @@ -0,0 +1,165 @@ +/* + * 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.buffer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.lang.UsesJava8; +import org.springframework.util.StopWatch; + +import static org.junit.Assert.assertEquals; + +/** + * Speed tests for {@link CounterService}. + * @author Dave Syer + */ +@RunWith(Theories.class) +@UsesJava8 +public class CounterServiceSpeedTests { + + @DataPoints + public static String[] values = new String[10]; + + public static String[] names = new String[] { "foo", "bar", "spam", "bucket" }; + + public static String[] sample = new String[1000]; + + private CounterBuffers counters = new CounterBuffers(); + + private CounterService service = new BufferCounterService(this.counters); + + private BufferMetricReader reader = new BufferMetricReader(this.counters, + new GaugeBuffers()); + + private static int threadCount = 2; + + private static final int number = Boolean.getBoolean("performance.test") ? 10000000 + : 100000; + + private static StopWatch watch = new StopWatch("count"); + + private static int count; + + private static PrintWriter err; + + @BeforeClass + public static void prime() throws FileNotFoundException { + err = new PrintWriter("/dev/null"); + final Random random = new Random(); + for (int i = 0; i < 1000; i++) { + sample[i] = names[random.nextInt(names.length)]; + } + } + + @AfterClass + public static void washup() { + System.err.println(watch); + } + + @Theory + public void raw(String input) throws Exception { + iterate("writeRaw"); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Rate(" + count + ")=" + rate + ", " + watch); + watch.start("readRaw" + count); + for (String name : names) { + this.counters.forEach(Pattern.compile(name).asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, LongBuffer value) { + err.println(name + "=" + value); + } + }); + } + final LongAdder total = new LongAdder(); + this.counters.forEach(Pattern.compile(".*").asPredicate(), + new BiConsumer() { + @Override + public void accept(String name, LongBuffer value) { + total.add(value.getValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertEquals(number * threadCount, total.longValue()); + } + + @Theory + public void reader(String input) throws Exception { + iterate("writeReader"); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Rate(" + count + ")=" + rate + ", " + watch); + watch.start("readReader" + count); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric metric) { + err.println(metric); + } + }); + final LongAdder total = new LongAdder(); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric value) { + total.add(value.getValue().intValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertEquals(number * threadCount, total.longValue()); + } + + private void iterate(String taskName) throws Exception { + watch.start(taskName + count++); + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Runnable task = new Runnable() { + @Override + public void run() { + for (int i = 0; i < number; i++) { + String name = sample[i % sample.length]; + CounterServiceSpeedTests.this.service.increment(name); + } + } + }; + Collection> futures = new HashSet>(); + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(task)); + } + for (Future future : futures) { + future.get(); + } + watch.stop(); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultCounterServiceSpeedTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultCounterServiceSpeedTests.java new file mode 100644 index 0000000000..7bb1485f65 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultCounterServiceSpeedTests.java @@ -0,0 +1,129 @@ +/* + * 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.buffer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; + +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.boot.actuate.metrics.writer.DefaultCounterService; +import org.springframework.lang.UsesJava8; +import org.springframework.util.StopWatch; + +import static org.junit.Assert.assertEquals; + +/** + * Speed tests for {@link DefaultCounterService}. + * + * @author Dave Syer + */ +@RunWith(Theories.class) +@UsesJava8 +public class DefaultCounterServiceSpeedTests { + + @DataPoints + public static String[] values = new String[10]; + + public static String[] names = new String[] { "foo", "bar", "spam", "bucket" }; + + public static String[] sample = new String[1000]; + + private InMemoryMetricRepository repository = new InMemoryMetricRepository(); + + private CounterService counterService = new DefaultCounterService(this.repository); + + private MetricReader reader = this.repository; + + private static int threadCount = 2; + + private static final int number = Boolean.getBoolean("performance.test") ? 2000000 + : 100000; + + private static int count; + + private static StopWatch watch = new StopWatch("count"); + + private static PrintWriter err; + + @BeforeClass + public static void prime() throws FileNotFoundException { + err = new PrintWriter("/dev/null"); + final Random random = new Random(); + for (int i = 0; i < 1000; i++) { + sample[i] = names[random.nextInt(names.length)]; + } + } + + @Theory + public void counters(String input) throws Exception { + watch.start("counters" + count++); + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Runnable task = new Runnable() { + @Override + public void run() { + for (int i = 0; i < number; i++) { + String name = sample[i % sample.length]; + DefaultCounterServiceSpeedTests.this.counterService.increment(name); + } + } + }; + Collection> futures = new HashSet>(); + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(task)); + } + for (Future future : futures) { + future.get(); + } + watch.stop(); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Counters rate(" + count + ")=" + rate + ", " + watch); + watch.start("read" + count); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric metric) { + err.println(metric); + } + }); + final LongAdder total = new LongAdder(); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric value) { + total.add(value.getValue().intValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertEquals(number * threadCount, total.longValue()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultGaugeServiceSpeedTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultGaugeServiceSpeedTests.java new file mode 100644 index 0000000000..595e2832d1 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DefaultGaugeServiceSpeedTests.java @@ -0,0 +1,129 @@ +/* + * 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.buffer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; + +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.boot.actuate.metrics.writer.DefaultGaugeService; +import org.springframework.lang.UsesJava8; +import org.springframework.util.StopWatch; + +import static org.junit.Assert.assertTrue; + +/** + * Speed tests for {@link DefaultGaugeService}. + * + * @author Dave Syer + */ +@RunWith(Theories.class) +@UsesJava8 +public class DefaultGaugeServiceSpeedTests { + + @DataPoints + public static String[] values = new String[10]; + + public static String[] names = new String[] { "foo", "bar", "spam", "bucket" }; + + public static String[] sample = new String[1000]; + + private InMemoryMetricRepository repository = new InMemoryMetricRepository(); + + private GaugeService gaugeService = new DefaultGaugeService(this.repository); + + private MetricReader reader = this.repository; + + private static int threadCount = 2; + + private static final int number = Boolean.getBoolean("performance.test") ? 5000000 + : 100000; + + private static int count; + + private static StopWatch watch = new StopWatch("count"); + + private static PrintWriter err; + + @BeforeClass + public static void prime() throws FileNotFoundException { + err = new PrintWriter("/dev/null"); + final Random random = new Random(); + for (int i = 0; i < 1000; i++) { + sample[i] = names[random.nextInt(names.length)]; + } + } + + @Theory + public void gauges(String input) throws Exception { + watch.start("gauges" + count++); + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Runnable task = new Runnable() { + @Override + public void run() { + for (int i = 0; i < number; i++) { + String name = sample[i % sample.length]; + DefaultGaugeServiceSpeedTests.this.gaugeService.submit(name, count + + i); + } + } + }; + Collection> futures = new HashSet>(); + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(task)); + } + for (Future future : futures) { + future.get(); + } + watch.stop(); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Gauges rate(" + count + ")=" + rate + ", " + watch); + watch.start("read" + count); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric metric) { + err.println(metric); + } + }); + final LongAdder total = new LongAdder(); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric value) { + total.add(value.getValue().intValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertTrue(0 < total.longValue()); + } +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DropwizardCounterServiceSpeedTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DropwizardCounterServiceSpeedTests.java new file mode 100644 index 0000000000..5005d97906 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/buffer/DropwizardCounterServiceSpeedTests.java @@ -0,0 +1,133 @@ +/* + * 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.buffer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Consumer; + +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.dropwizard.DropwizardMetricServices; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.reader.MetricRegistryMetricReader; +import org.springframework.lang.UsesJava8; +import org.springframework.util.StopWatch; + +import com.codahale.metrics.MetricRegistry; + +import static org.junit.Assert.assertEquals; + +/** + * Speeds tests for {@link DropwizardMetricServices DropwizardMetricServices'} + * {@link CounterService}. + * + * @author Dave Syer + */ +@RunWith(Theories.class) +@UsesJava8 +public class DropwizardCounterServiceSpeedTests { + + @DataPoints + public static String[] values = new String[10]; + + public static String[] names = new String[] { "foo", "bar", "spam", "bucket" }; + + public static String[] sample = new String[1000]; + + private MetricRegistry registry = new MetricRegistry(); + + private CounterService counterService = new DropwizardMetricServices(this.registry); + + private MetricReader reader = new MetricRegistryMetricReader(this.registry); + + private static int threadCount = 2; + + private static final int number = Boolean.getBoolean("performance.test") ? 10000000 + : 100000; + + private static int count; + + private static StopWatch watch = new StopWatch("count"); + + private static PrintWriter err; + + @BeforeClass + public static void prime() throws FileNotFoundException { + err = new PrintWriter("/dev/null"); + final Random random = new Random(); + for (int i = 0; i < 1000; i++) { + sample[i] = names[random.nextInt(names.length)]; + } + } + + @Theory + public void counters(String input) throws Exception { + watch.start("counters" + count++); + ExecutorService pool = Executors.newFixedThreadPool(threadCount); + Runnable task = new Runnable() { + @Override + public void run() { + for (int i = 0; i < number; i++) { + String name = sample[i % sample.length]; + DropwizardCounterServiceSpeedTests.this.counterService + .increment(name); + } + } + }; + Collection> futures = new HashSet>(); + for (int i = 0; i < threadCount; i++) { + futures.add(pool.submit(task)); + } + for (Future future : futures) { + future.get(); + } + watch.stop(); + double rate = number / watch.getLastTaskTimeMillis() * 1000; + System.err.println("Counters rate(" + count + ")=" + rate + ", " + watch); + watch.start("read" + count); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric metric) { + err.println(metric); + } + }); + final LongAdder total = new LongAdder(); + this.reader.findAll().forEach(new Consumer>() { + @Override + public void accept(Metric value) { + total.add(value.getValue().intValue()); + } + }); + watch.stop(); + System.err.println("Read(" + count + ")=" + watch.getLastTaskTimeMillis() + "ms"); + assertEquals(number * threadCount, total.longValue()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServicesTests.java similarity index 65% rename from spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriterTests.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServicesTests.java index 896fee979a..13b499b4b7 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DropwizardMetricWriterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/dropwizard/DropwizardMetricServicesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics.writer; +package org.springframework.boot.actuate.metrics.dropwizard; import java.util.ArrayList; import java.util.List; import org.junit.Test; -import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.dropwizard.DropwizardMetricServices; import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; @@ -29,56 +29,60 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; /** - * Tests for {@link DropwizardMetricWriter}. - * + * Tests for {@link DropwizardMetricServices}. + * * @author Dave Syer */ -public class DropwizardMetricWriterTests { +public class DropwizardMetricServicesTests { private final MetricRegistry registry = new MetricRegistry(); - private final DropwizardMetricWriter writer = new DropwizardMetricWriter(this.registry); + private final DropwizardMetricServices writer = new DropwizardMetricServices( + this.registry); @Test public void incrementCounter() { - this.writer.increment(new Delta("foo", 2)); - this.writer.increment(new Delta("foo", 1)); - assertEquals(3, this.registry.counter("foo").getCount()); + this.writer.increment("foo"); + this.writer.increment("foo"); + this.writer.increment("foo"); + assertEquals(3, this.registry.counter("counter.foo").getCount()); } @Test public void updatePredefinedMeter() { - this.writer.increment(new Delta("meter.foo", 2)); - this.writer.increment(new Delta("meter.foo", 1)); + this.writer.increment("meter.foo"); + this.writer.increment("meter.foo"); + this.writer.increment("meter.foo"); assertEquals(3, this.registry.meter("meter.foo").getCount()); } @Test public void updatePredefinedCounter() { - this.writer.increment(new Delta("counter.foo", 2)); - this.writer.increment(new Delta("counter.foo", 1)); + this.writer.increment("counter.foo"); + this.writer.increment("counter.foo"); + this.writer.increment("counter.foo"); assertEquals(3, this.registry.counter("counter.foo").getCount()); } @Test public void setGauge() { - this.writer.set(new Metric("foo", 2.1)); - this.writer.set(new Metric("foo", 2.3)); + this.writer.submit("foo", 2.1); + this.writer.submit("foo", 2.3); @SuppressWarnings("unchecked") - Gauge gauge = (Gauge) this.registry.getMetrics().get("foo"); + Gauge gauge = (Gauge) this.registry.getMetrics().get("gauge.foo"); assertEquals(new Double(2.3), gauge.getValue()); } @Test public void setPredfinedTimer() { - this.writer.set(new Metric("timer.foo", 200)); - this.writer.set(new Metric("timer.foo", 300)); + this.writer.submit("timer.foo", 200); + this.writer.submit("timer.foo", 300); assertEquals(2, this.registry.timer("timer.foo").getCount()); } @Test public void setPredfinedHistogram() { - this.writer.set(new Metric("histogram.foo", 2.1)); - this.writer.set(new Metric("histogram.foo", 2.3)); + this.writer.submit("histogram.foo", 2.1); + this.writer.submit("histogram.foo", 2.3); assertEquals(2, this.registry.histogram("histogram.foo").getCount()); } @@ -112,9 +116,9 @@ public class DropwizardMetricWriterTests { public static class WriterThread extends Thread { private int index; private boolean failed; - private DropwizardMetricWriter writer; + private DropwizardMetricServices writer; - public WriterThread(ThreadGroup group, int index, DropwizardMetricWriter writer) { + public WriterThread(ThreadGroup group, int index, DropwizardMetricServices writer) { super(group, "Writer-" + index); this.index = index; @@ -129,17 +133,9 @@ public class DropwizardMetricWriterTests { public void run() { for (int i = 0; i < 10000; i++) { try { - Metric metric1 = new Metric("timer.test.service", - this.index); - this.writer.set(metric1); - - Metric metric2 = new Metric( - "histogram.test.service", this.index); - this.writer.set(metric2); - - Metric metric3 = new Metric("gauge.test.service", - this.index); - this.writer.set(metric3); + this.writer.submit("timer.test.service", this.index); + this.writer.submit("histogram.test.service", this.index); + this.writer.submit("gauge.test.service", this.index); } catch (IllegalArgumentException iae) { this.failed = true; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java index 9ad012f145..e8b1e50d3f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * 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. @@ -41,6 +41,33 @@ public class MetricCopyExporterTests { assertEquals(1, this.writer.count()); } + @Test + public void exportIncludes() { + this.exporter.setIncludes("*"); + this.reader.set(new Metric("foo", 2.3)); + this.exporter.export(); + assertEquals(1, this.writer.count()); + } + + @Test + public void exportExcludesWithIncludes() { + this.exporter.setIncludes("*"); + this.exporter.setExcludes("foo"); + this.reader.set(new Metric("foo", 2.3)); + this.reader.set(new Metric("bar", 2.4)); + this.exporter.export(); + assertEquals(1, this.writer.count()); + } + + @Test + public void exportExcludesDefaultIncludes() { + this.exporter.setExcludes("foo"); + this.reader.set(new Metric("foo", 2.3)); + this.reader.set(new Metric("bar", 2.4)); + this.exporter.export(); + assertEquals(1, this.writer.count()); + } + @Test public void timestamp() { this.reader.set(new Metric("foo", 2.3)); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricExportersTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricExportersTests.java new file mode 100644 index 0000000000..6b0c332ce9 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricExportersTests.java @@ -0,0 +1,60 @@ +/* + * 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.export; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Dave Syer + */ +public class MetricExportersTests { + + private MetricExporters exporters; + private MetricExportProperties export = new MetricExportProperties(); + private Map writers = new LinkedHashMap(); + private MetricReader reader = Mockito.mock(MetricReader.class); + private MetricWriter writer = Mockito.mock(MetricWriter.class); + + @Test + public void emptyWriters() { + this.exporters = new MetricExporters(this.reader, this.writers, this.export); + this.exporters.configureTasks(new ScheduledTaskRegistrar()); + assertNotNull(this.exporters.getExporters()); + assertEquals(0, this.exporters.getExporters().size()); + } + + @Test + public void oneWriter() { + this.export.setUpDefaults(); + this.writers.put("foo", this.writer); + this.exporters = new MetricExporters(this.reader, this.writers, this.export); + this.exporters.configureTasks(new ScheduledTaskRegistrar()); + assertNotNull(this.exporters.getExporters()); + assertEquals(1, this.exporters.getExporters().size()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java new file mode 100644 index 0000000000..3da8bcfebf --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/jmx/DefaultMetricNamingStrategyTests.java @@ -0,0 +1,69 @@ +/* + * 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.jmx; + +import javax.management.ObjectName; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class DefaultMetricNamingStrategyTests { + + private DefaultMetricNamingStrategy strategy = new DefaultMetricNamingStrategy(); + + @Test + public void simpleName() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + } + + @Test + public void onePeriod() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("value")); + } + + @Test + public void twoPeriods() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar.spam"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name")); + assertEquals("Wrong name: " + name, "spam", name.getKeyProperty("value")); + } + + @Test + public void threePeriods() throws Exception { + ObjectName name = this.strategy.getObjectName(null, + "domain:type=MetricValue,name=foo.bar.spam.bucket"); + assertEquals("domain", name.getDomain()); + assertEquals("foo", name.getKeyProperty("type")); + assertEquals("Wrong name: " + name, "bar", name.getKeyProperty("name")); + assertEquals("Wrong name: " + name, "spam.bucket", name.getKeyProperty("value")); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriterTests.java new file mode 100644 index 0000000000..0c68878203 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/opentsdb/OpenTsdbMetricWriterTests.java @@ -0,0 +1,82 @@ +/* + * 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.opentsdb; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestOperations; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +/** + * @author Dave Syer + */ +public class OpenTsdbMetricWriterTests { + + private OpenTsdbMetricWriter writer; + private RestOperations restTemplate = Mockito.mock(RestOperations.class); + + @Before + public void init() { + this.writer = new OpenTsdbMetricWriter(); + this.writer.setRestTemplate(this.restTemplate); + } + + @Test + public void postSuccessfullyOnFlush() { + this.writer.set(new Metric("foo", 2.4)); + given( + this.restTemplate.postForEntity(Matchers.anyString(), + Matchers.any(Object.class), anyMap())) + .willReturn(emptyResponse()); + this.writer.flush(); + verify(this.restTemplate).postForEntity(Matchers.anyString(), + Matchers.any(Object.class), anyMap()); + } + + @Test + public void flushAutomaticlly() { + given( + this.restTemplate.postForEntity(Matchers.anyString(), + Matchers.any(Object.class), anyMap())) + .willReturn(emptyResponse()); + this.writer.setBufferSize(0); + this.writer.set(new Metric("foo", 2.4)); + verify(this.restTemplate).postForEntity(Matchers.anyString(), + Matchers.any(Object.class), anyMap()); + } + + @SuppressWarnings("rawtypes") + private ResponseEntity emptyResponse() { + return new ResponseEntity(Collections.emptyMap(), HttpStatus.OK); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Class anyMap() { + return Matchers.any(Class.class); + } + +} 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 0000000000..c90f6f4a0d --- /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 f065efc826..89ff0c6e43 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 f655a5fb5e..e699f61abc 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -889,48 +889,227 @@ beans are gathered by the endpoint. You can easily change that by defining your `MetricsEndpoint`. - [[production-ready-metric-repositories]] -=== Metric repositories -Metric service implementations are usually bound to a -{sc-spring-boot-actuator}/metrics/repository/MetricRepository.{sc-ext}[`MetricRepository`]. -A `MetricRepository` is responsible for storing and retrieving metric information. Spring -Boot provides an `InMemoryMetricRepository` and a `RedisMetricRepository` out of the -box (the in-memory repository is the default) but you can also write your own. The -`MetricRepository` interface is actually composed of higher level `MetricReader` and -`MetricWriter` interfaces. For full details refer to the -{dc-spring-boot-actuator}/metrics/repository/MetricRepository.{dc-ext}[Javadoc]. +=== Special features with Java 8 -There's nothing to stop you hooking a `MetricRepository` with back-end storage directly -into your app, but we recommend using the default `InMemoryMetricRepository` -(possibly with a custom `Map` instance if you are worried about heap usage) and -populating a back-end repository through a scheduled export job. In that way you get -some buffering in memory of the metric values and you can reduce the network -chatter by exporting less frequently or in batches. Spring Boot provides -an `Exporter` interface and a few basic implementations for you to get started with that. +The default implementation of `GaugeService` and `CounterService` provided by Spring Boot +depends on the version of Java that you are using. With Java 8 (or better) the +implementation switches to a high-performance version optimized for fast writes, backed by +atomic in-memory buffers, rather than by the immutable but relatively expensive +`Metric` type (counters are approximately 5 times faster and gauges approximately twice +as fast as the repository-based implementations). The Dropwizard metrics services (see +below) are also very efficient even for Java 7 (they have backports of some of the Java 8 +concurrency libraries), but they do not record timestamps for metric values. If +performance of metric gathering is a concern then it is always advisable to use one of the +high-performance options, and also to only read metrics infrequently, so that the writes +are buffered locally and only read when needed. + +NOTE: The old `MetricRepository` and its `InMemoryMetricRepository` implementation are not +used by default if you are on Java 8 or if you are using Dropwizard metrics. -[[production-ready-code-hale-metrics]] + +[[production-ready-metric-writers]] +=== Metric writers and aggregation + +Spring Boot provides a couple of implementations of a marker interface called `Exporter` +which can be used to copy metric readings from the in-memory buffers to a place where they +can be analysed and displayed. Indeed, if you provide a `@Bean` that implements the +`MetricWriter` interface, then it will automatically be hooked up to an `Exporter` and fed +metric updates every 5 seconds (configured via `spring.metrics.export.delayMillis`) via a +`@Scheduled` annotation in `MetricRepositoryAutoConfiguration`. + +The default exporter is a `MetricCopyExporter` which tries to optimize itself by not +copying values that haven't changed since it was last called. The optimization can be +switched off using a flag (`spring.metrics.export.ignoreTimestamps`). Note also that the +`MetricRegistry` has no support for timestamps, so the optimization is not available if +you are using Dropwizard metrics (all metrics will be copied on every tick). + + + +[[production-ready-metric-writers-export-to-redis]] +==== Example: Export to Redis + +If you provide a `@Bean` of type `RedisMetricRepository` the metrics are exported to a +Redis cache for aggregation. The `RedisMetricRepository` has 2 important parameters to +configure it for this purpose: `prefix` and `key` (passed into its constructor). It is +best to use a prefix that is unique to the application instance (e.g. using a random value +and maybe the logical name of the application to make it possible to correlate with other +instances of the same application). The "key" is used to keep a global index of all +metric names, so it should be unique "globally", whatever that means for your system (e.g. +2 instances of the same system could share a Redis cache if they have distinct keys). +Example: + +[source,java,indent=0] +---- + +@Value("metrics.mysystem.${random.value:0000}.${spring.application.name:application}") +private String prefix = "metrics.mysystem"; + +@Value("${metrics.key:keys.mysystem}") +private String key = "METRICSKEY"; + +@Bean +MetricWriter metricWriter() { + return new RedisMetricRepository(connectionFactory, prefix, key); +} +---- + +The prefix is constructed with the application name at the end, so it can easily be used +to identify a group of processes with the same logical name later. + +NOTE: it's important to set both the key and the prefix. The key is used for all +repository operations, and can be shared by multiple repositories. If multiple +repositories share a key (like in the case where you need to aggregate across them), then +you normally have a read-only "master" repository that has a short, but identifiable, +prefix (like "metrics.mysystem"), and many write-only repositories with prefixes that +start with the master prefix (like `metrics.mysystem.*` in the example above). It is +efficient to read all the keys from a "master" repository like that, but inefficient to +read a subset with a longer prefix (e.g. using one of the writing repositories). + + + +[[production-ready-metric-writers-export-to-open-tdsb]] +==== Example: Export to Open TSDB + +If you provide a `@Bean` of type `OpenTsdbHttpMetricWriter` the metrics are exported to +http://opentsdb.net/[Open TSDB] for aggregation. The `OpenTsdbHttpMetricWriter` has a +`url` property that you need to set to the Open TSDB "/put" endpoint, e.g. +`http://localhost:4242/api/put`). It also has a `namingStrategy` that you can customize or +configure to make the metrics match the data structure you need on the server. By default +it just passes through the metric name as an Open TSDB metric name and adds a tag "domain" +with value "org.springframework.metrics" and another tag "process" with value equals to +the object hash of the naming strategy. Thus, after running the application and generating +some metrics (e.g. by pinging the home page) you can inspect the metrics in the TDB UI +(http://localhost:4242 by default). Example: + +[source,indent=0] +---- +curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root +[ + { + "metric": "counter.status.200.root", + "tags": { + "domain": "org.springframework.metrics", + "process": "b968a76" + }, + "aggregateTags": [], + "dps": { + "1430492872": 2, + "1430492875": 6 + } + } +] +---- + + + +[[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 +physical sources. Sources for the same logical metric just need to publish them with a +period-separated prefix, and the reader will aggregate (by truncating the metric names, +and dropping the prefix). Counters are summed and everything else (i.e. gauges) take their +most recent value. + +This is very useful (for instance) if multiple application instances are feeding to a +central (e.g. redis) repository and you want to display the results. Particularly +recommended in conjunction with a `MetricReaderPublicMetrics` for hooking up to the +results to the "/metrics" endpoint. Example: + +[source,java,indent=0] +---- + @Bean + public PublicMetrics metricsAggregate() { + return new MetricReaderPublicMetrics(aggregates()); + } + + @Bean + protected MetricReader repository(RedisConnectionFactory connectionFactory) { + RedisMetricRepository repository = new RedisMetricRepository(connectionFactory, + "mysystem", "myorg.keys"); + return repository; + } + + @Bean + protected MetricReader aggregates() { + AggregateMetricReader repository = new AggregateMetricReader(repository()); + return repository; + } +---- + + + +[[production-ready-dropwizard-metrics]] === Dropwizard Metrics -User of the https://dropwizard.github.io/metrics/[Dropwizard '`Metrics`' library] will -automatically find that Spring Boot metrics are published to -`com.codahale.metrics.MetricRegistry`. A default `com.codahale.metrics.MetricRegistry` -Spring bean will be created when you declare a dependency to the -`io.dropwizard.metrics:metrics-core` library; you can also register you own `@Bean` -instance if you need customizations. Metrics from the `MetricRegistry` are also -automatically exposed via the `/metrics` endpoint. +A default `MetricRegistry` Spring bean will be created when you declare a dependency to +the `io.dropwizard.metrics:metric-core` library; you can also register you own `@Bean` +instance if you need customizations. Users of the +https://dropwizard.github.io/metrics/[Dropwizard '`Metrics`' library] will find that +Spring Boot metrics are automatically published to `com.codahale.metrics.MetricRegistry`. +Metrics from the `MetricRegistry` are also automatically exposed via the `/metrics` +endpoint -Users can create Dropwizard metrics by prefixing their metric names with the appropriate -type (e.g. `+histogram.*+`, `+meter.*+`). +When Dropwizard metrics are in use, the default `CounterService` and `GaugeService` are +replaced with a `DropwizardMetricServices`, which is a wrapper around the `MetricRegistry` +(so you can `@Autowired` one of those services and use it as normal). You can also create +"special" Dropwizard metrics by prefixing your metric names with the appropriate type +(i.e. `+timer.*+`, `+histogram.*+` for gauges, and `+meter.*+` for counters). [[production-ready-metrics-message-channel-integration]] === Message channel integration -If the '`Spring Messaging`' jar is on your classpath a `MessageChannel` called -`metricsChannel` is automatically created (unless one already exists). All metric update -events are additionally published as '`messages`' on that channel. Additional analysis or -actions can be taken by clients subscribing to that channel. +If a `MessageChannel` bean called `metricsChannel` exists, then a `MetricWriter` will be +created that writes metrics to that channel. The writer is automatically hooked up to an +exporter (as for all writers), so all metric values will appear on the channel, and +additional analysis or actions can be taken by subscribers (it's up to you to provide the +channel and any subscribers you need). diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 85fb2ae60d..277050ddfb 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -52,6 +52,9 @@ spring-boot-sample-jta-bitronix spring-boot-sample-jta-jndi spring-boot-sample-liquibase + spring-boot-sample-metrics-dropwizard + spring-boot-sample-metrics-opentsdb + spring-boot-sample-metrics-redis spring-boot-sample-parent-context spring-boot-sample-profile spring-boot-sample-secure diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/pom.xml b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/pom.xml new file mode 100644 index 0000000000..e3bfbce0ed --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + spring-boot-sample-metrics-dropwizard + spring-boot-sample-metrics-dropwizard + Spring Boot Metrics Redis Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + io.dropwizard.metrics + metrics-core + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/HelloWorldService.java b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/HelloWorldService.java new file mode 100644 index 0000000000..6f7dd258c7 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/HelloWorldService.java @@ -0,0 +1,40 @@ +/* + * 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 sample.metrics.dropwizard; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "service", ignoreUnknownFields = false) +public class HelloWorldService { + + private String name = "World"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelloMessage() { + return "Hello " + this.name; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleController.java b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleController.java new file mode 100644 index 0000000000..42ec65fbd2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleController.java @@ -0,0 +1,63 @@ +/* + * 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 sample.metrics.dropwizard; + +import java.util.Collections; +import java.util.Map; + +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.context.annotation.Description; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@Description("A controller for handling requests for hello messages") +public class SampleController { + + @Autowired + private HelloWorldService helloWorldService; + + @Autowired + private GaugeService gauges; + + @RequestMapping(value = "/", method = RequestMethod.GET) + @ResponseBody + public Map hello() { + this.gauges.submit("timer.test.value", Math.random() * 1000 + 1000); + return Collections.singletonMap("message", + this.helloWorldService.getHelloMessage()); + } + + protected static class Message { + + @NotBlank(message = "Message value cannot be empty") + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplication.java b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplication.java new file mode 100644 index 0000000000..ff902cf54c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplication.java @@ -0,0 +1,29 @@ +/* + * 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 sample.metrics.dropwizard; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleDropwizardMetricsApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleDropwizardMetricsApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/resources/application.properties new file mode 100644 index 0000000000..4107192475 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/main/resources/application.properties @@ -0,0 +1 @@ +service.name: Phil diff --git a/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/test/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplicationTests.java b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/test/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplicationTests.java new file mode 100644 index 0000000000..ec6f70a2c0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-dropwizard/src/test/java/sample/metrics/dropwizard/SampleDropwizardMetricsApplicationTests.java @@ -0,0 +1,57 @@ +/* + * 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 sample.metrics.dropwizard; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import com.codahale.metrics.MetricRegistry; + +import static org.junit.Assert.assertEquals; + +/** + * Basic integration tests for {@link SampleDropwizardMetricsApplication}. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleDropwizardMetricsApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +@DirtiesContext +public class SampleDropwizardMetricsApplicationTests { + + @Autowired + private MetricRegistry registry; + + @Autowired + private GaugeService gauges; + + @Test + public void timerCreated() { + this.gauges.submit("timer.test", 1234); + assertEquals(1, this.registry.getTimers().get("timer.test").getCount()); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/README.adoc b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/README.adoc new file mode 100644 index 0000000000..f6d5b1a838 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/README.adoc @@ -0,0 +1,29 @@ +Spring Boot sample with Open TSDB export for metrics. + +Start opentsdb, e.g. with [Docker Compose]() + +[source,indent=0] +---- +$ docker-compose up +---- + +Run the app and ping the home page (http://localhost:8080) a few times. Go and look at +the result in the TDB UI, e.g. + +[source,indent=0] +---- + $ curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root + [ + { + "metric": "counter.status.200.root", + "tags": { + "prefix": "spring.b968a76" + }, + "aggregateTags": [], + "dps": { + "1430492872": 2, + "1430492875": 6 + } + } + ] +---- \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/docker-compose.yml b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/docker-compose.yml new file mode 100644 index 0000000000..c9d453bd7c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/docker-compose.yml @@ -0,0 +1,4 @@ +opentsdb: + image: lancope/opentsdb + ports: + - "4242:4242" diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/pom.xml b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/pom.xml new file mode 100644 index 0000000000..eb20ebffc5 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + spring-boot-sample-metrics-opentsdb + spring-boot-sample-metrics-opentsdb + Spring Boot Actuator Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/HelloWorldService.java b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/HelloWorldService.java new file mode 100644 index 0000000000..e4bcc96cae --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/HelloWorldService.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2013 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 sample.metrics.opentsdb; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "service", ignoreUnknownFields = false) +public class HelloWorldService { + + private String name = "World"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelloMessage() { + return "Hello " + this.name; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleController.java b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleController.java new file mode 100644 index 0000000000..d4f36f55e9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleController.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2013 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 sample.metrics.opentsdb; + +import java.util.Collections; +import java.util.Map; + +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Description; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@Description("A controller for handling requests for hello messages") +public class SampleController { + + @Autowired + private HelloWorldService helloWorldService; + + @RequestMapping(value = "/", method = RequestMethod.GET) + @ResponseBody + public Map hello() { + return Collections.singletonMap("message", + this.helloWorldService.getHelloMessage()); + } + + protected static class Message { + + @NotBlank(message = "Message value cannot be empty") + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplication.java b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplication.java new file mode 100644 index 0000000000..06403caad9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplication.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2013 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 sample.metrics.opentsdb; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.metrics.opentsdb.DefaultOpenTsdbNamingStrategy; +import org.springframework.boot.actuate.metrics.opentsdb.OpenTsdbMetricWriter; +import org.springframework.boot.actuate.metrics.opentsdb.OpenTsdbNamingStrategy; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleOpenTsdbExportApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleOpenTsdbExportApplication.class, args); + } + + @Bean + @ConfigurationProperties("metrics.export") + public MetricWriter openTsdbMetricWriter() { + OpenTsdbMetricWriter writer = new OpenTsdbMetricWriter(); + writer.setNamingStrategy(namingStrategy()); + return writer; + } + + @Bean + @ConfigurationProperties("metrics.names") + public OpenTsdbNamingStrategy namingStrategy() { + return new DefaultOpenTsdbNamingStrategy(); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/resources/application.properties new file mode 100644 index 0000000000..22fc860490 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/main/resources/application.properties @@ -0,0 +1,2 @@ +service.name: Phil +metrics.names.tags.process: ${spring.application.name:application}:${random.value:0000} \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/test/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplicationTests.java b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/test/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplicationTests.java new file mode 100644 index 0000000000..8a2f70cdb1 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-opentsdb/src/test/java/sample/metrics/opentsdb/SampleOpenTsdbExportApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2014 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 sample.metrics.opentsdb; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Basic integration tests for {@link SampleOpenTsdbExportApplication}. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleOpenTsdbExportApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +@DirtiesContext +public class SampleOpenTsdbExportApplicationTests { + + @Test + public void contextLoads() { + + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/README.adoc b/spring-boot-samples/spring-boot-sample-metrics-redis/README.adoc new file mode 100644 index 0000000000..3038d54817 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/README.adoc @@ -0,0 +1,46 @@ +Spring Boot sample with Redis export for metrics. + +Start redis, e.g. with [Docker Compose]() + +[source,indent=0] +---- + $ docker-compose up +---- + +Run the app and ping the home page (http://localhost:8080) a few times. Go and look at +the result in Redis, e.g. + +[source,indent=0] +---- + $ redis-cli + 127.0.0.1:6379> keys * + 1) "keys.spring.metrics" + 2) "spring.metrics.counter.status.200.root" + 3) "spring.metrics.gauge.response.root" + 127.0.0.1:6379> zrange keys.spring.metrics 0 0 WITHSCORES + 1) "spring.metrics.counter.status.200.root" + 2) "4" +---- + +There is also an `AggregateMetricReader` with public metrics in the application context, +and you can see the result in the "/metrics" (metrics with names in "aggregate.*"). +The way the Redis repository was set up (with a random key in the metric names) makes the +aggregates work across restarts of the same application, or across a scaled up application +running in multiple processes. E.g. + +[source,indent=0] +---- + $ curl localhost:8080/metrics + { + ... + "aggregate.application.counter.status.200.metrics": 12, + "aggregate.application.counter.status.200.root": 29, + "aggregate.application.gauge.response.metrics": 43, + "aggregate.application.gauge.response.root": 5, + "counter.status.200.root": 2, + "counter.status.200.metrics": 1, + "gauge.response.metrics": 43, + "gauge.response.root": 5 + } +---- + diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/docker-compose.yml b/spring-boot-samples/spring-boot-sample-metrics-redis/docker-compose.yml new file mode 100644 index 0000000000..e5a62d0ef4 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/docker-compose.yml @@ -0,0 +1,4 @@ +redis: + image: redis + ports: + - "6379:6379" diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/pom.xml b/spring-boot-samples/spring-boot-sample-metrics-redis/pom.xml new file mode 100644 index 0000000000..dded5607f9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + spring-boot-sample-metrics-redis + spring-boot-sample-metrics-redis + Spring Boot Metrics Redis Sample + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-redis + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/AggregateMetricsConfiguration.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/AggregateMetricsConfiguration.java new file mode 100644 index 0000000000..a8b3fdddfa --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/AggregateMetricsConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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 sample.metrics.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; +import org.springframework.boot.actuate.endpoint.PublicMetrics; +import org.springframework.boot.actuate.metrics.aggregate.AggregateMetricReader; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * @author Dave Syer + */ +@Configuration +public class AggregateMetricsConfiguration { + + @Autowired + private ExportProperties export; + + @Autowired + private RedisConnectionFactory connectionFactory; + + @Bean + public PublicMetrics metricsAggregate() { + return new MetricReaderPublicMetrics(aggregatesMetricReader()); + } + + private MetricReader globalMetricsForAggregation() { + return new RedisMetricRepository(this.connectionFactory, + this.export.getAggregatePrefix(), this.export.getKey()); + } + + private MetricReader aggregatesMetricReader() { + AggregateMetricReader repository = new AggregateMetricReader( + globalMetricsForAggregation()); + return repository; + } +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/ExportProperties.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/ExportProperties.java new file mode 100644 index 0000000000..57c4685cb9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/ExportProperties.java @@ -0,0 +1,55 @@ +/* + * 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 sample.metrics.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; + +@ConfigurationProperties("redis.metrics.export") +class ExportProperties { + + private String prefix = "spring.metrics"; + private String key = "keys.spring.metrics"; + + public String getPrefix() { + return this.prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getAggregatePrefix() { + String[] tokens = StringUtils.delimitedListToStringArray(this.prefix, "."); + if (tokens.length > 1) { + if (StringUtils.hasText(tokens[1])) { + // If the prefix has 2 or more non-trivial parts, use the first 1 + return tokens[0]; + } + } + return this.prefix; + } + +} \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/HelloWorldService.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/HelloWorldService.java new file mode 100644 index 0000000000..9b08174c00 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/HelloWorldService.java @@ -0,0 +1,40 @@ +/* + * 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 sample.metrics.redis; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "service", ignoreUnknownFields = false) +public class HelloWorldService { + + private String name = "World"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHelloMessage() { + return "Hello " + this.name; + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleController.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleController.java new file mode 100644 index 0000000000..b72a7591d5 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleController.java @@ -0,0 +1,58 @@ +/* + * 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 sample.metrics.redis; + +import java.util.Collections; +import java.util.Map; + +import org.hibernate.validator.constraints.NotBlank; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Description; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@Description("A controller for handling requests for hello messages") +public class SampleController { + + @Autowired + private HelloWorldService helloWorldService; + + @RequestMapping(value = "/", method = RequestMethod.GET) + @ResponseBody + public Map hello() { + return Collections.singletonMap("message", + this.helloWorldService.getHelloMessage()); + } + + protected static class Message { + + @NotBlank(message = "Message value cannot be empty") + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleRedisExportApplication.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleRedisExportApplication.java new file mode 100644 index 0000000000..e60e8e81f7 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/java/sample/metrics/redis/SampleRedisExportApplication.java @@ -0,0 +1,54 @@ +/* + * 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 sample.metrics.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.metrics.jmx.JmxMetricWriter; +import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.jmx.export.MBeanExporter; + +@SpringBootApplication +@EnableConfigurationProperties(ExportProperties.class) +public class SampleRedisExportApplication { + + @Autowired + private ExportProperties export; + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleRedisExportApplication.class, args); + } + + @Bean + public RedisMetricRepository redisMetricWriter( + RedisConnectionFactory connectionFactory) { + return new RedisMetricRepository(connectionFactory, this.export.getPrefix(), + this.export.getKey()); + } + + @Bean + public JmxMetricWriter jmxMetricWriter( + @Qualifier("mbeanExporter") MBeanExporter exporter) { + return new JmxMetricWriter(exporter); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/resources/application.properties new file mode 100644 index 0000000000..61a8193c9f --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/main/resources/application.properties @@ -0,0 +1,4 @@ +service.name: Phil +redis.metrics.export.prefix: metrics.sample.${random.value:0000}.${spring.application.name:application} +redis.metrics.export.key: keys.metrics.sample +spring.jmx.default-domain: org.springframework.boot diff --git a/spring-boot-samples/spring-boot-sample-metrics-redis/src/test/java/sample/metrics/redis/SampleRedisExportApplicationTests.java b/spring-boot-samples/spring-boot-sample-metrics-redis/src/test/java/sample/metrics/redis/SampleRedisExportApplicationTests.java new file mode 100644 index 0000000000..65180f4c44 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-metrics-redis/src/test/java/sample/metrics/redis/SampleRedisExportApplicationTests.java @@ -0,0 +1,43 @@ +/* + * 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 sample.metrics.redis; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +/** + * Basic integration tests for {@link SampleRedisExportApplication}. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SampleRedisExportApplication.class) +@WebAppConfiguration +@IntegrationTest({ "server.port=0", "spring.jmx.enabled=true" }) +@DirtiesContext +public class SampleRedisExportApplicationTests { + + @Test + public void contextLoads() { + } + +}