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