Commit aa2b0206 authored by Dave Syer's avatar Dave Syer

Refactor metrics to expose richer feature set

Main user-facing interface is still Counter/GaugeService but the
back end behind that has more options. The Default*Services write
metrics to a MetricWriter and there are some variants of that, and
also variants of MetricReader (basic read-only actions).

MetricRepository is now a combination of MetricReader, MetricWriter
and some more methods that make it a bit more repository like.

There is also a MultiMetricReader and a MultiMetricRepository for
the common case where metrics are stored in related (often open
ended) groups. Examples would be complex metrics like histograms
and "rich" metrics with averages and statistics attached (which
are both closed) and "field counters" which count the occurrences
of values of a particular named field or slot in an incoming message
(e.g. counting Twitter hastags, open ended).

In memory and redis implementations are provided for the repositories.
Generally speaking the in memory repository should be used as a
local buffer and then scheduled "exports" can be executed to copy
metric values accross to a remote repository for aggregation.
There is an Exporter interface to support this and a few implementations
dealing with different strategies for storing the results (singly or
grouped).

Codahale metrics are also supported through the MetricWriter interface.
Currently implemented through a naming convention (since Codahale has
a fixed object model this makes sense): metrics beginning with "histogram"
are Histograms, "timer" for Timers, "meter" for Meters etc.

Support for message driven metric consumption and production are provided
through a MetricWriterMessageHandler and a MessageChannelMetricWriter.

No support yet for pagination in the repositories, or for HATEOAS style
HTTP endpoints.
parent 5d8e58d1
...@@ -41,7 +41,21 @@ ...@@ -41,7 +41,21 @@
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId> <artifactId>spring-context</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<!-- Optional --> <!-- Optional -->
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.lambdaworks</groupId>
<artifactId>lettuce</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
...@@ -57,6 +71,11 @@ ...@@ -57,6 +71,11 @@
<artifactId>spring-webmvc</artifactId> <artifactId>spring-webmvc</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId> <artifactId>spring-security-web</artifactId>
......
...@@ -40,8 +40,8 @@ import org.springframework.boot.actuate.endpoint.VanillaPublicMetrics; ...@@ -40,8 +40,8 @@ import org.springframework.boot.actuate.endpoint.VanillaPublicMetrics;
import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.SimpleHealthIndicator; import org.springframework.boot.actuate.health.SimpleHealthIndicator;
import org.springframework.boot.actuate.health.VanillaHealthIndicator; import org.springframework.boot.actuate.health.VanillaHealthIndicator;
import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.MetricRepository; import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository; import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.AutoConfigurationReport; import org.springframework.boot.autoconfigure.AutoConfigurationReport;
...@@ -78,7 +78,7 @@ public class EndpointAutoConfiguration { ...@@ -78,7 +78,7 @@ public class EndpointAutoConfiguration {
private InfoPropertiesConfiguration properties; private InfoPropertiesConfiguration properties;
@Autowired(required = false) @Autowired(required = false)
private MetricRepository metricRepository = new InMemoryMetricRepository(); private MetricReader metricRepository = new InMemoryMetricRepository();
@Autowired(required = false) @Autowired(required = false)
private PublicMetrics metrics; private PublicMetrics metrics;
......
...@@ -99,7 +99,7 @@ public class MetricFilterAutoConfiguration { ...@@ -99,7 +99,7 @@ public class MetricFilterAutoConfiguration {
finally { finally {
stopWatch.stop(); stopWatch.stop();
String gaugeKey = getKey("response" + suffix); String gaugeKey = getKey("response" + suffix);
MetricFilterAutoConfiguration.this.gaugeService.set(gaugeKey, MetricFilterAutoConfiguration.this.gaugeService.submit(gaugeKey,
stopWatch.getTotalTimeMillis()); stopWatch.getTotalTimeMillis());
String counterKey = getKey("status." + getStatus(response) + suffix); String counterKey = getKey("status." + getStatus(response) + suffix);
MetricFilterAutoConfiguration.this.counterService.increment(counterKey); MetricFilterAutoConfiguration.this.counterService.increment(counterKey);
......
...@@ -16,41 +16,159 @@ ...@@ -16,41 +16,159 @@
package org.springframework.boot.actuate.autoconfigure; 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.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.GaugeService;
import org.springframework.boot.actuate.metrics.InMemoryMetricRepository; import org.springframework.boot.actuate.metrics.export.Exporter;
import org.springframework.boot.actuate.metrics.MetricRepository; 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.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.Bean;
import org.springframework.context.annotation.Configuration; 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. * <p>
* {@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.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.*").
* </p>
* <p>
* 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 <code>@Primary</code>, 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.
* </p>
*
* @see GaugeService
* @see CounterService
* @see MetricWriter
* @see InMemoryMetricRepository
* @see CodahaleMetricWriter
* @see Exporter
* *
* @author Dave Syer * @author Dave Syer
*/ */
@Configuration @Configuration
public class MetricRepositoryAutoConfiguration { public class MetricRepositoryAutoConfiguration {
@Autowired
private MetricWriter writer;
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public CounterService counterService() { public CounterService counterService() {
return new DefaultCounterService(metricRepository()); return new DefaultCounterService(this.writer);
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public GaugeService gaugeService() { public GaugeService gaugeService() {
return new DefaultGaugeService(metricRepository()); return new DefaultGaugeService(this.writer);
} }
@Bean @Configuration
@ConditionalOnMissingBean @ConditionalOnMissingBean(MetricRepository.class)
protected MetricRepository metricRepository() { static class MetricRepositoryConfiguration {
return new InMemoryMetricRepository();
@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<MetricWriter> 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<MetricWriter> writers) {
return new CompositeMetricWriter(writers);
}
} }
} }
...@@ -47,7 +47,7 @@ public class MetricsEndpoint extends AbstractEndpoint<Map<String, Object>> { ...@@ -47,7 +47,7 @@ public class MetricsEndpoint extends AbstractEndpoint<Map<String, Object>> {
@Override @Override
public Map<String, Object> invoke() { public Map<String, Object> invoke() {
Map<String, Object> result = new LinkedHashMap<String, Object>(); Map<String, Object> result = new LinkedHashMap<String, Object>();
for (Metric metric : this.metrics.metrics()) { for (Metric<?> metric : this.metrics.metrics()) {
result.put(metric.getName(), metric.getValue()); result.put(metric.getName(), metric.getValue());
} }
return result; return result;
......
...@@ -31,6 +31,6 @@ public interface PublicMetrics { ...@@ -31,6 +31,6 @@ public interface PublicMetrics {
/** /**
* @return an indication of current state through metrics * @return an indication of current state through metrics
*/ */
Collection<Metric> metrics(); Collection<Metric<?>> metrics();
} }
...@@ -20,32 +20,36 @@ import java.util.Collection; ...@@ -20,32 +20,36 @@ import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import org.springframework.boot.actuate.metrics.Metric; 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; import org.springframework.util.Assert;
/** /**
* Default implementation of {@link PublicMetrics} that exposes all metrics from the * Default implementation of {@link PublicMetrics} that exposes all metrics from a
* {@link MetricRepository} along with memory information. * {@link MetricReader} along with memory information.
* *
* @author Dave Syer * @author Dave Syer
*/ */
public class VanillaPublicMetrics implements PublicMetrics { public class VanillaPublicMetrics implements PublicMetrics {
private MetricRepository metricRepository; private MetricReader reader;
public VanillaPublicMetrics(MetricRepository metricRepository) { public VanillaPublicMetrics(MetricReader reader) {
Assert.notNull(metricRepository, "MetricRepository must not be null"); Assert.notNull(reader, "MetricReader must not be null");
this.metricRepository = metricRepository; this.reader = reader;
} }
@Override @Override
public Collection<Metric> metrics() { public Collection<Metric<?>> metrics() {
Collection<Metric> result = new LinkedHashSet<Metric>( Collection<Metric<?>> result = new LinkedHashSet<Metric<?>>();
this.metricRepository.findAll()); for (Metric<?> metric : this.reader.findAll()) {
result.add(new Metric("mem", new Long(Runtime.getRuntime().totalMemory()) / 1024)); result.add(metric);
result.add(new Metric("mem.free", }
new Long(Runtime.getRuntime().freeMemory()) / 1024)); result.add(new Metric<Long>("mem",
result.add(new Metric("processors", Runtime.getRuntime().availableProcessors())); new Long(Runtime.getRuntime().totalMemory()) / 1024));
result.add(new Metric<Long>("mem.free", new Long(Runtime.getRuntime()
.freeMemory()) / 1024));
result.add(new Metric<Integer>("processors", Runtime.getRuntime()
.availableProcessors()));
return result; return result;
} }
......
...@@ -17,27 +17,27 @@ ...@@ -17,27 +17,27 @@
package org.springframework.boot.actuate.metrics; 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 * @author Dave Syer
*/ */
public interface CounterService { public interface CounterService {
/** /**
* Increment the specified metric by 1. * Increment the specified counter by 1.
* @param metricName the name of the metric * @param metricName the name of the counter
*/ */
void increment(String metricName); void increment(String metricName);
/** /**
* Decrement the specified metric by 1. * Decrement the specified counter by 1.
* @param metricName the name of the metric * @param metricName the name of the counter
*/ */
void decrement(String metricName); void decrement(String metricName);
/** /**
* Reset the specified metric to 0. * Reset the specified counter.
* @param metricName the name of the metric * @param metricName the name of the counter
*/ */
void reset(String metricName); void reset(String metricName);
......
...@@ -17,17 +17,23 @@ ...@@ -17,17 +17,23 @@
package org.springframework.boot.actuate.metrics; 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 * @author Dave Syer
*/ */
public interface GaugeService { public interface GaugeService {
/** /**
* Set the specified metric value * Set the specified gauge value
* @param metricName the metric to set * @param metricName the name of the gauge to set
* @param value the value of the metric * @param value the value of the gauge
*/ */
void set(String metricName, double value); void submit(String metricName, double value);
} }
...@@ -16,33 +16,45 @@ ...@@ -16,33 +16,45 @@
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics;
import java.util.Date;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/** /**
* Immutable class that can be used to hold any arbitrary system measurement value. For * Immutable class that can be used to hold any arbitrary system measurement value (a
* example a metric might record the number of active connections. * 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 * @author Dave Syer
* @see MetricRepository
* @see CounterService
*/ */
public final class Metric { public class Metric<T extends Number> {
private final String name; 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. * Create a new {@link Metric} instance.
* @param name the name of the metric * @param name the name of the metric
* @param value the value of the metric * @param value the value of the metric
* @param timestamp the timestamp for the metric
*/ */
public Metric(String name, double value) { public Metric(String name, T value, Date timestamp) {
super();
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
this.name = name; this.name = name;
this.value = value; this.value = value;
this.timestamp = timestamp;
} }
/** /**
...@@ -55,17 +67,28 @@ public final class Metric { ...@@ -55,17 +67,28 @@ public final class Metric {
/** /**
* Returns the value of the metric. * Returns the value of the metric.
*/ */
public double getValue() { public T getValue() {
return this.value; 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. * Create a new {@link Metric} with an incremented value.
* @param amount the amount that the new metric will differ from this one * @param amount the amount that the new metric will differ from this one
* @return a new {@link Metric} instance * @return a new {@link Metric} instance
*/ */
public Metric increment(int amount) { public Metric<Long> increment(int amount) {
return new Metric(this.name, new Double(((int) this.value) + amount)); return new Metric<Long>(this.getName(), new Long(this.getValue().longValue()
+ amount));
} }
/** /**
...@@ -73,41 +96,49 @@ public final class Metric { ...@@ -73,41 +96,49 @@ public final class Metric {
* @param value the value of the new metric * @param value the value of the new metric
* @return a new {@link Metric} instance * @return a new {@link Metric} instance
*/ */
public Metric set(double value) { public <S extends Number> Metric<S> set(S value) {
return new Metric(this.name, value); return new Metric<S>(this.getName(), value);
}
@Override
public String toString() {
return "Metric [name=" + this.name + ", value=" + this.value + "]";
} }
@Override @Override
public int hashCode() { public int hashCode() {
int valueHashCode = ObjectUtils.hashCode(this.value);
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.name); result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
result = prime * result + (valueHashCode ^ (valueHashCode >>> 32)); result = prime * result
+ ((this.timestamp == null) ? 0 : this.timestamp.hashCode());
result = prime * result + ((this.value == null) ? 0 : this.value.hashCode());
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj)
return true; return true;
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 (obj == null) { else if (!this.name.equals(other.name))
return false; return false;
if (this.timestamp == null) {
if (other.timestamp != null)
return false;
} }
if (getClass() == obj.getClass()) { else if (!this.timestamp.equals(other.timestamp))
Metric other = (Metric) obj; return false;
boolean result = ObjectUtils.nullSafeEquals(this.name, other.name); if (this.value == null) {
result &= Double.doubleToLongBits(this.value) == Double if (other.value != null)
.doubleToLongBits(other.value); return false;
return result;
} }
return super.equals(obj); else if (!this.value.equals(other.value))
return false;
return true;
} }
} }
/*
* 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<Metric<?>> values = new ArrayList<Metric<?>>();
for (Metric<?> metric : next(group)) {
Metric<?> value = new Metric<Number>(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<String> 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<Metric<?>> values);
/**
* Get the next group of metrics to write.
*
* @param group the group name to write
* @return some metrics to write
*/
protected abstract Iterable<Metric<?>> next(String group);
}
\ No newline at end of file
/*
* 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 <code>@Scheduled</code> annotation in a Spring ApplicationContext.
*
* @author Dave Syer
*/
public interface Exporter {
void export();
}
/*
* 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<Metric<?>> next(String group) {
return this.reader.findAll();
}
@Override
protected void write(String group, Collection<Metric<?>> values) {
for (Metric<?> value : values) {
this.writer.set(value);
}
}
}
/*
* 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<String> groups = new HashSet<String>();
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<String> groups) {
this.groups = groups;
}
@Override
protected Iterable<Metric<?>> next(String group) {
return this.reader.findAll(group);
}
@Override
protected Iterable<String> groups() {
return this.groups;
}
@Override
protected void write(String group, Collection<Metric<?>> values) {
if (this.writer instanceof MultiMetricRepository && !values.isEmpty()) {
((MultiMetricRepository) this.writer).save(group, values);
}
else {
for (Metric<?> value : values) {
this.writer.set(value);
}
}
}
}
/*
* 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
* <code>foo</code> is stored as
* <code>[foo.min, foo.max. foo.val, foo.count, foo.avg, foo.alpha]</code>. 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<Metric<?>> next(String group) {
RichGauge rich = this.reader.findOne(group);
Collection<Metric<?>> metrics = new ArrayList<Metric<?>>();
metrics.add(new Metric<Number>(group + MIN, rich.getMin()));
metrics.add(new Metric<Number>(group + MAX, rich.getMax()));
metrics.add(new Metric<Number>(group + COUNT, rich.getCount()));
metrics.add(new Metric<Number>(group + VALUE, rich.getValue()));
metrics.add(new Metric<Number>(group + AVG, rich.getAverage()));
metrics.add(new Metric<Number>(group + ALPHA, rich.getAlpha()));
return metrics;
}
@Override
protected Iterable<String> groups() {
Collection<String> names = new HashSet<String>();
for (RichGauge rich : this.reader.findAll()) {
names.add(rich.getName());
}
return names;
}
@Override
protected void write(String group, Collection<Metric<?>> values) {
if (this.writer instanceof MultiMetricRepository) {
((MultiMetricRepository) this.writer).save(group, values);
}
else {
for (Metric<?> value : values) {
this.writer.set(value);
}
}
}
}
/*
* 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<Metric<?>> findAll();
long count();
}
/*
* 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<Metric<?>> findAll(String prefix);
}
/*
* 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<Metric<?>> metrics = new SimpleInMemoryRepository<Metric<?>>();
private Collection<String> groups = new HashSet<String>();
@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<Metric<?>>() {
@Override
public Metric<?> modify(Metric<?> current) {
if (current != null) {
Metric<? extends Number> metric = current;
return new Metric<Long>(metricName, metric.increment(amount)
.getValue(), timestamp);
}
else {
return new Metric<Long>(metricName, new Long(amount), timestamp);
}
}
});
}
@Override
public void set(Metric<?> value) {
this.metrics.set(value.getName(), value);
}
@Override
public void save(String group, Collection<Metric<?>> values) {
for (Metric<?> metric : values) {
set(metric);
}
this.groups.add(group);
}
@Override
public Iterable<String> 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<Metric<?>> findAll() {
return this.metrics.findAll();
}
@Override
public Iterable<Metric<?>> findAll(String metricNamePrefix) {
return this.metrics.findAllWithPrefix(metricNamePrefix);
}
}
/*
* 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 {
}
/*
* 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<Metric<?>> values);
void reset(String group);
Iterable<String> groups();
long count();
}
/*
* 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<String, String> zSetOperations;
private RedisOperations<String, String> redisOperations;
private ValueOperations<String, Long> longOperations;
public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory) {
Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null");
this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory);
RedisTemplate<String, Long> 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<Metric<?>> findAll() {
// This set is sorted
Set<String> keys = this.zSetOperations.range(0, -1);
Iterator<String> keysIt = keys.iterator();
List<Metric<?>> result = new ArrayList<Metric<?>>(keys.size());
List<String> 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<Double>(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);
}
}
/*
* 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
* <code>[prefix]</code> + "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<String, String> zSetOperations;
private RedisOperations<String, String> 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<Metric<?>> findAll(String metricNamePrefix) {
BoundZSetOperations<String, String> zSetOperations = this.redisOperations
.boundZSetOps(keyFor(metricNamePrefix));
Set<String> keys = zSetOperations.range(0, -1);
Iterator<String> keysIt = keys.iterator();
List<Metric<?>> result = new ArrayList<Metric<?>>(keys.size());
List<String> 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<Metric<?>> values) {
String groupKey = keyFor(group);
trackMembership(groupKey);
BoundZSetOperations<String, String> 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<String> groups() {
Set<String> range = this.zSetOperations.range(0, -1);
Collection<String> result = new ArrayList<String>();
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<String, String> zSetOperations = this.redisOperations
.boundZSetOps(groupKey);
Set<String> 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<Double>(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);
}
}
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 <K, V> RedisTemplate<K, V> createRedisTemplate(
RedisConnectionFactory connectionFactory, Class<V> valueClass) {
RedisTemplate<K, V> redisTemplate = new RedisTemplate<K, V>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer<V>(valueClass));
// avoids proxy
redisTemplate.setExposeConnection(true);
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
static RedisOperations<String, String> stringTemplate(
RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
/*
* 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<RichGauge> repository = new SimpleInMemoryRepository<RichGauge>();
@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<RichGauge>() {
@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<RichGauge> findAll() {
return this.repository.findAll();
}
@Override
public long count() {
return this.repository.count();
}
}
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.
* <p>
* 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 <a
* href="http://www.itl.nist.gov/div898/handbook/pmc/section4/pmc431.htm">NIST
* document</a>.
*
* @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 + "]";
}
}
/*
* 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<RichGauge> findAll();
long count();
}
/*
* 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 {
}
...@@ -14,80 +14,90 @@ ...@@ -14,80 +14,90 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.NavigableMap;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/** /**
* {@link MetricRepository} implementation that stores metric in-memory. * Repository utility that stores stuff in memory with period-separated String keys.
* *
* @author Dave Syer * @author Dave Syer
*/ */
public class InMemoryMetricRepository implements MetricRepository { public class SimpleInMemoryRepository<T> {
private ConcurrentMap<String, Measurement> metrics = new ConcurrentHashMap<String, Measurement>(); private ConcurrentNavigableMap<String, T> values = new ConcurrentSkipListMap<String, T>();
private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<String, Object>(); private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<String, Object>();
@Override public static interface Callback<T> {
public void increment(String metricName, int amount, Date timestamp) { T modify(T current);
Object lock = this.locks.putIfAbsent(metricName, new Object()); }
public T update(String name, Callback<T> callback) {
Object lock = this.locks.putIfAbsent(name, new Object());
if (lock == null) { if (lock == null) {
lock = this.locks.get(metricName); lock = this.locks.get(name);
} }
synchronized (lock) { synchronized (lock) {
Measurement current = this.metrics.get(metricName); T current = this.values.get(name);
T value = callback.modify(current);
if (current != null) { if (current != null) {
Metric metric = current.getMetric(); this.values.replace(name, current, value);
this.metrics.replace(metricName, current, new Measurement(timestamp,
metric.increment(amount)));
return;
} }
else { else {
this.metrics.putIfAbsent(metricName, new Measurement(timestamp, this.values.putIfAbsent(name, value);
new Metric(metricName, amount)));
} }
return this.values.get(name);
} }
} }
@Override public void set(String name, T value) {
public void set(String metricName, double value, Date timestamp) { T current = this.values.get(name);
Measurement current = this.metrics.get(metricName);
if (current != null) { if (current != null) {
Metric metric = current.getMetric(); this.values.replace(name, current, value);
this.metrics.replace(metricName, current,
new Measurement(timestamp, metric.set(value)));
} }
else { else {
this.metrics.putIfAbsent(metricName, new Measurement(timestamp, new Metric( this.values.putIfAbsent(name, value);
metricName, value)));
} }
} }
@Override public long count() {
public void delete(String metricName) { return this.values.size();
this.metrics.remove(metricName);
} }
@Override public void remove(String name) {
public Metric findOne(String metricName) { this.values.remove(name);
if (this.metrics.containsKey(metricName)) { }
return this.metrics.get(metricName).getMetric();
public T findOne(String name) {
if (this.values.containsKey(name)) {
return this.values.get(name);
} }
return new Metric(metricName, 0); return null;
} }
@Override public Iterable<T> findAll() {
public Collection<Metric> findAll() { return new ArrayList<T>(this.values.values());
ArrayList<Metric> result = new ArrayList<Metric>(); }
for (Measurement measurement : this.metrics.values()) {
result.add(measurement.getMetric()); public Iterable<T> findAllWithPrefix(String prefix) {
if (prefix.endsWith(".*")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (!prefix.endsWith(".")) {
prefix = prefix + ".";
} }
return result; return new ArrayList<T>(this.values.subMap(prefix, false, prefix + "~", true)
.values());
}
protected NavigableMap<String, T> getValues() {
return this.values;
} }
} }
/*
* 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:
* <ul>
* <li>Updates to {@link #increment(Delta)} with names in "meter.*" are treated as
* {@link Meter} events</li>
* <li>Other deltas are treated as simple {@link Counter} values</li>
* <li>Inputs to {@link #set(Metric)} with names in "histogram.*" are treated as
* {@link Histogram} updates</li>
* <li>Inputs to {@link #set(Metric)} with names in "timer.*" are treated as {@link Timer}
* updates</li>
* <li>Other metrics are treated as simple {@link Gauge} values (single valued
* measurements of type double)</li>
* </ul>
*
* @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<Double> {
private final double gauge;
private SimpleGauge(double gauge) {
this.gauge = gauge;
}
@Override
public Double getValue() {
return this.gauge;
}
}
}
/*
* 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<MetricWriter> writers = new ArrayList<MetricWriter>();
public CompositeMetricWriter(MetricWriter... writers) {
for (MetricWriter writer : writers) {
this.writers.add(writer);
}
}
public CompositeMetricWriter(List<MetricWriter> 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);
}
}
}
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
* limitations under the License. * 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}. * Default implementation of {@link CounterService}.
...@@ -25,34 +25,33 @@ import java.util.Date; ...@@ -25,34 +25,33 @@ import java.util.Date;
*/ */
public class DefaultCounterService implements CounterService { public class DefaultCounterService implements CounterService {
private MetricRepository repository; private final MetricWriter writer;
/** /**
* Create a {@link DefaultCounterService} instance. * 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) { public DefaultCounterService(MetricWriter writer) {
super(); this.writer = writer;
this.repository = repository;
} }
@Override @Override
public void increment(String metricName) { public void increment(String metricName) {
this.repository.increment(wrap(metricName), 1, new Date()); this.writer.increment(new Delta<Long>(wrap(metricName), 1L));
} }
@Override @Override
public void decrement(String metricName) { public void decrement(String metricName) {
this.repository.increment(wrap(metricName), -1, new Date()); this.writer.increment(new Delta<Long>(wrap(metricName), -1L));
} }
@Override @Override
public void reset(String metricName) { public void reset(String metricName) {
this.repository.set(wrap(metricName), 0, new Date()); this.writer.increment(new Delta<Long>(wrap(metricName), 0L));
} }
private String wrap(String metricName) { private String wrap(String metricName) {
if (metricName.startsWith("counter")) { if (metricName.startsWith("counter") || metricName.startsWith("meter")) {
return metricName; return metricName;
} }
else { else {
......
...@@ -14,9 +14,10 @@ ...@@ -14,9 +14,10 @@
* limitations under the License. * 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}. * Default implementation of {@link GaugeService}.
...@@ -25,24 +26,24 @@ import java.util.Date; ...@@ -25,24 +26,24 @@ import java.util.Date;
*/ */
public class DefaultGaugeService implements GaugeService { public class DefaultGaugeService implements GaugeService {
private MetricRepository metricRepository; private final MetricWriter writer;
/** /**
* Create a new {@link DefaultGaugeService} instance. * Create a {@link DefaultCounterService} instance.
* @param counterRepository * @param writer the underlying writer used to manage metrics
*/ */
public DefaultGaugeService(MetricRepository counterRepository) { public DefaultGaugeService(MetricWriter writer) {
super(); this.writer = writer;
this.metricRepository = counterRepository;
} }
@Override @Override
public void set(String metricName, double value) { public void submit(String metricName, double value) {
this.metricRepository.set(wrap(metricName), value, new Date()); this.writer.set(new Metric<Double>(wrap(metricName), value));
} }
private String wrap(String metricName) { private String wrap(String metricName) {
if (metricName.startsWith("gauge")) { if (metricName.startsWith("gauge") || metricName.startsWith("histogram")
|| metricName.startsWith("timer")) {
return metricName; return metricName;
} }
else { else {
......
/*
* 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<T extends Number> extends Metric<T> {
public Delta(String name, T value, Date timestamp) {
super(name, value, timestamp);
}
public Delta(String name, T value) {
super(name, value);
}
}
...@@ -14,66 +14,47 @@ ...@@ -14,66 +14,47 @@
* limitations under the License. * 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.Metric;
import org.springframework.messaging.MessageChannel;
import org.springframework.util.ObjectUtils; import org.springframework.messaging.support.MessageBuilder;
/** /**
* A {@link Metric} at a given point in time. * 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 * @author Dave Syer
*/ */
public final class Measurement { public class MessageChannelMetricWriter implements MetricWriter {
private Date timestamp;
private Metric metric; private static final String METRIC_NAME = "metricName";
public Measurement(Date timestamp, Metric metric) { private String DELETE = "delete";
this.timestamp = timestamp;
this.metric = metric;
}
public Date getTimestamp() { private final MessageChannel channel;
return this.timestamp;
}
public Metric getMetric() { public MessageChannelMetricWriter(MessageChannel channel) {
return this.metric; this.channel = channel;
} }
@Override @Override
public String toString() { public void increment(Delta<?> delta) {
return "Measurement [dateTime=" + this.timestamp + ", metric=" + this.metric this.channel.send(MessageBuilder.withPayload(delta)
+ "]"; .setHeader(METRIC_NAME, delta.getName()).build());
} }
@Override @Override
public int hashCode() { public void set(Metric<?> value) {
final int prime = 31; this.channel.send(MessageBuilder.withPayload(value)
int result = 1; .setHeader(METRIC_NAME, value.getName()).build());
result = prime * result + ObjectUtils.nullSafeHashCode(this.timestamp);
result = prime * result + ObjectUtils.nullSafeHashCode(this.metric);
return result;
} }
@Override @Override
public boolean equals(Object obj) { public void reset(String metricName) {
if (this == obj) { this.channel.send(MessageBuilder.withPayload(this.DELETE)
return true; .setHeader(METRIC_NAME, metricName).build());
}
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);
} }
} }
/*
* 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);
}
/*
* 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
...@@ -64,7 +64,7 @@ public class MetricFilterAutoConfigurationTests { ...@@ -64,7 +64,7 @@ public class MetricFilterAutoConfigurationTests {
}).given(chain).doFilter(request, response); }).given(chain).doFilter(request, response);
filter.doFilter(request, response, chain); filter.doFilter(request, response, chain);
verify(context.getBean(CounterService.class)).increment("status.200.test.path"); 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()); anyDouble());
context.close(); context.close();
} }
......
...@@ -16,34 +16,79 @@ ...@@ -16,34 +16,79 @@
package org.springframework.boot.actuate.autoconfigure; package org.springframework.boot.actuate.autoconfigure;
import java.util.concurrent.Executor;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration;
import org.springframework.boot.actuate.metrics.CounterService; 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.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.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/** /**
* Tests for {@link MetricRepositoryAutoConfiguration}. * Tests for {@link MetricRepositoryAutoConfiguration}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Dave Syer
*/ */
public class MetricRepositoryAutoConfigurationTests { public class MetricRepositoryAutoConfigurationTests {
@Test @Test
public void createServices() { public void createServices() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SyncTaskExecutorConfiguration.class,
MetricRepositoryAutoConfiguration.class); MetricRepositoryAutoConfiguration.class);
assertNotNull(context.getBean(DefaultGaugeService.class)); DefaultGaugeService gaugeService = context.getBean(DefaultGaugeService.class);
assertNotNull(gaugeService);
assertNotNull(context.getBean(DefaultCounterService.class)); 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<Double> gauge = (Gauge<Double>) registry.getMetrics().get("gauge.foo");
assertEquals(new Double(2.7), gauge.getValue());
context.close(); context.close();
} }
...@@ -56,6 +101,26 @@ public class MetricRepositoryAutoConfigurationTests { ...@@ -56,6 +101,26 @@ public class MetricRepositoryAutoConfigurationTests {
context.close(); 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 @Configuration
public static class Config { public static class Config {
......
...@@ -41,7 +41,7 @@ public class MetricsEndpointTests extends AbstractEndpointTests<MetricsEndpoint> ...@@ -41,7 +41,7 @@ public class MetricsEndpointTests extends AbstractEndpointTests<MetricsEndpoint>
@Test @Test
public void invoke() throws Exception { public void invoke() throws Exception {
assertThat(getEndpointBean().invoke().get("a"), equalTo((Object) 0.5)); assertThat(getEndpointBean().invoke().get("a"), equalTo((Object) 0.5f));
} }
@Configuration @Configuration
...@@ -50,11 +50,11 @@ public class MetricsEndpointTests extends AbstractEndpointTests<MetricsEndpoint> ...@@ -50,11 +50,11 @@ public class MetricsEndpointTests extends AbstractEndpointTests<MetricsEndpoint>
@Bean @Bean
public MetricsEndpoint endpoint() { public MetricsEndpoint endpoint() {
final Metric metric = new Metric("a", 0.5f); final Metric<Float> metric = new Metric<Float>("a", 0.5f);
PublicMetrics metrics = new PublicMetrics() { PublicMetrics metrics = new PublicMetrics() {
@Override @Override
public Collection<Metric> metrics() { public Collection<Metric<?>> metrics() {
return Collections.singleton(metric); return Collections.<Metric<?>> singleton(metric);
} }
}; };
return new MetricsEndpoint(metrics); return new MetricsEndpoint(metrics);
......
...@@ -21,9 +21,8 @@ import java.util.HashMap; ...@@ -21,9 +21,8 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.junit.Test; 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.Metric;
import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
...@@ -39,14 +38,14 @@ public class VanillaPublicMetricsTests { ...@@ -39,14 +38,14 @@ public class VanillaPublicMetricsTests {
@Test @Test
public void testMetrics() throws Exception { public void testMetrics() throws Exception {
InMemoryMetricRepository repository = new InMemoryMetricRepository(); InMemoryMetricRepository repository = new InMemoryMetricRepository();
repository.set("a", 0.5, new Date()); repository.set(new Metric<Double>("a", 0.5, new Date()));
VanillaPublicMetrics publicMetrics = new VanillaPublicMetrics(repository); VanillaPublicMetrics publicMetrics = new VanillaPublicMetrics(repository);
Map<String, Metric> results = new HashMap<String, Metric>(); Map<String, Metric<?>> results = new HashMap<String, Metric<?>>();
for (Metric metric : publicMetrics.metrics()) { for (Metric<?> metric : publicMetrics.metrics()) {
results.put(metric.getName(), metric); results.put(metric.getName(), metric);
} }
assertTrue(results.containsKey("mem")); assertTrue(results.containsKey("mem"));
assertTrue(results.containsKey("mem.free")); assertTrue(results.containsKey("mem.free"));
assertThat(results.get("a").getValue(), equalTo(0.5)); assertThat(results.get("a").getValue().doubleValue(), equalTo(0.5));
} }
} }
...@@ -16,24 +16,23 @@ ...@@ -16,24 +16,23 @@
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Date;
/** /**
* A Repository used to manage {@link Metric}s.
*
* @author Dave Syer * @author Dave Syer
*/ */
public interface MetricRepository { public abstract class Iterables {
void increment(String metricName, int amount, Date timestamp); public static <T> Collection<T> collection(Iterable<T> iterable) {
if (iterable instanceof Collection) {
void set(String metricName, double value, Date timestamp); return (Collection<T>) iterable;
}
void delete(String metricName); ArrayList<T> list = new ArrayList<T>();
for (T t : iterable) {
Metric findOne(String metricName); list.add(t);
}
Collection<Metric> findAll(); return list;
}
} }
/*
* 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<Number>("foo", 2.3));
this.exporter.export();
assertEquals(1, this.writer.count());
}
@Test
public void timestamp() {
this.reader.set(new Metric<Number>("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<Number>("foo", 2.3));
this.exporter.setIgnoreTimestamps(true);
this.exporter.setEarliestTimestamp(new Date(System.currentTimeMillis() + 10000));
this.exporter.export();
assertEquals(1, this.writer.count());
}
}
/*
* 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<Number>("foo.bar", 2.3));
this.reader.set(new Metric<Number>("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<Number>("foo.bar", 2.3));
this.reader.set(new Metric<Number>("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<Number>("foo.bar", 2.3));
this.reader.set(new Metric<Number>("foo.spam", 1.3));
this.reader.set(new Metric<Number>("foobar.spam", 1.3));
this.exporter.setGroups(Collections.singleton("foo"));
this.exporter.export();
assertEquals(1, Iterables.collection(this.writer.groups()).size());
}
}
/*
* 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<Number>("foo", 2.3));
this.exporter.export();
assertEquals(1, Iterables.collection(this.writer.groups()).size());
}
}
/*
* 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<Integer>("foo", 1, new Date()));
assertEquals(1.0, this.repository.findOne("foo").getValue().doubleValue(), 0.01);
}
@Test
public void set() {
this.repository.set(new Metric<Double>("foo", 2.5, new Date()));
assertEquals(2.5, this.repository.findOne("foo").getValue().doubleValue(), 0.01);
}
}
/*
* 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<Number>("foo.bar", 1));
this.repository.increment(new Delta<Number>("foo.bar", 1));
this.repository.increment(new Delta<Number>("foo.spam", 1));
Set<String> names = new HashSet<String>();
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<Number>("foo.bar", 1));
Set<String> names = new HashSet<String>();
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<Number>("foo.bar", 1));
Set<String> names = new HashSet<String>();
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<Number>("foo.bar", 1));
this.repository.increment(new Delta<Number>("foobar.spam", 1));
Set<String> names = new HashSet<String>();
for (Metric<?> metric : this.repository.findAll("foo")) {
names.add(metric.getName());
}
assertEquals(1, names.size());
assertTrue(names.contains("foo.bar"));
}
}
/*
* 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<Number>("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<Long>("foo", 3L));
assertEquals(3, this.repository.findOne("foo").getValue().longValue());
}
@Test
public void findAll() {
this.repository.increment(new Delta<Long>("foo", 3L));
this.repository.set(new Metric<Number>("bar", 12.3));
assertEquals(2, Iterables.collection(this.repository.findAll()).size());
}
@Test
public void findOneWithAll() {
this.repository.increment(new Delta<Long>("foo", 3L));
Metric<?> metric = this.repository.findAll().iterator().next();
assertEquals("foo", metric.getName());
}
@Test
public void count() {
this.repository.increment(new Delta<Long>("foo", 3L));
this.repository.set(new Metric<Number>("bar", 12.3));
assertEquals(2, this.repository.count());
}
}
/*
* 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.<Metric<?>> asList(new Metric<Number>(
"foo.val", 12.3), new Metric<Number>("foo.bar", 11.3)));
assertEquals(2, Iterables.collection(this.repository.findAll("foo")).size());
}
@Test
public void groups() {
this.repository.save("foo", Arrays.<Metric<?>> asList(new Metric<Number>(
"foo.val", 12.3), new Metric<Number>("foo.bar", 11.3)));
this.repository.save("bar", Arrays.<Metric<?>> asList(new Metric<Number>(
"bar.val", 12.3), new Metric<Number>("bar.foo", 11.3)));
assertEquals(2, Iterables.collection(this.repository.groups()).size());
}
@Test
public void count() {
this.repository.save("foo", Arrays.<Metric<?>> asList(new Metric<Number>(
"foo.val", 12.3), new Metric<Number>("foo.bar", 11.3)));
this.repository.save("bar", Arrays.<Metric<?>> asList(new Metric<Number>(
"bar.val", 12.3), new Metric<Number>("bar.foo", 11.3)));
assertEquals(2, this.repository.count());
}
}
/*
* 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;
}
}
/*
* 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<Double>("foo", 1d));
this.repository.set(new Metric<Double>("foo", 2d));
assertEquals(2L, this.repository.findOne("foo").getCount());
assertEquals(2d, this.repository.findOne("foo").getValue(), 0.01);
}
}
...@@ -14,11 +14,11 @@ ...@@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
...@@ -26,40 +26,97 @@ import java.util.concurrent.Future; ...@@ -26,40 +26,97 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.actuate.metrics.util.SimpleInMemoryRepository.Callback;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
* Tests for {@link InMemoryMetricRepository}. * @author Dave Syer
*/ */
public class InMemoryMetricRepositoryTests { public class InMemoryRepositoryTests {
private InMemoryMetricRepository repository = new InMemoryMetricRepository(); private SimpleInMemoryRepository<String> repository = new SimpleInMemoryRepository<String>();
@Test @Test
public void increment() { public void setAndGet() {
this.repository.increment("foo", 1, new Date()); this.repository.set("foo", "bar");
assertEquals(1.0, this.repository.findOne("foo").getValue(), 0.01); assertEquals("bar", this.repository.findOne("foo"));
} }
@Test @Test
public void incrementConcurrent() throws Exception { public void updateExisting() {
this.repository.set("foo", "spam");
this.repository.update("foo", new Callback<String>() {
@Override
public String modify(String current) {
return "bar";
}
});
assertEquals("bar", this.repository.findOne("foo"));
}
@Test
public void updateNonexistent() {
this.repository.update("foo", new Callback<String>() {
@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<String> iterator = this.repository.findAllWithPrefix("foo.*").iterator();
assertEquals("spam", iterator.next());
assertFalse(iterator.hasNext());
}
@Test
public void updateConcurrent() throws Exception {
final SimpleInMemoryRepository<Integer> repository = new SimpleInMemoryRepository<Integer>();
Collection<Callable<Boolean>> tasks = new ArrayList<Callable<Boolean>>(); Collection<Callable<Boolean>> tasks = new ArrayList<Callable<Boolean>>();
for (int i = 0; i < 100; i++) { for (int i = 0; i < 1000; i++) {
tasks.add(new Callable<Boolean>() { tasks.add(new Callable<Boolean>() {
@Override @Override
public Boolean call() throws Exception { public Boolean call() throws Exception {
InMemoryMetricRepositoryTests.this.repository.increment("foo", 1, repository.update("foo", new Callback<Integer>() {
new Date()); @Override
public Integer modify(Integer current) {
if (current == null) {
return 1;
}
return current + 1;
}
});
return true; return true;
} }
}); });
tasks.add(new Callable<Boolean>() { tasks.add(new Callable<Boolean>() {
@Override @Override
public Boolean call() throws Exception { public Boolean call() throws Exception {
InMemoryMetricRepositoryTests.this.repository.increment("foo", -1, repository.update("foo", new Callback<Integer>() {
new Date()); @Override
public Integer modify(Integer current) {
if (current == null) {
return -1;
}
return current - 1;
}
});
return true; return true;
} }
}); });
...@@ -68,13 +125,7 @@ public class InMemoryMetricRepositoryTests { ...@@ -68,13 +125,7 @@ public class InMemoryMetricRepositoryTests {
for (Future<Boolean> future : all) { for (Future<Boolean> future : all) {
assertTrue(future.get(1, TimeUnit.SECONDS)); assertTrue(future.get(1, TimeUnit.SECONDS));
} }
assertEquals(0, this.repository.findOne("foo").getValue(), 0.01); assertEquals(new Integer(0), repository.findOne("foo"));
}
@Test
public void set() {
this.repository.set("foo", 1, new Date());
assertEquals(1.0, this.repository.findOne("foo").getValue(), 0.01);
} }
} }
/*
* 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<Number>("foo", 2));
this.writer.increment(new Delta<Number>("foo", 1));
assertEquals(3, this.registry.counter("foo").getCount());
}
@Test
public void updatePredefinedMeter() {
this.writer.increment(new Delta<Number>("meter.foo", 2));
this.writer.increment(new Delta<Number>("meter.foo", 1));
assertEquals(3, this.registry.meter("meter.foo").getCount());
}
@Test
public void updatePredefinedCounter() {
this.writer.increment(new Delta<Number>("counter.foo", 2));
this.writer.increment(new Delta<Number>("counter.foo", 1));
assertEquals(3, this.registry.counter("counter.foo").getCount());
}
@Test
public void setGauge() {
this.writer.set(new Metric<Number>("foo", 2.1));
this.writer.set(new Metric<Number>("foo", 2.3));
@SuppressWarnings("unchecked")
Gauge<Double> gauge = (Gauge<Double>) this.registry.getMetrics().get("foo");
assertEquals(new Double(2.3), gauge.getValue());
}
@Test
public void setPredfinedTimer() {
this.writer.set(new Metric<Number>("timer.foo", 200));
this.writer.set(new Metric<Number>("timer.foo", 300));
assertEquals(2, this.registry.timer("timer.foo").getCount());
}
@Test
public void setPredfinedHistogram() {
this.writer.set(new Metric<Number>("histogram.foo", 2.1));
this.writer.set(new Metric<Number>("histogram.foo", 2.3));
assertEquals(2, this.registry.histogram("histogram.foo").getCount());
}
}
...@@ -14,14 +14,15 @@ ...@@ -14,14 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics.writer;
import java.util.Date;
import org.junit.Test; 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.junit.Assert.assertEquals;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
...@@ -30,19 +31,26 @@ import static org.mockito.Mockito.verify; ...@@ -30,19 +31,26 @@ import static org.mockito.Mockito.verify;
*/ */
public class DefaultCounterServiceTests { public class DefaultCounterServiceTests {
private MetricRepository repository = mock(MetricRepository.class); private MetricWriter repository = mock(MetricWriter.class);
private DefaultCounterService service = new DefaultCounterService(this.repository); private DefaultCounterService service = new DefaultCounterService(this.repository);
@Test @Test
public void incrementPrependsCounter() { public void incrementPrependsCounter() {
this.service.increment("foo"); this.service.increment("foo");
verify(this.repository).increment(eq("counter.foo"), eq(1), any(Date.class)); @SuppressWarnings("rawtypes")
ArgumentCaptor<Delta> captor = ArgumentCaptor.forClass(Delta.class);
verify(this.repository).increment(captor.capture());
assertEquals("counter.foo", captor.getValue().getName());
} }
@Test @Test
public void decrementPrependsCounter() { public void decrementPrependsCounter() {
this.service.decrement("foo"); this.service.decrement("foo");
verify(this.repository).increment(eq("counter.foo"), eq(-1), any(Date.class)); @SuppressWarnings("rawtypes")
ArgumentCaptor<Delta> captor = ArgumentCaptor.forClass(Delta.class);
verify(this.repository).increment(captor.capture());
assertEquals("counter.foo", captor.getValue().getName());
assertEquals(-1L, captor.getValue().getValue());
} }
} }
...@@ -14,14 +14,13 @@ ...@@ -14,14 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics.writer;
import java.util.Date;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.actuate.metrics.Metric;
import static org.mockito.Matchers.any; import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
...@@ -30,14 +29,18 @@ import static org.mockito.Mockito.verify; ...@@ -30,14 +29,18 @@ import static org.mockito.Mockito.verify;
*/ */
public class DefaultGaugeServiceTests { public class DefaultGaugeServiceTests {
private MetricRepository repository = mock(MetricRepository.class); private MetricWriter repository = mock(MetricWriter.class);
private DefaultGaugeService service = new DefaultGaugeService(this.repository); private DefaultGaugeService service = new DefaultGaugeService(this.repository);
@Test @Test
public void setPrependsGuager() { public void setPrependsGauge() {
this.service.set("foo", 2.3); this.service.submit("foo", 2.3);
verify(this.repository).set(eq("gauge.foo"), eq(2.3), any(Date.class)); @SuppressWarnings("rawtypes")
ArgumentCaptor<Metric> captor = ArgumentCaptor.forClass(Metric.class);
verify(this.repository).set(captor.capture());
assertEquals("gauge.foo", captor.getValue().getName());
assertEquals(2.3, captor.getValue().getValue());
} }
} }
/*
* 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<Integer>("foo", 1));
verify(this.channel).send(any(Message.class));
}
@Test
public void messageSentOnSet() {
this.observer.set(new Metric<Double>("foo", 1d));
verify(this.channel).send(any(Message.class));
}
}
...@@ -35,6 +35,8 @@ import org.eclipse.aether.repository.RemoteRepository; ...@@ -35,6 +35,8 @@ import org.eclipse.aether.repository.RemoteRepository;
/** /**
* (Copied from aether source code - not available yet in Maven repo.) * (Copied from aether source code - not available yet in Maven repo.)
*
* @author Dave Syer
*/ */
public final class JreProxySelector implements ProxySelector { public final class JreProxySelector implements ProxySelector {
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<properties> <properties>
<activemq.version>5.7.0</activemq.version> <activemq.version>5.7.0</activemq.version>
<aspectj.version>1.7.4</aspectj.version> <aspectj.version>1.7.4</aspectj.version>
<codahale-metrics.version>3.0.1</codahale-metrics.version>
<commons-dbcp.version>1.4</commons-dbcp.version> <commons-dbcp.version>1.4</commons-dbcp.version>
<commons-pool.version>1.6</commons-pool.version> <commons-pool.version>1.6</commons-pool.version>
<gradle.version>1.6</gradle.version> <gradle.version>1.6</gradle.version>
...@@ -60,6 +61,26 @@ ...@@ -60,6 +61,26 @@
<artifactId>logback-classic</artifactId> <artifactId>logback-classic</artifactId>
<version>${logback.version}</version> <version>${logback.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-graphite</artifactId>
<version>${codahale-metrics.version}</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-ganglia</artifactId>
<version>${codahale-metrics.version}</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>${codahale-metrics.version}</version>
</dependency>
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-servlets</artifactId>
<version>${codahale-metrics.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
......
...@@ -20,11 +20,13 @@ import java.util.Collections; ...@@ -20,11 +20,13 @@ import java.util.Collections;
import java.util.Map; import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Description;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
@Controller @Controller
@Description("A controller for handling requests for hello messages")
public class SampleController { public class SampleController {
@Autowired @Autowired
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment