From aa2b020660fae78b0db90f9fbcd2fed9d3311c16 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 6 Dec 2013 16:10:50 +0000 Subject: [PATCH] Refactor metrics to expose richer feature set Main user-facing interface is still Counter/GaugeService but the back end behind that has more options. The Default*Services write metrics to a MetricWriter and there are some variants of that, and also variants of MetricReader (basic read-only actions). MetricRepository is now a combination of MetricReader, MetricWriter and some more methods that make it a bit more repository like. There is also a MultiMetricReader and a MultiMetricRepository for the common case where metrics are stored in related (often open ended) groups. Examples would be complex metrics like histograms and "rich" metrics with averages and statistics attached (which are both closed) and "field counters" which count the occurrences of values of a particular named field or slot in an incoming message (e.g. counting Twitter hastags, open ended). In memory and redis implementations are provided for the repositories. Generally speaking the in memory repository should be used as a local buffer and then scheduled "exports" can be executed to copy metric values accross to a remote repository for aggregation. There is an Exporter interface to support this and a few implementations dealing with different strategies for storing the results (singly or grouped). Codahale metrics are also supported through the MetricWriter interface. Currently implemented through a naming convention (since Codahale has a fixed object model this makes sense): metrics beginning with "histogram" are Histograms, "timer" for Timers, "meter" for Meters etc. Support for message driven metric consumption and production are provided through a MetricWriterMessageHandler and a MessageChannelMetricWriter. No support yet for pagination in the repositories, or for HATEOAS style HTTP endpoints. --- spring-boot-actuator/pom.xml | 19 ++ .../EndpointAutoConfiguration.java | 6 +- .../MetricFilterAutoConfiguration.java | 2 +- .../MetricRepositoryAutoConfiguration.java | 140 ++++++++++++- .../actuate/endpoint/MetricsEndpoint.java | 2 +- .../boot/actuate/endpoint/PublicMetrics.java | 2 +- .../endpoint/VanillaPublicMetrics.java | 32 +-- .../boot/actuate/metrics/CounterService.java | 14 +- .../boot/actuate/metrics/GaugeService.java | 16 +- .../metrics/InMemoryMetricRepository.java | 93 --------- .../boot/actuate/metrics/Measurement.java | 79 -------- .../boot/actuate/metrics/Metric.java | 95 ++++++--- .../export/AbstractMetricExporter.java | 122 +++++++++++ .../boot/actuate/metrics/export/Exporter.java | 33 +++ .../metrics/export/MetricCopyExporter.java | 55 +++++ .../export/PrefixMetricGroupExporter.java | 77 +++++++ .../metrics/export/RichGaugeExporter.java | 102 ++++++++++ .../actuate/metrics/reader/MetricReader.java | 34 ++++ .../metrics/reader/PrefixMetricReader.java | 30 +++ .../repository/InMemoryMetricRepository.java | 105 ++++++++++ .../metrics/repository/MetricRepository.java | 29 +++ .../repository/MultiMetricRepository.java | 40 ++++ .../redis/RedisMetricRepository.java | 155 ++++++++++++++ .../redis/RedisMultiMetricRepository.java | 151 ++++++++++++++ .../metrics/repository/redis/RedisUtils.java | 33 +++ .../rich/InMemoryRichGaugeRepository.java | 81 ++++++++ .../boot/actuate/metrics/rich/RichGauge.java | 190 ++++++++++++++++++ .../actuate/metrics/rich/RichGaugeReader.java | 32 +++ .../metrics/rich/RichGaugeRepository.java | 28 +++ .../util/SimpleInMemoryRepository.java | 103 ++++++++++ .../metrics/writer/CodahaleMetricWriter.java | 111 ++++++++++ .../metrics/writer/CompositeMetricWriter.java | 65 ++++++ .../{ => writer}/DefaultCounterService.java | 21 +- .../{ => writer}/DefaultGaugeService.java | 23 ++- .../boot/actuate/metrics/writer/Delta.java | 38 ++++ .../writer/MessageChannelMetricWriter.java | 60 ++++++ .../actuate/metrics/writer/MetricWriter.java | 34 ++++ .../writer/MetricWriterMessageHandler.java | 50 +++++ .../MetricFilterAutoConfigurationTests.java | 2 +- ...etricRepositoryAutoConfigurationTests.java | 75 ++++++- .../endpoint/MetricsEndpointTests.java | 8 +- .../endpoint/VanillaPublicMetricsTests.java | 11 +- .../InMemoryMetricRepositoryTests.java | 80 -------- .../boot/actuate/metrics/Iterables.java} | 25 ++- .../export/MetricCopyExporterTests.java | 60 ++++++ .../PrefixMetricGroupExporterTests.java | 66 ++++++ .../export/RichGaugeExporterTests.java | 43 ++++ .../InMemoryMetricRepositoryTests.java | 47 +++++ .../InMemoryPrefixMetricRepositoryTests.java | 83 ++++++++ .../redis/RedisMetricRepositoryTests.java | 91 +++++++++ .../RedisMultiMetricRepositoryTests.java | 84 ++++++++ .../metrics/repository/redis/RedisServer.java | 144 +++++++++++++ .../InMemoryRichGaugeRepositoryTests.java | 39 ++++ .../metrics/util/InMemoryRepositoryTests.java | 131 ++++++++++++ .../writer/CodahaleMetricWriterTests.java | 79 ++++++++ .../DefaultCounterServiceTests.java | 24 ++- .../DefaultGaugeServiceTests.java | 21 +- .../MessageChannelMetricWriterTests.java | 50 +++++ .../cli/compiler/grape/JreProxySelector.java | 2 + spring-boot-dependencies/pom.xml | 21 ++ .../sample/actuator/SampleController.java | 2 + 61 files changed, 3095 insertions(+), 395 deletions(-) delete mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepository.java delete mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Measurement.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/MetricReader.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/PrefixMetricReader.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MetricRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MultiMetricRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisUtils.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGauge.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeReader.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/{ => writer}/DefaultCounterService.java (66%) rename spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/{ => writer}/DefaultGaugeService.java (59%) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/Delta.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriter.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriterMessageHandler.java delete mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepositoryTests.java rename spring-boot-actuator/src/{main/java/org/springframework/boot/actuate/metrics/MetricRepository.java => test/java/org/springframework/boot/actuate/metrics/Iterables.java} (68%) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporterTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporterTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryPrefixMetricRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisServer.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/util/InMemoryRepositoryTests.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriterTests.java rename spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/{ => writer}/DefaultCounterServiceTests.java (54%) rename spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/{ => writer}/DefaultGaugeServiceTests.java (61%) create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriterTests.java diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 82137d735b9..bc302bea7a6 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -41,7 +41,21 @@ org.springframework spring-context + + org.springframework + spring-messaging + + + com.codahale.metrics + metrics-core + true + + + com.lambdaworks + lettuce + true + javax.servlet javax.servlet-api @@ -57,6 +71,11 @@ spring-webmvc true + + org.springframework.data + spring-data-redis + true + org.springframework.security spring-security-web diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java index 3033494544d..95f95acf0fa 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointAutoConfiguration.java @@ -40,8 +40,8 @@ import org.springframework.boot.actuate.endpoint.VanillaPublicMetrics; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.SimpleHealthIndicator; import org.springframework.boot.actuate.health.VanillaHealthIndicator; -import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; -import org.springframework.boot.actuate.metrics.MetricRepository; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.autoconfigure.AutoConfigurationReport; @@ -78,7 +78,7 @@ public class EndpointAutoConfiguration { private InfoPropertiesConfiguration properties; @Autowired(required = false) - private MetricRepository metricRepository = new InMemoryMetricRepository(); + private MetricReader metricRepository = new InMemoryMetricRepository(); @Autowired(required = false) private PublicMetrics metrics; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java index 90ad9e30340..e15f26aee16 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfiguration.java @@ -99,7 +99,7 @@ public class MetricFilterAutoConfiguration { finally { stopWatch.stop(); String gaugeKey = getKey("response" + suffix); - MetricFilterAutoConfiguration.this.gaugeService.set(gaugeKey, + MetricFilterAutoConfiguration.this.gaugeService.submit(gaugeKey, stopWatch.getTotalTimeMillis()); String counterKey = getKey("status." + getStatus(response) + suffix); MetricFilterAutoConfiguration.this.counterService.increment(counterKey); 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 293463e3148..889e7af7402 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,41 +16,159 @@ package org.springframework.boot.actuate.autoconfigure; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.metrics.CounterService; -import org.springframework.boot.actuate.metrics.DefaultCounterService; -import org.springframework.boot.actuate.metrics.DefaultGaugeService; import org.springframework.boot.actuate.metrics.GaugeService; -import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; -import org.springframework.boot.actuate.metrics.MetricRepository; +import org.springframework.boot.actuate.metrics.export.Exporter; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.boot.actuate.metrics.repository.MetricRepository; +import org.springframework.boot.actuate.metrics.writer.CodahaleMetricWriter; +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.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.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; 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 com.codahale.metrics.MetricRegistry; /** - * {@link EnableAutoConfiguration Auto-configuration} for metrics services. + *

+ * {@link EnableAutoConfiguration Auto-configuration} for metrics services. Creates + * user-facing {@link GaugeService} and {@link CounterService} instances, and also back + * end repositories to catch the data pumped into them. + *

+ *

+ * An {@link InMemoryMetricRepository} is always created unless another + * {@link MetricRepository} is already provided by the user. In general, even if metric + * data needs to be stored and analysed remotely, it is recommended to use an in-memory + * repository to buffer metric updates locally. The values can be exported (e.g. on a + * 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. + *

+ *

+ * 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.*"). + *

+ *

+ * 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. + *

+ * + * @see GaugeService + * @see CounterService + * @see MetricWriter + * @see InMemoryMetricRepository + * @see CodahaleMetricWriter + * @see Exporter * * @author Dave Syer */ @Configuration public class MetricRepositoryAutoConfiguration { + @Autowired + private MetricWriter writer; + @Bean @ConditionalOnMissingBean public CounterService counterService() { - return new DefaultCounterService(metricRepository()); + return new DefaultCounterService(this.writer); } @Bean @ConditionalOnMissingBean public GaugeService gaugeService() { - return new DefaultGaugeService(metricRepository()); + return new DefaultGaugeService(this.writer); } - @Bean - @ConditionalOnMissingBean - protected MetricRepository metricRepository() { - return new InMemoryMetricRepository(); + @Configuration + @ConditionalOnMissingBean(MetricRepository.class) + static class MetricRepositoryConfiguration { + + @Bean + public InMemoryMetricRepository metricRepository() { + return new InMemoryMetricRepository(); + } + + } + + @Configuration + @ConditionalOnClass(MessageChannel.class) + static class MetricsChannelConfiguration { + + @Autowired(required = false) + @Qualifier("metricsExecutor") + private Executor executor = Executors.newSingleThreadExecutor(); + + @Bean + @ConditionalOnMissingBean(name = "metricsChannel") + public SubscribableChannel metricsChannel() { + return new ExecutorSubscribableChannel(this.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 CodahaleMetricRegistryConfiguration { + + @Bean + @ConditionalOnMissingBean + public MetricRegistry metricRegistry() { + return new MetricRegistry(); + } + + @Bean + public CodahaleMetricWriter codahaleMetricWriter(MetricRegistry metricRegistry) { + return new CodahaleMetricWriter(metricRegistry); + } + + @Bean + @Primary + @ConditionalOnMissingClass(name = "org.springframework.messaging.MessageChannel") + @ConditionalOnMissingBean(name = "primaryMetricWriter") + public MetricWriter primaryMetricWriter(List writers) { + return new CompositeMetricWriter(writers); + } + } } 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 bd41c270ffa..286f4e61baa 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 @@ -47,7 +47,7 @@ public class MetricsEndpoint extends AbstractEndpoint> { @Override public Map invoke() { Map result = new LinkedHashMap(); - for (Metric metric : this.metrics.metrics()) { + for (Metric metric : this.metrics.metrics()) { result.put(metric.getName(), metric.getValue()); } return result; diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/PublicMetrics.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/PublicMetrics.java index 82ad4728003..26db0fe4709 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/PublicMetrics.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/PublicMetrics.java @@ -31,6 +31,6 @@ public interface PublicMetrics { /** * @return an indication of current state through metrics */ - Collection metrics(); + Collection> metrics(); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetrics.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetrics.java index 1520256252c..37c4e1daa81 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetrics.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetrics.java @@ -20,32 +20,36 @@ import java.util.Collection; import java.util.LinkedHashSet; import org.springframework.boot.actuate.metrics.Metric; -import org.springframework.boot.actuate.metrics.MetricRepository; +import org.springframework.boot.actuate.metrics.reader.MetricReader; import org.springframework.util.Assert; /** - * Default implementation of {@link PublicMetrics} that exposes all metrics from the - * {@link MetricRepository} along with memory information. + * Default implementation of {@link PublicMetrics} that exposes all metrics from a + * {@link MetricReader} along with memory information. * * @author Dave Syer */ public class VanillaPublicMetrics implements PublicMetrics { - private MetricRepository metricRepository; + private MetricReader reader; - public VanillaPublicMetrics(MetricRepository metricRepository) { - Assert.notNull(metricRepository, "MetricRepository must not be null"); - this.metricRepository = metricRepository; + public VanillaPublicMetrics(MetricReader reader) { + Assert.notNull(reader, "MetricReader must not be null"); + this.reader = reader; } @Override - public Collection metrics() { - Collection result = new LinkedHashSet( - this.metricRepository.findAll()); - result.add(new Metric("mem", new Long(Runtime.getRuntime().totalMemory()) / 1024)); - result.add(new Metric("mem.free", - new Long(Runtime.getRuntime().freeMemory()) / 1024)); - result.add(new Metric("processors", Runtime.getRuntime().availableProcessors())); + public Collection> metrics() { + Collection> result = new LinkedHashSet>(); + for (Metric metric : this.reader.findAll()) { + result.add(metric); + } + result.add(new Metric("mem", + new Long(Runtime.getRuntime().totalMemory()) / 1024)); + result.add(new Metric("mem.free", new Long(Runtime.getRuntime() + .freeMemory()) / 1024)); + result.add(new Metric("processors", Runtime.getRuntime() + .availableProcessors())); return result; } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/CounterService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/CounterService.java index f550dc06b70..75d84660387 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/CounterService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/CounterService.java @@ -17,27 +17,27 @@ package org.springframework.boot.actuate.metrics; /** - * A service that can be used to increment, decrement and reset a {@link Metric}. + * A service that can be used to increment, decrement and reset a named counter value. * * @author Dave Syer */ public interface CounterService { /** - * Increment the specified metric by 1. - * @param metricName the name of the metric + * Increment the specified counter by 1. + * @param metricName the name of the counter */ void increment(String metricName); /** - * Decrement the specified metric by 1. - * @param metricName the name of the metric + * Decrement the specified counter by 1. + * @param metricName the name of the counter */ void decrement(String metricName); /** - * Reset the specified metric to 0. - * @param metricName the name of the metric + * Reset the specified counter. + * @param metricName the name of the counter */ void reset(String metricName); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/GaugeService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/GaugeService.java index 0295c8514d3..fff5d534b39 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/GaugeService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/GaugeService.java @@ -17,17 +17,23 @@ package org.springframework.boot.actuate.metrics; /** - * A service that can be used to manage a {@link Metric} as a gauge. + * A service that can be used to submit a named double value for storage and analysis. Any + * statistics or analysis that needs to be carried out is best left for other concerns, + * but ultimately they are under control of the implementation of this service. For + * instance, the value submitted here could be a method execution timing result, and it + * would go to a backend that keeps a histogram of recent values for comparison purposes. + * Or it could be a simple measurement of a sensor value (like a temperature reading) to + * be passed on to a monitoring system in its raw form. * * @author Dave Syer */ public interface GaugeService { /** - * Set the specified metric value - * @param metricName the metric to set - * @param value the value of the metric + * Set the specified gauge value + * @param metricName the name of the gauge to set + * @param value the value of the gauge */ - void set(String metricName, double value); + void submit(String metricName, double value); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepository.java deleted file mode 100644 index 9c3d638e4b5..00000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepository.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.metrics; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * {@link MetricRepository} implementation that stores metric in-memory. - * - * @author Dave Syer - */ -public class InMemoryMetricRepository implements MetricRepository { - - private ConcurrentMap metrics = new ConcurrentHashMap(); - - private ConcurrentMap locks = new ConcurrentHashMap(); - - @Override - public void increment(String metricName, int amount, Date timestamp) { - Object lock = this.locks.putIfAbsent(metricName, new Object()); - if (lock == null) { - lock = this.locks.get(metricName); - } - synchronized (lock) { - Measurement current = this.metrics.get(metricName); - if (current != null) { - Metric metric = current.getMetric(); - this.metrics.replace(metricName, current, new Measurement(timestamp, - metric.increment(amount))); - return; - } - else { - this.metrics.putIfAbsent(metricName, new Measurement(timestamp, - new Metric(metricName, amount))); - } - } - } - - @Override - public void set(String metricName, double value, Date timestamp) { - Measurement current = this.metrics.get(metricName); - if (current != null) { - Metric metric = current.getMetric(); - this.metrics.replace(metricName, current, - new Measurement(timestamp, metric.set(value))); - } - else { - this.metrics.putIfAbsent(metricName, new Measurement(timestamp, new Metric( - metricName, value))); - } - } - - @Override - public void delete(String metricName) { - this.metrics.remove(metricName); - } - - @Override - public Metric findOne(String metricName) { - if (this.metrics.containsKey(metricName)) { - return this.metrics.get(metricName).getMetric(); - } - return new Metric(metricName, 0); - } - - @Override - public Collection findAll() { - ArrayList result = new ArrayList(); - for (Measurement measurement : this.metrics.values()) { - result.add(measurement.getMetric()); - } - return result; - } - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Measurement.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Measurement.java deleted file mode 100644 index 206e6f390d4..00000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Measurement.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.metrics; - -import java.util.Date; - -import org.springframework.util.ObjectUtils; - -/** - * A {@link Metric} at a given point in time. - * - * @author Dave Syer - */ -public final class Measurement { - - private Date timestamp; - - private Metric metric; - - public Measurement(Date timestamp, Metric metric) { - this.timestamp = timestamp; - this.metric = metric; - } - - public Date getTimestamp() { - return this.timestamp; - } - - public Metric getMetric() { - return this.metric; - } - - @Override - public String toString() { - return "Measurement [dateTime=" + this.timestamp + ", metric=" + this.metric - + "]"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ObjectUtils.nullSafeHashCode(this.timestamp); - result = prime * result + ObjectUtils.nullSafeHashCode(this.metric); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() == obj.getClass()) { - Measurement other = (Measurement) obj; - boolean result = ObjectUtils.nullSafeEquals(this.timestamp, other.timestamp); - result &= ObjectUtils.nullSafeEquals(this.metric, other.metric); - return result; - } - return super.equals(obj); - } - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Metric.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Metric.java index 245dee71510..f880bc34347 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Metric.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/Metric.java @@ -16,33 +16,45 @@ package org.springframework.boot.actuate.metrics; +import java.util.Date; + import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** - * Immutable class that can be used to hold any arbitrary system measurement value. For - * example a metric might record the number of active connections. + * Immutable class that can be used to hold any arbitrary system measurement value (a + * named numeric value with a timestamp). For example a metric might record the number of + * active connections to a server, or the temperature of a meeting room. * * @author Dave Syer - * @see MetricRepository - * @see CounterService */ -public final class Metric { +public class Metric { private final String name; - private final double value; + private final T value; + + private Date timestamp; + + /** + * Create a new {@link Metric} instance for the current time. + * @param name the name of the metric + * @param value the value of the metric + */ + public Metric(String name, T value) { + this(name, value, new Date()); + } /** * Create a new {@link Metric} instance. * @param name the name of the metric * @param value the value of the metric + * @param timestamp the timestamp for the metric */ - public Metric(String name, double value) { - super(); + public Metric(String name, T value, Date timestamp) { Assert.notNull(name, "Name must not be null"); this.name = name; this.value = value; + this.timestamp = timestamp; } /** @@ -55,17 +67,28 @@ public final class Metric { /** * Returns the value of the metric. */ - public double getValue() { + public T getValue() { return this.value; } + public Date getTimestamp() { + return this.timestamp; + } + + @Override + public String toString() { + return "Metric [name=" + this.name + ", value=" + this.value + ", timestamp=" + + this.timestamp + "]"; + } + /** * Create a new {@link Metric} with an incremented value. * @param amount the amount that the new metric will differ from this one * @return a new {@link Metric} instance */ - public Metric increment(int amount) { - return new Metric(this.name, new Double(((int) this.value) + amount)); + public Metric increment(int amount) { + return new Metric(this.getName(), new Long(this.getValue().longValue() + + amount)); } /** @@ -73,41 +96,49 @@ public final class Metric { * @param value the value of the new metric * @return a new {@link Metric} instance */ - public Metric set(double value) { - return new Metric(this.name, value); - } - - @Override - public String toString() { - return "Metric [name=" + this.name + ", value=" + this.value + "]"; + public Metric set(S value) { + return new Metric(this.getName(), value); } @Override public int hashCode() { - int valueHashCode = ObjectUtils.hashCode(this.value); final int prime = 31; int result = 1; - result = prime * result + ObjectUtils.nullSafeHashCode(this.name); - result = prime * result + (valueHashCode ^ (valueHashCode >>> 32)); + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + result = prime * result + + ((this.timestamp == null) ? 0 : this.timestamp.hashCode()); + result = prime * result + ((this.value == null) ? 0 : this.value.hashCode()); return result; } @Override public boolean equals(Object obj) { - if (this == obj) { + if (this == obj) return true; - } - if (obj == null) { + if (obj == null) return false; + if (getClass() != obj.getClass()) + return false; + Metric other = (Metric) obj; + if (this.name == null) { + if (other.name != null) + return false; } - if (getClass() == obj.getClass()) { - Metric other = (Metric) obj; - boolean result = ObjectUtils.nullSafeEquals(this.name, other.name); - result &= Double.doubleToLongBits(this.value) == Double - .doubleToLongBits(other.value); - return result; + else if (!this.name.equals(other.name)) + return false; + if (this.timestamp == null) { + if (other.timestamp != null) + return false; } - return super.equals(obj); + else if (!this.timestamp.equals(other.timestamp)) + return false; + if (this.value == null) { + if (other.value != null) + return false; + } + else if (!this.value.equals(other.value)) + return false; + return true; } } 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 new file mode 100644 index 00000000000..42a454c3659 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/AbstractMetricExporter.java @@ -0,0 +1,122 @@ +/* + * 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.metrics.export; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.util.StringUtils; + +/** + * Base class for metric exporters that have common features, principally a prefix for + * exported metrics and filtering by timestamp (so only new values are included in the + * export). + * + * @author Dave Syer + */ +public abstract class AbstractMetricExporter implements Exporter { + + private volatile AtomicBoolean processing = new AtomicBoolean(false); + + private Date earliestTimestamp = new Date(); + + private boolean ignoreTimestamps = false; + + private final String prefix; + + public AbstractMetricExporter(String prefix) { + this.prefix = !StringUtils.hasText(prefix) ? "" : (prefix.endsWith(".") ? prefix + : prefix + "."); + } + + /** + * The earliest time for which data will be exported. + * + * @param earliestTimestamp the timestamp to set + */ + public void setEarliestTimestamp(Date earliestTimestamp) { + this.earliestTimestamp = earliestTimestamp; + } + + /** + * Ignore timestamps (export all metrics). + * + * @param ignoreTimestamps the flag to set + */ + public void setIgnoreTimestamps(boolean ignoreTimestamps) { + this.ignoreTimestamps = ignoreTimestamps; + } + + @Override + public void export() { + if (!this.processing.compareAndSet(false, true)) { + // skip a tick + return; + } + try { + for (String group : groups()) { + Collection> values = new ArrayList>(); + for (Metric metric : next(group)) { + Metric value = new Metric(this.prefix + metric.getName(), + metric.getValue(), metric.getTimestamp()); + Date timestamp = metric.getTimestamp(); + if (!this.ignoreTimestamps && this.earliestTimestamp.after(timestamp)) { + continue; + } + values.add(value); + } + write(group, values); + } + } + finally { + this.processing.set(false); + } + } + + /** + * 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 + * String, subclasses should override this method. Otherwise the default should be + * fine (iteration over all metrics). + * + * @return groups of metrics to iterate over (default singleton empty string) + */ + protected Iterable groups() { + return Collections.singleton(""); + } + + /** + * Write the values associated with a group. + * + * @param group the group to write + * @param values the values to write + */ + protected abstract void write(String group, Collection> values); + + /** + * Get the next group of metrics to write. + * + * @param group the group name to write + * @return some metrics to write + */ + protected abstract Iterable> next(String group); + +} \ No newline at end of file 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 new file mode 100644 index 00000000000..a3ea4a38141 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/Exporter.java @@ -0,0 +1,33 @@ +/* + * 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.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 + * of this interface. You might for instance create an instance of an Exporter and trigger + * it using a @Scheduled annotation in a Spring ApplicationContext. + * + * @author Dave Syer + */ +public interface Exporter { + + void export(); + +} 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 new file mode 100644 index 00000000000..34858360dc8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporter.java @@ -0,0 +1,55 @@ +/* + * 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.metrics.export; + +import java.util.Collection; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * @author Dave Syer + */ +public class MetricCopyExporter extends AbstractMetricExporter { + + private final MetricReader reader; + private final MetricWriter writer; + + public MetricCopyExporter(MetricReader reader, MetricWriter writer) { + this(reader, writer, ""); + } + + public MetricCopyExporter(MetricReader reader, MetricWriter writer, String prefix) { + super(prefix); + this.reader = reader; + this.writer = writer; + } + + @Override + protected Iterable> next(String group) { + return this.reader.findAll(); + } + + @Override + protected void write(String group, Collection> values) { + for (Metric value : values) { + this.writer.set(value); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporter.java new file mode 100644 index 00000000000..cd2ef1a0b59 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporter.java @@ -0,0 +1,77 @@ +/* + * 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.metrics.export; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.PrefixMetricReader; +import org.springframework.boot.actuate.metrics.repository.MultiMetricRepository; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * @author Dave Syer + */ +public class PrefixMetricGroupExporter extends AbstractMetricExporter { + + private final PrefixMetricReader reader; + private final MetricWriter writer; + private Set groups = new HashSet(); + + public PrefixMetricGroupExporter(PrefixMetricReader reader, MetricWriter writer) { + this(reader, writer, ""); + } + + public PrefixMetricGroupExporter(PrefixMetricReader reader, MetricWriter writer, + String prefix) { + super(prefix); + this.reader = reader; + this.writer = writer; + } + + /** + * @param groups the groups to set + */ + public void setGroups(Set groups) { + this.groups = groups; + } + + @Override + protected Iterable> next(String group) { + return this.reader.findAll(group); + } + + @Override + protected Iterable groups() { + return this.groups; + } + + @Override + protected void write(String group, Collection> values) { + if (this.writer instanceof MultiMetricRepository && !values.isEmpty()) { + ((MultiMetricRepository) this.writer).save(group, values); + } + else { + for (Metric value : values) { + this.writer.set(value); + } + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporter.java new file mode 100644 index 00000000000..550e019019c --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporter.java @@ -0,0 +1,102 @@ +/* + * 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.metrics.export; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.MultiMetricRepository; +import org.springframework.boot.actuate.metrics.rich.RichGauge; +import org.springframework.boot.actuate.metrics.rich.RichGaugeReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * Exporter or converter for {@link RichGauge} data to a metric-based back end. Each gauge + * measurement is stored as a set of related metrics with a common prefix (the name of the + * gauge), and suffixes that describe the data. For example, a gauge called + * foo is stored as + * [foo.min, foo.max. foo.val, foo.count, foo.avg, foo.alpha]. If the + * {@link MetricWriter} provided is a {@link MultiMetricRepository} then the values for a + * gauge will be stored as a group, and hence will be retrievable from the repository in a + * single query (or optionally individually). + * + * @author Dave Syer + */ +public class RichGaugeExporter extends AbstractMetricExporter { + + private static final String MIN = ".min"; + + private static final String MAX = ".max"; + + private static final String COUNT = ".count"; + + private static final String VALUE = ".val"; + + private static final String AVG = ".avg"; + + private static final String ALPHA = ".alpha"; + + private final RichGaugeReader reader; + private final MetricWriter writer; + + public RichGaugeExporter(RichGaugeReader reader, MetricWriter writer) { + this(reader, writer, ""); + } + + public RichGaugeExporter(RichGaugeReader reader, MetricWriter writer, String prefix) { + super(prefix); + this.reader = reader; + this.writer = writer; + } + + @Override + protected Iterable> next(String group) { + RichGauge rich = this.reader.findOne(group); + Collection> metrics = new ArrayList>(); + metrics.add(new Metric(group + MIN, rich.getMin())); + metrics.add(new Metric(group + MAX, rich.getMax())); + metrics.add(new Metric(group + COUNT, rich.getCount())); + metrics.add(new Metric(group + VALUE, rich.getValue())); + metrics.add(new Metric(group + AVG, rich.getAverage())); + metrics.add(new Metric(group + ALPHA, rich.getAlpha())); + return metrics; + } + + @Override + protected Iterable groups() { + Collection names = new HashSet(); + for (RichGauge rich : this.reader.findAll()) { + names.add(rich.getName()); + } + return names; + } + + @Override + protected void write(String group, Collection> values) { + if (this.writer instanceof MultiMetricRepository) { + ((MultiMetricRepository) this.writer).save(group, values); + } + else { + for (Metric value : values) { + this.writer.set(value); + } + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/MetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/MetricReader.java new file mode 100644 index 00000000000..a54f437f34a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/MetricReader.java @@ -0,0 +1,34 @@ +/* + * 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.metrics.reader; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * A simple reader interface used to interrogate {@link Metric}s. + * + * @author Dave Syer + */ +public interface MetricReader { + + Metric findOne(String metricName); + + Iterable> findAll(); + + long count(); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/PrefixMetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/PrefixMetricReader.java new file mode 100644 index 00000000000..71e366306f9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/reader/PrefixMetricReader.java @@ -0,0 +1,30 @@ +/* + * 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.metrics.reader; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * Interface for extracting metrics as a group whose name starts with a prefix. + * + * @author Dave Syer + */ +public interface PrefixMetricReader { + + Iterable> findAll(String prefix); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepository.java new file mode 100644 index 00000000000..360b89d04c7 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepository.java @@ -0,0 +1,105 @@ +/* + * 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.metrics.repository; + +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.PrefixMetricReader; +import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository; +import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository.Callback; +import org.springframework.boot.actuate.metrics.writer.Delta; + +/** + * {@link MetricRepository} and {@link MultiMetricRepository} implementation that stores + * metrics in memory. + * + * @author Dave Syer + */ +public class InMemoryMetricRepository implements MetricRepository, MultiMetricRepository, + PrefixMetricReader { + + private SimpleInMemoryRepository> metrics = new SimpleInMemoryRepository>(); + private Collection groups = new HashSet(); + + @Override + public void increment(Delta delta) { + final String metricName = delta.getName(); + final int amount = delta.getValue().intValue(); + final Date timestamp = delta.getTimestamp(); + this.metrics.update(metricName, new Callback>() { + @Override + public Metric modify(Metric current) { + if (current != null) { + Metric metric = current; + return new Metric(metricName, metric.increment(amount) + .getValue(), timestamp); + } + else { + return new Metric(metricName, new Long(amount), timestamp); + } + } + }); + } + + @Override + public void set(Metric value) { + this.metrics.set(value.getName(), value); + } + + @Override + public void save(String group, Collection> values) { + for (Metric metric : values) { + set(metric); + } + this.groups.add(group); + } + + @Override + public Iterable groups() { + return Collections.unmodifiableCollection(this.groups); + } + + @Override + public long count() { + return this.metrics.count(); + } + + @Override + public void reset(String metricName) { + this.metrics.remove(metricName); + } + + @Override + public Metric findOne(String metricName) { + return this.metrics.findOne(metricName); + } + + @Override + public Iterable> findAll() { + return this.metrics.findAll(); + } + + @Override + public Iterable> findAll(String metricNamePrefix) { + return this.metrics.findAllWithPrefix(metricNamePrefix); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MetricRepository.java new file mode 100644 index 00000000000..07ab38cda5d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MetricRepository.java @@ -0,0 +1,29 @@ +/* + * 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.metrics.repository; + +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * Convenient combination of reader and writer concerns. + * + * @author Dave Syer + */ +public interface MetricRepository extends MetricReader, MetricWriter { + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MultiMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MultiMetricRepository.java new file mode 100644 index 00000000000..8838a98a56e --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/MultiMetricRepository.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 org.springframework.boot.actuate.metrics.repository; + +import java.util.Collection; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.PrefixMetricReader; + +/** + * A repository for metrics that allows efficient storage and retrieval of groups of + * metrics with a common name prefix (their group name). + * + * @author Dave Syer + */ +public interface MultiMetricRepository extends PrefixMetricReader { + + void save(String group, Collection> values); + + void reset(String group); + + Iterable groups(); + + long count(); + +} 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 new file mode 100644 index 00000000000..bc2cc1df225 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepository.java @@ -0,0 +1,155 @@ +/* + * 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.metrics.repository.redis; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.MetricRepository; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.BoundZSetOperations; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.util.Assert; + +/** + * A {@link MetricRepository} implementation for a redis backend. Metric values are stored + * as regular hash values against a key composed of the metric name prefixed with a + * constant (default "spring.metrics."). + * + * @author Dave Syer + */ +public class RedisMetricRepository implements MetricRepository { + + private static final String DEFAULT_METRICS_PREFIX = "spring.metrics."; + + private String prefix = DEFAULT_METRICS_PREFIX; + + private String keys = this.prefix + "keys"; + + private BoundZSetOperations zSetOperations; + + private RedisOperations redisOperations; + + private ValueOperations longOperations; + + public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory) { + Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null"); + this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory); + RedisTemplate longRedisTemplate = RedisUtils.createRedisTemplate( + redisConnectionFactory, Long.class); + this.longOperations = longRedisTemplate.opsForValue(); + this.zSetOperations = this.redisOperations.boundZSetOps(this.keys); + } + + /** + * The prefix for all metrics keys. + * + * @param prefix the prefix to set for all metrics keys + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + this.keys = this.prefix + "keys"; + } + + @Override + public Metric findOne(String metricName) { + String redisKey = keyFor(metricName); + String raw = this.redisOperations.opsForValue().get(redisKey); + if (raw != null) { + return deserialize(redisKey, raw); + } + else { + return null; + } + } + + @Override + public Iterable> findAll() { + + // This set is sorted + Set keys = this.zSetOperations.range(0, -1); + Iterator keysIt = keys.iterator(); + + List> result = new ArrayList>(keys.size()); + List values = this.redisOperations.opsForValue().multiGet(keys); + for (String v : values) { + result.add(deserialize(keysIt.next(), v)); + } + return result; + + } + + @Override + public long count() { + return this.zSetOperations.size(); + } + + @Override + public void increment(Delta delta) { + String name = delta.getName(); + String key = keyFor(name); + trackMembership(key); + this.longOperations.increment(key, delta.getValue().longValue()); + } + + @Override + public void set(Metric value) { + String raw = serialize(value); + String name = value.getName(); + String key = keyFor(name); + trackMembership(key); + this.redisOperations.opsForValue().set(key, raw); + } + + @Override + public void reset(String metricName) { + String key = keyFor(metricName); + if (this.zSetOperations.remove(key) == 1) { + this.redisOperations.delete(key); + } + } + + // TODO: memorize timestamps as well? + private Metric deserialize(String redisKey, String v) { + return new Metric(nameFor(redisKey), Double.valueOf(v)); + } + + private String serialize(Metric entity) { + return String.valueOf(entity.getValue()); + } + + private String keyFor(String name) { + return this.prefix + name; + } + + private String nameFor(String redisKey) { + Assert.state(redisKey != null && redisKey.startsWith(this.prefix), + "Invalid key does not start with prefix: " + redisKey); + return redisKey.substring(this.prefix.length()); + } + + private void trackMembership(String redisKey) { + this.zSetOperations.add(redisKey, 0.0D); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepository.java new file mode 100644 index 00000000000..1d1473adef8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepository.java @@ -0,0 +1,151 @@ +/* + * 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.metrics.repository.redis; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.MultiMetricRepository; +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; + +/** + * {@link MultiMetricRepository} implementation backed by a redis store. Metric values are + * stored as regular values against a key composed of the group name prefixed with a + * constant prefix (default "spring.groups."). The group names are stored as a zset under + * [prefix] + "keys". + * + * @author Dave Syer + */ +public class RedisMultiMetricRepository implements MultiMetricRepository { + + private static final String DEFAULT_METRICS_PREFIX = "spring.groups."; + + private String prefix = DEFAULT_METRICS_PREFIX; + private String keys = this.prefix + "keys"; + + private BoundZSetOperations zSetOperations; + + private RedisOperations redisOperations; + + public RedisMultiMetricRepository(RedisConnectionFactory redisConnectionFactory) { + Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null"); + this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory); + this.zSetOperations = this.redisOperations.boundZSetOps(this.keys); + } + + /** + * The prefix for all metrics keys. + * + * @param prefix the prefix to set for all metrics keys + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + this.keys = this.prefix + "keys"; + } + + @Override + public Iterable> findAll(String metricNamePrefix) { + + BoundZSetOperations zSetOperations = this.redisOperations + .boundZSetOps(keyFor(metricNamePrefix)); + + Set keys = zSetOperations.range(0, -1); + Iterator keysIt = keys.iterator(); + + List> result = new ArrayList>(keys.size()); + List values = this.redisOperations.opsForValue().multiGet(keys); + for (String v : values) { + result.add(deserialize(keysIt.next(), v)); + } + return result; + + } + + @Override + public void save(String group, Collection> values) { + String groupKey = keyFor(group); + trackMembership(groupKey); + BoundZSetOperations zSetOperations = this.redisOperations + .boundZSetOps(groupKey); + for (Metric metric : values) { + String raw = serialize(metric); + String key = keyFor(metric.getName()); + zSetOperations.add(key, 0.0D); + this.redisOperations.opsForValue().set(key, raw); + } + } + + @Override + public Iterable groups() { + Set range = this.zSetOperations.range(0, -1); + Collection result = new ArrayList(); + for (String key : range) { + result.add(nameFor(key)); + } + return range; + } + + @Override + public long count() { + return this.zSetOperations.size(); + } + + @Override + public void reset(String group) { + String groupKey = keyFor(group); + if (this.redisOperations.hasKey(groupKey)) { + BoundZSetOperations zSetOperations = this.redisOperations + .boundZSetOps(groupKey); + Set keys = zSetOperations.range(0, -1); + for (String key : keys) { + this.redisOperations.delete(key); + } + this.redisOperations.delete(groupKey); + } + this.zSetOperations.remove(groupKey); + } + + private Metric deserialize(String redisKey, String v) { + return new Metric(nameFor(redisKey), Double.valueOf(v)); + } + + private String serialize(Metric entity) { + return String.valueOf(entity.getValue()); + } + + private String keyFor(String name) { + return this.prefix + name; + } + + private String nameFor(String redisKey) { + Assert.state(redisKey != null && redisKey.startsWith(this.prefix), + "Invalid key does not start with prefix: " + redisKey); + return redisKey.substring(this.prefix.length()); + } + + private void trackMembership(String redisKey) { + this.zSetOperations.add(redisKey, 0.0D); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisUtils.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisUtils.java new file mode 100644 index 00000000000..40763e47bbd --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/repository/redis/RedisUtils.java @@ -0,0 +1,33 @@ +package org.springframework.boot.actuate.metrics.repository.redis; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author Luke Taylor + */ +class RedisUtils { + + static RedisTemplate createRedisTemplate( + RedisConnectionFactory connectionFactory, Class valueClass) { + RedisTemplate redisTemplate = new RedisTemplate(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericToStringSerializer(valueClass)); + + // avoids proxy + redisTemplate.setExposeConnection(true); + + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } + + static RedisOperations stringTemplate( + RedisConnectionFactory redisConnectionFactory) { + return new StringRedisTemplate(redisConnectionFactory); + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepository.java new file mode 100644 index 00000000000..3c09421f0f8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepository.java @@ -0,0 +1,81 @@ +/* + * 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.metrics.rich; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository; +import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository.Callback; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * In memory implementation of {@link MetricWriter} and {@link RichGaugeReader}. When you + * set a metric value (using {@link MetricWriter#set(Metric)}) it is used to update a rich + * gauge (increment is a no-op). Gauge values can then be read out using the reader + * operations. + * + * @author Dave Syer + */ +public class InMemoryRichGaugeRepository implements RichGaugeRepository { + + private SimpleInMemoryRepository repository = new SimpleInMemoryRepository(); + + @Override + public void increment(Delta delta) { + // No-op + } + + @Override + public void set(Metric metric) { + + final String name = metric.getName(); + final double value = metric.getValue().doubleValue(); + this.repository.update(name, new Callback() { + @Override + public RichGauge modify(RichGauge current) { + if (current == null) { + current = new RichGauge(name, value); + } + else { + current.set(value); + } + return current; + } + }); + + } + + @Override + public void reset(String metricName) { + this.repository.remove(metricName); + } + + @Override + public RichGauge findOne(String metricName) { + return this.repository.findOne(metricName); + } + + @Override + public Iterable findAll() { + return this.repository.findAll(); + } + + @Override + public long count() { + return this.repository.count(); + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGauge.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGauge.java new file mode 100644 index 00000000000..211fbc4f6f6 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGauge.java @@ -0,0 +1,190 @@ +package org.springframework.boot.actuate.metrics.rich; + +import org.springframework.util.Assert; + +/** + * A gauge which stores the maximum, minimum and average in addition to the current value. + *

+ * The value of the average will depend on whether a weight ('alpha') is set for the + * gauge. If it is unset, the average will contain a simple arithmetic mean. If a weight + * is set, an exponential moving average will be calculated as defined in this NIST + * document. + * + * @author Luke Taylor + */ +public final class RichGauge { + + private final String name; + + private double value; + + private double average; + + private double max; + + private double min; + + private long count; + + private double alpha; + + /** + * Creates an "empty" gauge. + * + * The average, max and min will be zero, but this initial value will not be included + * after the first value has been set on the gauge. + * + * @param name the name under which the gauge will be stored. + */ + public RichGauge(String name) { + this(name, 0.0); + this.count = 0; + } + + public RichGauge(String name, double value) { + Assert.notNull(name, "The gauge name cannot be null or empty"); + this.name = name; + this.value = value; + this.average = this.value; + this.min = this.value; + this.max = this.value; + this.alpha = -1.0; + this.count = 1; + } + + public RichGauge(String name, double value, double alpha, double mean, double max, + double min, long count) { + this.name = name; + this.value = value; + this.alpha = alpha; + this.average = mean; + this.max = max; + this.min = min; + this.count = count; + } + + /** + * @return the name of the gauge + */ + public String getName() { + return this.name; + } + + /** + * @return the current value + */ + public double getValue() { + return this.value; + } + + /** + * Either an exponential weighted moving average or a simple mean, respectively, + * depending on whether the weight 'alpha' has been set for this gauge. + * + * @return The average over all the accumulated values + */ + public double getAverage() { + return this.average; + } + + /** + * @return the maximum value + */ + public double getMax() { + return this.max; + } + + /** + * @return the minimum value + */ + public double getMin() { + return this.min; + } + + /** + * @return Number of times the value has been set. + */ + public long getCount() { + return this.count; + } + + /** + * @return the smoothing constant value. + */ + public double getAlpha() { + return this.alpha; + } + + public RichGauge setAlpha(double alpha) { + Assert.isTrue(alpha == -1 || (alpha > 0.0 && alpha < 1.0), + "Smoothing constant must be between 0 and 1, or -1 to use arithmetic mean"); + this.alpha = alpha; + return this; + } + + RichGauge set(double value) { + if (this.count == 0) { + this.max = value; + this.min = value; + } + else if (value > this.max) { + this.max = value; + } + else if (value < this.min) { + this.min = value; + } + + if (this.alpha > 0.0 && this.count > 0) { + this.average = this.alpha * this.value + (1 - this.alpha) * this.average; + } + else { + double sum = this.average * this.count; + sum += value; + this.average = sum / (this.count + 1); + } + this.count++; + this.value = value; + return this; + } + + RichGauge reset() { + this.value = 0.0; + this.max = 0.0; + this.min = 0.0; + this.average = 0.0; + this.count = 0; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + RichGauge richGauge = (RichGauge) o; + + if (!this.name.equals(richGauge.name)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public String toString() { + return "Gauge [name = " + this.name + ", value = " + this.value + ", alpha = " + + this.alpha + ", average = " + this.average + ", max = " + this.max + + ", min = " + this.min + ", count = " + this.count + "]"; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeReader.java new file mode 100644 index 00000000000..c1a5d80b8d9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeReader.java @@ -0,0 +1,32 @@ +/* + * 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.metrics.rich; + +/** + * A basic set of read operations for {@link RichGauge} instances. + * + * @author Dave Syer + */ +public interface RichGaugeReader { + + RichGauge findOne(String name); + + Iterable findAll(); + + long count(); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeRepository.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeRepository.java new file mode 100644 index 00000000000..7af85e9b199 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/rich/RichGaugeRepository.java @@ -0,0 +1,28 @@ +/* + * 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.metrics.rich; + +import org.springframework.boot.actuate.metrics.writer.MetricWriter; + +/** + * Convenient combination of reader and writer concerns for {@link RichGauge} instances. + * + * @author Dave Syer + */ +public interface RichGaugeRepository extends RichGaugeReader, MetricWriter { + +} 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 new file mode 100644 index 00000000000..a434a0b2e84 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/util/SimpleInMemoryRepository.java @@ -0,0 +1,103 @@ +/* + * 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.metrics.util; + +import java.util.ArrayList; +import java.util.NavigableMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; + +/** + * Repository utility that stores stuff in memory with period-separated String keys. + * + * @author Dave Syer + */ +public class SimpleInMemoryRepository { + + private ConcurrentNavigableMap values = new ConcurrentSkipListMap(); + + private ConcurrentMap locks = new ConcurrentHashMap(); + + public static interface Callback { + T modify(T current); + } + + public T update(String name, Callback callback) { + Object lock = this.locks.putIfAbsent(name, new Object()); + if (lock == null) { + lock = this.locks.get(name); + } + 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); + } + } + + public void set(String name, T value) { + T current = this.values.get(name); + if (current != null) { + this.values.replace(name, current, value); + } + else { + this.values.putIfAbsent(name, value); + } + } + + public long count() { + return this.values.size(); + } + + public void remove(String name) { + this.values.remove(name); + } + + public T findOne(String name) { + if (this.values.containsKey(name)) { + return this.values.get(name); + } + return null; + } + + public Iterable findAll() { + return new ArrayList(this.values.values()); + } + + public Iterable findAllWithPrefix(String prefix) { + if (prefix.endsWith(".*")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + if (!prefix.endsWith(".")) { + prefix = prefix + "."; + } + return new ArrayList(this.values.subMap(prefix, false, prefix + "~", true) + .values()); + } + + protected NavigableMap getValues() { + return this.values; + } + +} 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 new file mode 100644 index 00000000000..47c5f9d4e25 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriter.java @@ -0,0 +1,111 @@ +/* + * 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.metrics.writer; + +import java.util.concurrent.TimeUnit; + +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 + */ +public class CodahaleMetricWriter implements MetricWriter { + + private final MetricRegistry registry; + + /** + * @param registry + */ + public CodahaleMetricWriter(MetricRegistry registry) { + this.registry = registry; + } + + @Override + public void increment(Delta delta) { + String name = delta.getName(); + long value = delta.getValue().longValue(); + if (name.startsWith("meter")) { + Meter meter = this.registry.meter(name); + meter.mark(value); + } + else { + Counter counter = this.registry.counter(name); + counter.inc(value); + } + } + + @Override + public void set(Metric value) { + String name = value.getName(); + if (name.startsWith("histogram")) { + long longValue = value.getValue().longValue(); + Histogram metric = this.registry.histogram(name); + metric.update(longValue); + } + else if (name.startsWith("timer")) { + long longValue = value.getValue().longValue(); + Timer metric = this.registry.timer(name); + metric.update(longValue, TimeUnit.MILLISECONDS); + } + else { + final double gauge = value.getValue().doubleValue(); + this.registry.remove(name); + this.registry.register(name, new SimpleGauge(gauge)); + } + } + + @Override + public void reset(String metricName) { + this.registry.remove(metricName); + } + + private static class SimpleGauge implements Gauge { + + private final double gauge; + + private SimpleGauge(double gauge) { + this.gauge = gauge; + } + + @Override + public Double getValue() { + return this.gauge; + } + } + +} 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 new file mode 100644 index 00000000000..881695a8fa2 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/CompositeMetricWriter.java @@ -0,0 +1,65 @@ +/* + * 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.metrics.writer; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * Composite implementation of {@link MetricWriter} that just sends its input to all of + * the delegates that have been registered. + * + * @author Dave Syer + */ +public class CompositeMetricWriter implements MetricWriter { + + private List writers = new ArrayList(); + + public CompositeMetricWriter(MetricWriter... writers) { + for (MetricWriter writer : writers) { + this.writers.add(writer); + } + } + + public CompositeMetricWriter(List writers) { + this.writers.addAll(writers); + } + + @Override + public void increment(Delta delta) { + for (MetricWriter writer : this.writers) { + writer.increment(delta); + } + } + + @Override + public void set(Metric value) { + for (MetricWriter writer : this.writers) { + writer.set(value); + } + } + + @Override + public void reset(String metricName) { + for (MetricWriter writer : this.writers) { + writer.reset(metricName); + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultCounterService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java similarity index 66% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultCounterService.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java index 31e6e1438a6..d9433a43fbd 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultCounterService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterService.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics; +package org.springframework.boot.actuate.metrics.writer; -import java.util.Date; +import org.springframework.boot.actuate.metrics.CounterService; /** * Default implementation of {@link CounterService}. @@ -25,34 +25,33 @@ import java.util.Date; */ public class DefaultCounterService implements CounterService { - private MetricRepository repository; + private final MetricWriter writer; /** * Create a {@link DefaultCounterService} instance. - * @param repository the underlying repository used to manage metrics + * @param writer the underlying writer used to manage metrics */ - public DefaultCounterService(MetricRepository repository) { - super(); - this.repository = repository; + public DefaultCounterService(MetricWriter writer) { + this.writer = writer; } @Override public void increment(String metricName) { - this.repository.increment(wrap(metricName), 1, new Date()); + this.writer.increment(new Delta(wrap(metricName), 1L)); } @Override public void decrement(String metricName) { - this.repository.increment(wrap(metricName), -1, new Date()); + this.writer.increment(new Delta(wrap(metricName), -1L)); } @Override public void reset(String metricName) { - this.repository.set(wrap(metricName), 0, new Date()); + this.writer.increment(new Delta(wrap(metricName), 0L)); } private String wrap(String metricName) { - if (metricName.startsWith("counter")) { + if (metricName.startsWith("counter") || metricName.startsWith("meter")) { return metricName; } else { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultGaugeService.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java similarity index 59% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultGaugeService.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java index 9e94ca1ff79..e25a9538b10 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/DefaultGaugeService.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeService.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics; +package org.springframework.boot.actuate.metrics.writer; -import java.util.Date; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.Metric; /** * Default implementation of {@link GaugeService}. @@ -25,24 +26,24 @@ import java.util.Date; */ public class DefaultGaugeService implements GaugeService { - private MetricRepository metricRepository; + private final MetricWriter writer; /** - * Create a new {@link DefaultGaugeService} instance. - * @param counterRepository + * Create a {@link DefaultCounterService} instance. + * @param writer the underlying writer used to manage metrics */ - public DefaultGaugeService(MetricRepository counterRepository) { - super(); - this.metricRepository = counterRepository; + public DefaultGaugeService(MetricWriter writer) { + this.writer = writer; } @Override - public void set(String metricName, double value) { - this.metricRepository.set(wrap(metricName), value, new Date()); + public void submit(String metricName, double value) { + this.writer.set(new Metric(wrap(metricName), value)); } private String wrap(String metricName) { - if (metricName.startsWith("gauge")) { + if (metricName.startsWith("gauge") || metricName.startsWith("histogram") + || metricName.startsWith("timer")) { return metricName; } else { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/Delta.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/Delta.java new file mode 100644 index 00000000000..df59a5312e1 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/Delta.java @@ -0,0 +1,38 @@ +/* + * 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.metrics.writer; + +import java.util.Date; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * A value object representing an increment in a metric value (usually a counter). + * + * @author Dave Syer + */ +public class Delta extends Metric { + + public Delta(String name, T value, Date timestamp) { + super(name, value, timestamp); + } + + public Delta(String name, T value) { + super(name, value); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriter.java new file mode 100644 index 00000000000..2beb8c585e8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriter.java @@ -0,0 +1,60 @@ +/* + * 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.metrics.writer; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.MessageBuilder; + +/** + * A {@link MetricWriter} that publishes the metric updates on a {@link MessageChannel}. + * The messages have the writer input ({@link Delta} or {@link Metric}) as payload, and + * carry an additional header "metricName" with the name of the metric in it. + * + * @author Dave Syer + */ +public class MessageChannelMetricWriter implements MetricWriter { + + private static final String METRIC_NAME = "metricName"; + + private String DELETE = "delete"; + + private final MessageChannel channel; + + public MessageChannelMetricWriter(MessageChannel channel) { + this.channel = channel; + } + + @Override + public void increment(Delta delta) { + this.channel.send(MessageBuilder.withPayload(delta) + .setHeader(METRIC_NAME, delta.getName()).build()); + } + + @Override + public void set(Metric value) { + this.channel.send(MessageBuilder.withPayload(value) + .setHeader(METRIC_NAME, value.getName()).build()); + } + + @Override + public void reset(String metricName) { + this.channel.send(MessageBuilder.withPayload(this.DELETE) + .setHeader(METRIC_NAME, metricName).build()); + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriter.java new file mode 100644 index 00000000000..1a6e7868a79 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriter.java @@ -0,0 +1,34 @@ +/* + * 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.metrics.writer; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * Basic strategy for write operations on {@link Metric} data. + * + * @author Dave Syer + */ +public interface MetricWriter { + + void increment(Delta delta); + + void set(Metric value); + + void reset(String metricName); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriterMessageHandler.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriterMessageHandler.java new file mode 100644 index 00000000000..bba4baa7c45 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/writer/MetricWriterMessageHandler.java @@ -0,0 +1,50 @@ +/* + * 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.metrics.writer; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; + +/** + * A {@link MessageHandler} that updates {@link Metric} values through a + * {@link MetricWriter}. + * + * @author Dave Syer + */ +public final class MetricWriterMessageHandler implements MessageHandler { + + private final MetricWriter observer; + + public MetricWriterMessageHandler(MetricWriter observer) { + this.observer = observer; + } + + @Override + public void handleMessage(Message message) throws MessagingException { + Object payload = message.getPayload(); + if (payload instanceof Delta) { + Delta value = (Delta) payload; + this.observer.increment(value); + } + else { + Metric value = (Metric) payload; + this.observer.set(value); + } + } +} \ No newline at end of file diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java index 3de0240fb6d..3d4dc1e6d93 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MetricFilterAutoConfigurationTests.java @@ -64,7 +64,7 @@ public class MetricFilterAutoConfigurationTests { }).given(chain).doFilter(request, response); filter.doFilter(request, response, chain); verify(context.getBean(CounterService.class)).increment("status.200.test.path"); - verify(context.getBean(GaugeService.class)).set(eq("response.test.path"), + verify(context.getBean(GaugeService.class)).submit(eq("response.test.path"), anyDouble()); context.close(); } 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 e490e395e79..8af794e0110 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 @@ -16,34 +16,79 @@ package org.springframework.boot.actuate.autoconfigure; +import java.util.concurrent.Executor; + import org.junit.Test; -import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration; import org.springframework.boot.actuate.metrics.CounterService; -import org.springframework.boot.actuate.metrics.DefaultCounterService; -import org.springframework.boot.actuate.metrics.DefaultGaugeService; 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.writer.DefaultCounterService; +import org.springframework.boot.actuate.metrics.writer.DefaultGaugeService; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; 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 com.codahale.metrics.Gauge; +import com.codahale.metrics.MetricRegistry; 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.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link MetricRepositoryAutoConfiguration}. * * @author Phillip Webb + * @author Dave Syer */ public class MetricRepositoryAutoConfigurationTests { @Test - public void createServices() { + public void createServices() throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + SyncTaskExecutorConfiguration.class, MetricRepositoryAutoConfiguration.class); - assertNotNull(context.getBean(DefaultGaugeService.class)); + DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + assertNotNull(gaugeService); assertNotNull(context.getBean(DefaultCounterService.class)); + gaugeService.submit("foo", 2.7); + assertEquals(2.7, context.getBean(MetricReader.class).findOne("gauge.foo") + .getValue()); + context.close(); + } + + @Test + public void provideAdditionalWriter() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + SyncTaskExecutorConfiguration.class, WriterConfig.class, + MetricRepositoryAutoConfiguration.class); + DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + assertNotNull(gaugeService); + gaugeService.submit("foo", 2.7); + MetricWriter writer = context.getBean("writer", MetricWriter.class); + verify(writer).set(any(Metric.class)); + context.close(); + } + + @Test + public void codahaleInstalledIfPresent() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + SyncTaskExecutorConfiguration.class, WriterConfig.class, + MetricRepositoryAutoConfiguration.class); + DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class); + assertNotNull(gaugeService); + gaugeService.submit("foo", 2.7); + MetricRegistry registry = context.getBean(MetricRegistry.class); + @SuppressWarnings("unchecked") + Gauge gauge = (Gauge) registry.getMetrics().get("gauge.foo"); + assertEquals(new Double(2.7), gauge.getValue()); context.close(); } @@ -56,6 +101,26 @@ public class MetricRepositoryAutoConfigurationTests { context.close(); } + @Configuration + public static class SyncTaskExecutorConfiguration { + + @Bean + public Executor metricsExecutor() { + return new SyncTaskExecutor(); + } + + } + + @Configuration + public static class WriterConfig { + + @Bean + public MetricWriter writer() { + return mock(MetricWriter.class); + } + + } + @Configuration public static class Config { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/MetricsEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/MetricsEndpointTests.java index a05efee4319..e24febd7add 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/MetricsEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/MetricsEndpointTests.java @@ -41,7 +41,7 @@ public class MetricsEndpointTests extends AbstractEndpointTests @Test public void invoke() throws Exception { - assertThat(getEndpointBean().invoke().get("a"), equalTo((Object) 0.5)); + assertThat(getEndpointBean().invoke().get("a"), equalTo((Object) 0.5f)); } @Configuration @@ -50,11 +50,11 @@ public class MetricsEndpointTests extends AbstractEndpointTests @Bean public MetricsEndpoint endpoint() { - final Metric metric = new Metric("a", 0.5f); + final Metric metric = new Metric("a", 0.5f); PublicMetrics metrics = new PublicMetrics() { @Override - public Collection metrics() { - return Collections.singleton(metric); + public Collection> metrics() { + return Collections.> singleton(metric); } }; return new MetricsEndpoint(metrics); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetricsTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetricsTests.java index 58f10884458..d573fa31af2 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetricsTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/VanillaPublicMetricsTests.java @@ -21,9 +21,8 @@ import java.util.HashMap; import java.util.Map; import org.junit.Test; -import org.springframework.boot.actuate.endpoint.VanillaPublicMetrics; -import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; @@ -39,14 +38,14 @@ public class VanillaPublicMetricsTests { @Test public void testMetrics() throws Exception { InMemoryMetricRepository repository = new InMemoryMetricRepository(); - repository.set("a", 0.5, new Date()); + repository.set(new Metric("a", 0.5, new Date())); VanillaPublicMetrics publicMetrics = new VanillaPublicMetrics(repository); - Map results = new HashMap(); - for (Metric metric : publicMetrics.metrics()) { + Map> results = new HashMap>(); + for (Metric metric : publicMetrics.metrics()) { results.put(metric.getName(), metric); } assertTrue(results.containsKey("mem")); assertTrue(results.containsKey("mem.free")); - assertThat(results.get("a").getValue(), equalTo(0.5)); + assertThat(results.get("a").getValue().doubleValue(), equalTo(0.5)); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepositoryTests.java deleted file mode 100644 index 12fa6cb4769..00000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/InMemoryMetricRepositoryTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.metrics; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -/** - * Tests for {@link InMemoryMetricRepository}. - */ -public class InMemoryMetricRepositoryTests { - - private InMemoryMetricRepository repository = new InMemoryMetricRepository(); - - @Test - public void increment() { - this.repository.increment("foo", 1, new Date()); - assertEquals(1.0, this.repository.findOne("foo").getValue(), 0.01); - } - - @Test - public void incrementConcurrent() throws Exception { - Collection> tasks = new ArrayList>(); - for (int i = 0; i < 100; i++) { - tasks.add(new Callable() { - @Override - public Boolean call() throws Exception { - InMemoryMetricRepositoryTests.this.repository.increment("foo", 1, - new Date()); - return true; - } - }); - tasks.add(new Callable() { - @Override - public Boolean call() throws Exception { - InMemoryMetricRepositoryTests.this.repository.increment("foo", -1, - new Date()); - return true; - } - }); - } - List> all = Executors.newFixedThreadPool(10).invokeAll(tasks); - for (Future future : all) { - assertTrue(future.get(1, TimeUnit.SECONDS)); - } - assertEquals(0, this.repository.findOne("foo").getValue(), 0.01); - } - - @Test - public void set() { - this.repository.set("foo", 1, new Date()); - assertEquals(1.0, this.repository.findOne("foo").getValue(), 0.01); - } - -} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricRepository.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/Iterables.java similarity index 68% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricRepository.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/Iterables.java index b03e01af110..33cc8754bb6 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricRepository.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/Iterables.java @@ -16,24 +16,23 @@ package org.springframework.boot.actuate.metrics; +import java.util.ArrayList; import java.util.Collection; -import java.util.Date; /** - * A Repository used to manage {@link Metric}s. - * * @author Dave Syer */ -public interface MetricRepository { +public abstract class Iterables { - void increment(String metricName, int amount, Date timestamp); - - void set(String metricName, double value, Date timestamp); - - void delete(String metricName); - - Metric findOne(String metricName); - - Collection findAll(); + public static Collection collection(Iterable iterable) { + if (iterable instanceof Collection) { + return (Collection) iterable; + } + ArrayList list = new ArrayList(); + for (T t : iterable) { + list.add(t); + } + return list; + } } 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 new file mode 100644 index 00000000000..67a29e84b04 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/MetricCopyExporterTests.java @@ -0,0 +1,60 @@ +/* + * 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.metrics.export; + +import java.util.Date; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class MetricCopyExporterTests { + + private InMemoryMetricRepository writer = new InMemoryMetricRepository(); + private InMemoryMetricRepository reader = new InMemoryMetricRepository(); + private MetricCopyExporter exporter = new MetricCopyExporter(this.reader, this.writer); + + @Test + public void export() { + this.reader.set(new Metric("foo", 2.3)); + this.exporter.export(); + assertEquals(1, this.writer.count()); + } + + @Test + public void timestamp() { + this.reader.set(new Metric("foo", 2.3)); + this.exporter.setEarliestTimestamp(new Date(System.currentTimeMillis() + 10000)); + this.exporter.export(); + assertEquals(0, this.writer.count()); + } + + @Test + public void ignoreTimestamp() { + this.reader.set(new Metric("foo", 2.3)); + this.exporter.setIgnoreTimestamps(true); + this.exporter.setEarliestTimestamp(new Date(System.currentTimeMillis() + 10000)); + this.exporter.export(); + assertEquals(1, this.writer.count()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporterTests.java new file mode 100644 index 00000000000..4e8ac03ba8f --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/PrefixMetricGroupExporterTests.java @@ -0,0 +1,66 @@ +/* + * 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.metrics.export; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Iterables; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class PrefixMetricGroupExporterTests { + + private InMemoryMetricRepository writer = new InMemoryMetricRepository(); + private InMemoryMetricRepository reader = new InMemoryMetricRepository(); + private PrefixMetricGroupExporter exporter = new PrefixMetricGroupExporter( + this.reader, this.writer); + + @Test + public void prefixedMetricsCopied() { + this.reader.set(new Metric("foo.bar", 2.3)); + this.reader.set(new Metric("foo.spam", 1.3)); + this.exporter.setGroups(Collections.singleton("foo")); + this.exporter.export(); + assertEquals(1, Iterables.collection(this.writer.groups()).size()); + } + + @Test + public void unprefixedMetricsNotCopied() { + this.reader.set(new Metric("foo.bar", 2.3)); + this.reader.set(new Metric("foo.spam", 1.3)); + this.exporter.setGroups(Collections.singleton("bar")); + this.exporter.export(); + assertEquals(0, Iterables.collection(this.writer.groups()).size()); + } + + @Test + public void onlyPrefixedMetricsCopied() { + this.reader.set(new Metric("foo.bar", 2.3)); + this.reader.set(new Metric("foo.spam", 1.3)); + this.reader.set(new Metric("foobar.spam", 1.3)); + this.exporter.setGroups(Collections.singleton("foo")); + this.exporter.export(); + assertEquals(1, Iterables.collection(this.writer.groups()).size()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporterTests.java new file mode 100644 index 00000000000..ae30c5275b9 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/RichGaugeExporterTests.java @@ -0,0 +1,43 @@ +/* + * 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.metrics.export; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Iterables; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository; +import org.springframework.boot.actuate.metrics.rich.InMemoryRichGaugeRepository; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class RichGaugeExporterTests { + + private InMemoryRichGaugeRepository reader = new InMemoryRichGaugeRepository(); + private InMemoryMetricRepository writer = new InMemoryMetricRepository(); + private RichGaugeExporter exporter = new RichGaugeExporter(this.reader, this.writer); + + @Test + public void prefixedMetricsCopied() { + this.reader.set(new Metric("foo", 2.3)); + this.exporter.export(); + assertEquals(1, Iterables.collection(this.writer.groups()).size()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepositoryTests.java new file mode 100644 index 00000000000..aa264d11bbc --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryMetricRepositoryTests.java @@ -0,0 +1,47 @@ +/* + * 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.metrics.repository; + +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; + +/** + * Tests for {@link InMemoryMetricRepository}. + */ +public class InMemoryMetricRepositoryTests { + + private InMemoryMetricRepository repository = new InMemoryMetricRepository(); + + @Test + public void increment() { + this.repository.increment(new Delta("foo", 1, new Date())); + assertEquals(1.0, this.repository.findOne("foo").getValue().doubleValue(), 0.01); + } + + @Test + public void set() { + this.repository.set(new Metric("foo", 2.5, new Date())); + assertEquals(2.5, this.repository.findOne("foo").getValue().doubleValue(), 0.01); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryPrefixMetricRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryPrefixMetricRepositoryTests.java new file mode 100644 index 00000000000..58f0dcc3e90 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/InMemoryPrefixMetricRepositoryTests.java @@ -0,0 +1,83 @@ +/* + * 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.metrics.repository; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Dave Syer + */ +public class InMemoryPrefixMetricRepositoryTests { + + private InMemoryMetricRepository repository = new InMemoryMetricRepository(); + + @Test + public void registeredPrefixCounted() { + this.repository.increment(new Delta("foo.bar", 1)); + this.repository.increment(new Delta("foo.bar", 1)); + this.repository.increment(new Delta("foo.spam", 1)); + Set names = new HashSet(); + for (Metric metric : this.repository.findAll("foo")) { + names.add(metric.getName()); + } + assertEquals(2, names.size()); + assertTrue(names.contains("foo.bar")); + } + + @Test + public void perfixWithWildcard() { + this.repository.increment(new Delta("foo.bar", 1)); + Set names = new HashSet(); + for (Metric metric : this.repository.findAll("foo.*")) { + names.add(metric.getName()); + } + assertEquals(1, names.size()); + assertTrue(names.contains("foo.bar")); + } + + @Test + public void perfixWithPeriod() { + this.repository.increment(new Delta("foo.bar", 1)); + Set names = new HashSet(); + for (Metric metric : this.repository.findAll("foo.")) { + names.add(metric.getName()); + } + assertEquals(1, names.size()); + assertTrue(names.contains("foo.bar")); + } + + @Test + public void onlyRegisteredPrefixCounted() { + this.repository.increment(new Delta("foo.bar", 1)); + this.repository.increment(new Delta("foobar.spam", 1)); + Set names = new HashSet(); + for (Metric metric : this.repository.findAll("foo")) { + names.add(metric.getName()); + } + assertEquals(1, names.size()); + assertTrue(names.contains("foo.bar")); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepositoryTests.java new file mode 100644 index 00000000000..9f05be460ca --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMetricRepositoryTests.java @@ -0,0 +1,91 @@ +/* + * 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.metrics.repository.redis; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Iterables; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.data.redis.core.StringRedisTemplate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Dave Syer + */ +public class RedisMetricRepositoryTests { + + @Rule + public RedisServer redis = RedisServer.running(); + private RedisMetricRepository repository; + + @Before + public void init() { + this.repository = new RedisMetricRepository(this.redis.getResource()); + } + + @After + public void clear() { + assertNotNull(new StringRedisTemplate(this.redis.getResource()).opsForValue() + .get("spring.metrics.foo")); + this.repository.reset("foo"); + this.repository.reset("bar"); + assertNull(new StringRedisTemplate(this.redis.getResource()).opsForValue().get( + "spring.metrics.foo")); + } + + @Test + public void setAndGet() { + this.repository.set(new Metric("foo", 12.3)); + Metric metric = this.repository.findOne("foo"); + assertEquals("foo", metric.getName()); + assertEquals(12.3, metric.getValue().doubleValue(), 0.01); + } + + @Test + public void incrementAndGet() { + this.repository.increment(new Delta("foo", 3L)); + assertEquals(3, this.repository.findOne("foo").getValue().longValue()); + } + + @Test + public void findAll() { + this.repository.increment(new Delta("foo", 3L)); + this.repository.set(new Metric("bar", 12.3)); + assertEquals(2, Iterables.collection(this.repository.findAll()).size()); + } + + @Test + public void findOneWithAll() { + this.repository.increment(new Delta("foo", 3L)); + Metric metric = this.repository.findAll().iterator().next(); + assertEquals("foo", metric.getName()); + } + + @Test + public void count() { + this.repository.increment(new Delta("foo", 3L)); + this.repository.set(new Metric("bar", 12.3)); + assertEquals(2, this.repository.count()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepositoryTests.java new file mode 100644 index 00000000000..0af5c7a8911 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisMultiMetricRepositoryTests.java @@ -0,0 +1,84 @@ +/* + * 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.metrics.repository.redis; + +import java.util.Arrays; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Iterables; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.data.redis.core.StringRedisTemplate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Dave Syer + */ +public class RedisMultiMetricRepositoryTests { + + @Rule + public RedisServer redis = RedisServer.running(); + private RedisMultiMetricRepository repository; + + @Before + public void init() { + this.repository = new RedisMultiMetricRepository(this.redis.getResource()); + } + + @After + public void clear() { + assertTrue(new StringRedisTemplate(this.redis.getResource()).opsForZSet().size( + "spring.groups.foo") > 0); + this.repository.reset("foo"); + this.repository.reset("bar"); + assertNull(new StringRedisTemplate(this.redis.getResource()).opsForValue().get( + "spring.groups.foo")); + assertNull(new StringRedisTemplate(this.redis.getResource()).opsForValue().get( + "spring.groups.bar")); + } + + @Test + public void setAndGet() { + this.repository.save("foo", Arrays.> asList(new Metric( + "foo.val", 12.3), new Metric("foo.bar", 11.3))); + assertEquals(2, Iterables.collection(this.repository.findAll("foo")).size()); + } + + @Test + public void groups() { + this.repository.save("foo", Arrays.> asList(new Metric( + "foo.val", 12.3), new Metric("foo.bar", 11.3))); + this.repository.save("bar", Arrays.> asList(new Metric( + "bar.val", 12.3), new Metric("bar.foo", 11.3))); + assertEquals(2, Iterables.collection(this.repository.groups()).size()); + } + + @Test + public void count() { + this.repository.save("foo", Arrays.> asList(new Metric( + "foo.val", 12.3), new Metric("foo.bar", 11.3))); + this.repository.save("bar", Arrays.> asList(new Metric( + "bar.val", 12.3), new Metric("bar.foo", 11.3))); + assertEquals(2, this.repository.count()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisServer.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisServer.java new file mode 100644 index 00000000000..1929a273419 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/repository/redis/RedisServer.java @@ -0,0 +1,144 @@ +/* + * 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.metrics.repository.redis; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Assume; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; + +import static org.junit.Assert.fail; + +/** + * @author Eric Bottard + * @author Gary Russell + * @author Dave Syer + */ +public class RedisServer implements TestRule { + + private static final String EXTERNAL_SERVERS_REQUIRED = "EXTERNAL_SERVERS_REQUIRED"; + + protected LettuceConnectionFactory resource; + + private String resourceDescription = "Redis ConnectionFactory"; + + private static final Log logger = LogFactory.getLog(RedisServer.class); + + public static RedisServer running() { + return new RedisServer(); + } + + private RedisServer() { + } + + @Override + public Statement apply(final Statement base, Description description) { + try { + this.resource = obtainResource(); + } + catch (Exception e) { + maybeCleanup(); + + return failOrSkip(e); + } + + return new Statement() { + + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } + finally { + try { + cleanupResource(); + } + catch (Exception ignored) { + RedisServer.logger.warn( + "Exception while trying to cleanup proper resource", + ignored); + } + } + } + + }; + } + + private Statement failOrSkip(Exception e) { + String serversRequired = System.getenv(EXTERNAL_SERVERS_REQUIRED); + if ("true".equalsIgnoreCase(serversRequired)) { + logger.error(this.resourceDescription + " IS REQUIRED BUT NOT AVAILABLE", e); + fail(this.resourceDescription + " IS NOT AVAILABLE"); + // Never reached, here to satisfy method signature + return null; + } + else { + logger.error(this.resourceDescription + " IS NOT AVAILABLE, SKIPPING TESTS", + e); + return new Statement() { + + @Override + public void evaluate() throws Throwable { + Assume.assumeTrue("Skipping test due to " + + RedisServer.this.resourceDescription + + " not being available", false); + } + }; + } + } + + private void maybeCleanup() { + if (this.resource != null) { + try { + cleanupResource(); + } + catch (Exception ignored) { + logger.warn("Exception while trying to cleanup failed resource", ignored); + } + } + } + + public RedisConnectionFactory getResource() { + return this.resource; + } + + /** + * Perform cleanup of the {@link #resource} field, which is guaranteed to be non null. + * + * @throws Exception any exception thrown by this method will be logged and swallowed + */ + protected void cleanupResource() throws Exception { + this.resource.destroy(); + } + + /** + * Try to obtain and validate a resource. Implementors should either set the + * {@link #resource} field with a valid resource and return normally, or throw an + * exception. + */ + protected LettuceConnectionFactory obtainResource() throws Exception { + LettuceConnectionFactory resource = new LettuceConnectionFactory(); + resource.afterPropertiesSet(); + resource.getConnection().close(); + return resource; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepositoryTests.java new file mode 100644 index 00000000000..ac8383977d1 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/rich/InMemoryRichGaugeRepositoryTests.java @@ -0,0 +1,39 @@ +/* + * 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.metrics.rich; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class InMemoryRichGaugeRepositoryTests { + + private InMemoryRichGaugeRepository repository = new InMemoryRichGaugeRepository(); + + @Test + public void writeAndRead() { + this.repository.set(new Metric("foo", 1d)); + this.repository.set(new Metric("foo", 2d)); + assertEquals(2L, this.repository.findOne("foo").getCount()); + assertEquals(2d, this.repository.findOne("foo").getValue(), 0.01); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/util/InMemoryRepositoryTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/util/InMemoryRepositoryTests.java new file mode 100644 index 00000000000..b9e825a184a --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/util/InMemoryRepositoryTests.java @@ -0,0 +1,131 @@ +/* + * 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.metrics.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository.Callback; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Dave Syer + */ +public class InMemoryRepositoryTests { + + private SimpleInMemoryRepository repository = new SimpleInMemoryRepository(); + + @Test + public void setAndGet() { + this.repository.set("foo", "bar"); + assertEquals("bar", this.repository.findOne("foo")); + } + + @Test + public void updateExisting() { + this.repository.set("foo", "spam"); + this.repository.update("foo", new Callback() { + @Override + public String modify(String current) { + return "bar"; + } + }); + assertEquals("bar", this.repository.findOne("foo")); + } + + @Test + public void updateNonexistent() { + this.repository.update("foo", new Callback() { + @Override + public String modify(String current) { + return "bar"; + } + }); + assertEquals("bar", this.repository.findOne("foo")); + } + + @Test + public void findWithPrefix() { + this.repository.set("foo", "bar"); + this.repository.set("foo.bar", "one"); + this.repository.set("foo.min", "two"); + this.repository.set("foo.max", "three"); + assertEquals(3, ((Collection) this.repository.findAllWithPrefix("foo")).size()); + } + + @Test + public void patternsAcceptedForRegisteredPrefix() { + this.repository.set("foo.bar", "spam"); + Iterator iterator = this.repository.findAllWithPrefix("foo.*").iterator(); + assertEquals("spam", iterator.next()); + assertFalse(iterator.hasNext()); + } + + @Test + public void updateConcurrent() throws Exception { + final SimpleInMemoryRepository repository = new SimpleInMemoryRepository(); + Collection> tasks = new ArrayList>(); + for (int i = 0; i < 1000; i++) { + tasks.add(new Callable() { + @Override + public Boolean call() throws Exception { + repository.update("foo", new Callback() { + @Override + public Integer modify(Integer current) { + if (current == null) { + return 1; + } + return current + 1; + } + }); + return true; + } + }); + tasks.add(new Callable() { + @Override + public Boolean call() throws Exception { + repository.update("foo", new Callback() { + @Override + public Integer modify(Integer current) { + if (current == null) { + return -1; + } + return current - 1; + } + }); + return true; + } + }); + } + List> all = Executors.newFixedThreadPool(10).invokeAll(tasks); + for (Future future : all) { + assertTrue(future.get(1, TimeUnit.SECONDS)); + } + assertEquals(new Integer(0), repository.findOne("foo")); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriterTests.java new file mode 100644 index 00000000000..150bed13ef4 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/CodahaleMetricWriterTests.java @@ -0,0 +1,79 @@ +/* + * 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.metrics.writer; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.MetricRegistry; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +public class CodahaleMetricWriterTests { + + private MetricRegistry registry = new MetricRegistry(); + private CodahaleMetricWriter writer = new CodahaleMetricWriter(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()); + } + + @Test + public void updatePredefinedMeter() { + this.writer.increment(new Delta("meter.foo", 2)); + this.writer.increment(new Delta("meter.foo", 1)); + 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)); + 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)); + @SuppressWarnings("unchecked") + Gauge gauge = (Gauge) this.registry.getMetrics().get("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)); + 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)); + assertEquals(2, this.registry.histogram("histogram.foo").getCount()); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultCounterServiceTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterServiceTests.java similarity index 54% rename from spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultCounterServiceTests.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterServiceTests.java index 693d79c252c..a66ab5b4d8f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultCounterServiceTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultCounterServiceTests.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics; - -import java.util.Date; +package org.springframework.boot.actuate.metrics.writer; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.actuate.metrics.writer.DefaultCounterService; +import org.springframework.boot.actuate.metrics.writer.Delta; +import org.springframework.boot.actuate.metrics.writer.MetricWriter; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -30,19 +31,26 @@ import static org.mockito.Mockito.verify; */ public class DefaultCounterServiceTests { - private MetricRepository repository = mock(MetricRepository.class); + private MetricWriter repository = mock(MetricWriter.class); private DefaultCounterService service = new DefaultCounterService(this.repository); @Test public void incrementPrependsCounter() { this.service.increment("foo"); - verify(this.repository).increment(eq("counter.foo"), eq(1), any(Date.class)); + @SuppressWarnings("rawtypes") + ArgumentCaptor captor = ArgumentCaptor.forClass(Delta.class); + verify(this.repository).increment(captor.capture()); + assertEquals("counter.foo", captor.getValue().getName()); } @Test public void decrementPrependsCounter() { this.service.decrement("foo"); - verify(this.repository).increment(eq("counter.foo"), eq(-1), any(Date.class)); + @SuppressWarnings("rawtypes") + ArgumentCaptor captor = ArgumentCaptor.forClass(Delta.class); + verify(this.repository).increment(captor.capture()); + assertEquals("counter.foo", captor.getValue().getName()); + assertEquals(-1L, captor.getValue().getValue()); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultGaugeServiceTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeServiceTests.java similarity index 61% rename from spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultGaugeServiceTests.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeServiceTests.java index e926ca1261d..61dd5e99a99 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/DefaultGaugeServiceTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/DefaultGaugeServiceTests.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.actuate.metrics; - -import java.util.Date; +package org.springframework.boot.actuate.metrics.writer; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.boot.actuate.metrics.Metric; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -30,14 +29,18 @@ import static org.mockito.Mockito.verify; */ public class DefaultGaugeServiceTests { - private MetricRepository repository = mock(MetricRepository.class); + private MetricWriter repository = mock(MetricWriter.class); private DefaultGaugeService service = new DefaultGaugeService(this.repository); @Test - public void setPrependsGuager() { - this.service.set("foo", 2.3); - verify(this.repository).set(eq("gauge.foo"), eq(2.3), any(Date.class)); + public void setPrependsGauge() { + this.service.submit("foo", 2.3); + @SuppressWarnings("rawtypes") + ArgumentCaptor captor = ArgumentCaptor.forClass(Metric.class); + verify(this.repository).set(captor.capture()); + assertEquals("gauge.foo", captor.getValue().getName()); + assertEquals(2.3, captor.getValue().getValue()); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriterTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriterTests.java new file mode 100644 index 00000000000..97038789175 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/writer/MessageChannelMetricWriterTests.java @@ -0,0 +1,50 @@ +/* + * 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.metrics.writer; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * @author Dave Syer + */ +public class MessageChannelMetricWriterTests { + + private MessageChannel channel = mock(MessageChannel.class); + + private MessageChannelMetricWriter observer = new MessageChannelMetricWriter( + this.channel); + + @Test + public void messageSentOnAdd() { + this.observer.increment(new Delta("foo", 1)); + verify(this.channel).send(any(Message.class)); + } + + @Test + public void messageSentOnSet() { + this.observer.set(new Metric("foo", 1d)); + verify(this.channel).send(any(Message.class)); + } + +} diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/JreProxySelector.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/JreProxySelector.java index 3fc039c8d38..1738971383e 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/JreProxySelector.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/compiler/grape/JreProxySelector.java @@ -35,6 +35,8 @@ import org.eclipse.aether.repository.RemoteRepository; /** * (Copied from aether source code - not available yet in Maven repo.) + * + * @author Dave Syer */ public final class JreProxySelector implements ProxySelector { diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index b2c1926ceef..06374a8c888 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -9,6 +9,7 @@ 5.7.0 1.7.4 + 3.0.1 1.4 1.6 1.6 @@ -60,6 +61,26 @@ logback-classic ${logback.version}
+ + com.codahale.metrics + metrics-graphite + ${codahale-metrics.version} + + + com.codahale.metrics + metrics-ganglia + ${codahale-metrics.version} + + + com.codahale.metrics + metrics-core + ${codahale-metrics.version} + + + com.codahale.metrics + metrics-servlets + ${codahale-metrics.version} + com.fasterxml.jackson.core jackson-databind diff --git a/spring-boot-samples/spring-boot-sample-actuator/src/main/java/org/springframework/boot/sample/actuator/SampleController.java b/spring-boot-samples/spring-boot-sample-actuator/src/main/java/org/springframework/boot/sample/actuator/SampleController.java index ffb6a0c083d..4c79ca8ca55 100644 --- a/spring-boot-samples/spring-boot-sample-actuator/src/main/java/org/springframework/boot/sample/actuator/SampleController.java +++ b/spring-boot-samples/spring-boot-sample-actuator/src/main/java/org/springframework/boot/sample/actuator/SampleController.java @@ -20,11 +20,13 @@ import java.util.Collections; import java.util.Map; 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.ResponseBody; @Controller +@Description("A controller for handling requests for hello messages") public class SampleController { @Autowired