diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java
new file mode 100644
index 00000000000..b3cb11f7305
--- /dev/null
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2014-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.boot.actuate.metrics.aggregate;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.boot.actuate.metrics.Metric;
+import org.springframework.boot.actuate.metrics.reader.MetricReader;
+import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
+import org.springframework.util.StringUtils;
+
+/**
+ * A metric reader that aggregates values from a source reader, normally one that has been
+ * collecting data from many sources in the same form (like a scaled-out application). The
+ * source has metrics with names in the form *.*.counter.** and
+ * *.*.[anything].** (the length of the prefix is controlled by the
+ * {@link #setTruncateKeyLength(int) truncateKeyLength} property, and defaults to 2,
+ * meaning 2 period separated fields), and the result has metric names in the form
+ * aggregate.count.** and aggregate.[anything].**. Counters are
+ * summed and anything else (i.e. gauges) are aggregated by choosing the most recent
+ * value.
+ *
+ * @author Dave Syer
+ *
+ */
+public class AggregateMetricReader implements MetricReader {
+
+ private MetricReader source;
+
+ private int truncate = 2;
+
+ private String prefix = "aggregate.";
+
+ public AggregateMetricReader(MetricReader source) {
+ this.source = source;
+ }
+
+ /**
+ * The number of period-separated keys to remove from the start of the input metric
+ * names before aggregating.
+ *
+ * @param truncate length of source metric prefixes
+ */
+ public void setTruncateKeyLength(int truncate) {
+ this.truncate = truncate;
+ }
+
+ /**
+ * Prefix to apply to all output metrics. A period will be appended if no present in
+ * the provided value.
+ *
+ * @param prefix the prefix to use default "aggregator.")
+ */
+ public void setPrefix(String prefix) {
+ this.prefix = prefix.endsWith(".") ? prefix : prefix + ".";
+ }
+
+ @Override
+ public Metric> findOne(String metricName) {
+ if (!metricName.startsWith(this.prefix)) {
+ return null;
+ }
+ InMemoryMetricRepository result = new InMemoryMetricRepository();
+ String baseName = metricName.substring(this.prefix.length());
+ for (Metric> metric : this.source.findAll()) {
+ String name = getSourceKey(metric.getName());
+ if (baseName.equals(name)) {
+ update(result, name, metric);
+ }
+ }
+ return result.findOne(metricName);
+ }
+
+ @Override
+ public Iterable> findAll() {
+ InMemoryMetricRepository result = new InMemoryMetricRepository();
+ for (Metric> metric : this.source.findAll()) {
+ String key = getSourceKey(metric.getName());
+ if (key != null) {
+ update(result, key, metric);
+ }
+ }
+ return result.findAll();
+ }
+
+ @Override
+ public long count() {
+ Set names = new HashSet();
+ for (Metric> metric : this.source.findAll()) {
+ String name = getSourceKey(metric.getName());
+ if (name != null) {
+ names.add(name);
+ }
+ }
+ return names.size();
+ }
+
+ private void update(InMemoryMetricRepository result, String key, Metric> metric) {
+ String name = this.prefix + key;
+ Metric> aggregate = result.findOne(name);
+ if (aggregate == null) {
+ aggregate = new Metric(name, metric.getValue(), metric.getTimestamp());
+ }
+ else if (key.startsWith("counter")) {
+ // accumulate all values
+ aggregate = new Metric(name, metric.increment(
+ aggregate.getValue().intValue()).getValue(), metric.getTimestamp());
+ }
+ else if (aggregate.getTimestamp().before(metric.getTimestamp())) {
+ // sort by timestamp and only take the latest
+ aggregate = new Metric(name, metric.getValue(), metric.getTimestamp());
+ }
+ result.set(aggregate);
+ }
+
+ private String getSourceKey(String name) {
+ String[] keys = StringUtils.delimitedListToStringArray(name, ".");
+ if (keys.length <= this.truncate) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder(keys[this.truncate]);
+ for (int i = this.truncate + 1; i < keys.length; i++) {
+ builder.append(".").append(keys[i]);
+ }
+ return builder.toString();
+ }
+
+}
diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java
new file mode 100644
index 00000000000..5e3ee336aa3
--- /dev/null
+++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.actuate.metrics.aggregate;
+
+import java.util.Date;
+
+import org.junit.Test;
+import org.springframework.boot.actuate.metrics.Metric;
+import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
+import org.springframework.boot.actuate.metrics.writer.Delta;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author Dave Syer
+ */
+public class AggregateMetricReaderTests {
+
+ private InMemoryMetricRepository source = new InMemoryMetricRepository();
+
+ private AggregateMetricReader reader = new AggregateMetricReader(this.source);
+
+ @Test
+ public void writeAndReadDefaults() {
+ this.source.set(new Metric("foo.bar.spam", 2.3));
+ assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
+ }
+
+ @Test
+ public void writeAndReadLatestValue() {
+ this.source.set(new Metric("foo.bar.spam", 2.3, new Date(100L)));
+ this.source.set(new Metric("oof.rab.spam", 2.4, new Date(0L)));
+ assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
+ }
+
+ @Test
+ public void writeAndReadExtraLong() {
+ this.source.set(new Metric("blee.foo.bar.spam", 2.3));
+ this.reader.setTruncateKeyLength(3);
+ assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
+ }
+
+ @Test
+ public void onlyPrefixed() {
+ this.source.set(new Metric("foo.bar.spam", 2.3));
+ assertNull(this.reader.findOne("spam"));
+ }
+
+ @Test
+ public void incrementCounter() {
+ this.source.increment(new Delta("foo.bar.counter.spam", 2L));
+ this.source.increment(new Delta("oof.rab.counter.spam", 3L));
+ assertEquals(5L, this.reader.findOne("aggregate.counter.spam").getValue());
+ }
+
+ @Test
+ public void countGauges() {
+ this.source.set(new Metric("foo.bar.spam", 2.3));
+ this.source.set(new Metric("oof.rab.spam", 2.4));
+ assertEquals(1, this.reader.count());
+ }
+
+ @Test
+ public void countGaugesAndCounters() {
+ this.source.set(new Metric("foo.bar.spam", 2.3));
+ this.source.set(new Metric("oof.rab.spam", 2.4));
+ this.source.increment(new Delta("foo.bar.counter.spam", 2L));
+ this.source.increment(new Delta("oof.rab.counter.spam", 3L));
+ assertEquals(2, this.reader.count());
+ }
+
+}
diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
index 6761384b0fb..0c5ea7ea821 100644
--- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
+++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
@@ -890,7 +890,7 @@ beans are gathered by the endpoint. You can easily change that by defining your
[[production-ready-metric-repositories]]
-=== Performance
+=== Special features with Java 8
The default implementation of `GaugeService` and `CounterService` provided by Spring Boot
depends on the version of Java that you are using. With Java 8 (or better) the
@@ -944,10 +944,10 @@ Example:
----
@Value("${spring.application.name:application}.${random.value:0000}")
-private String prefix = "metrics;
+private String prefix = "metrics";
@Value("${metrics.key:METRICSKEY}")
-private String key = "KEY;
+private String key = "METRICSKEY";
@Bean
MetricWriter metricWriter() {
@@ -957,15 +957,51 @@ MetricWriter metricWriter() {
+[[production-ready-metric-aggregation]]
+=== Aggregating metrics from multiple sources
+There is an `AggregateMetricReader` that you can use to consolidate metrics from different
+physical sources. Sources for the same logical metric just need to publish them with a
+period-separated prefix, and the reader will aggregate (by truncating the metric names,
+and dropping the prefix). Counters are summed and everything else (i.e. gauges) take their
+most recent value.
+
+This is very useful (for instance) if multiple application instances are feeding to a
+central (e.g. redis) repository and you want to display the results. Particularly
+recommended in conjunction with a `MetricReaderPublicMetrics` for hooking up to the
+results to the "/metrics" endpoint. Example:
+
+[source,java,indent=0]
+----
+ @Bean
+ public PublicMetrics metricsAggregate() {
+ return new MetricReaderPublicMetrics(aggregates());
+ }
+
+ @Bean
+ protected MetricReader repository(RedisConnectionFactory connectionFactory) {
+ RedisMetricRepository repository = new RedisMetricRepository(connectionFactory,
+ "metrics", "METRICSKEY");
+ return repository;
+ }
+
+ @Bean
+ protected MetricReader aggregates() {
+ AggregateMetricReader repository = new AggregateMetricReader(repository());
+ return repository;
+ }
+----
+
+
+
[[production-ready-dropwizard-metrics]]
=== Dropwizard Metrics
-Users of the https://dropwizard.github.io/metrics/[Dropwizard '`Metrics`' library] will
-automatically find that Spring Boot metrics are published to
-`com.codahale.metrics.MetricRegistry`. A default `com.codahale.metrics.MetricRegistry`
-Spring bean will be created when you declare a dependency to the
-`io.dropwizard.metrics:metrics-core` library; you can also register you own `@Bean`
-instance if you need customizations. Metrics from the `MetricRegistry` are also
-automatically exposed via the `/metrics` endpoint.
+A default `MetricRegistry` Spring bean will be created when you declare a dependency to
+the `io.dropwizard.metrics:metric-core` library; you can also register you own `@Bean`
+instance if you need customizations. Users of the
+https://dropwizard.github.io/metrics/[Dropwizard '`Metrics`' library] will find that
+Spring Boot metrics are automatically published to `com.codahale.metrics.MetricRegistry`.
+Metrics from the `MetricRegistry` are also automatically exposed via the `/metrics`
+endpoint
When Dropwizard metrics are in use, the default `CounterService` and `GaugeService` are
replaced with a `DropwizardMetricServices`, which is a wrapper around the `MetricRegistry`