Add AggregateMetricReader able to aggregate counters and gauges

Different physical sources for the same logical metric just need to
publish them with a period-separated prefix, and this reader will
aggregate (by truncating the metric names, dropping the prefix).

Very useful (for instance) if multiple application instances are
feeding to a central (e.g. redis) repository and you want to
display the results. Useful in conjunction with a
MetricReaderPublicMetrics for hooking up to the /metrics endpoint.
This commit is contained in:
Dave Syer 2015-04-23 10:36:21 +01:00 committed by Andy Wilkinson
parent 710423d176
commit 0fde04d325
3 changed files with 275 additions and 10 deletions

View File

@ -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 <code>*.*.counter.**</code> and
* <code>*.*.[anything].**</code> (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
* <code>aggregate.count.**</code> and <code>aggregate.[anything].**</code>. 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<Metric<?>> 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<String> names = new HashSet<String>();
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<Number>(name, metric.getValue(), metric.getTimestamp());
}
else if (key.startsWith("counter")) {
// accumulate all values
aggregate = new Metric<Number>(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<Number>(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();
}
}

View File

@ -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<Double>("foo.bar.spam", 2.3));
assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
}
@Test
public void writeAndReadLatestValue() {
this.source.set(new Metric<Double>("foo.bar.spam", 2.3, new Date(100L)));
this.source.set(new Metric<Double>("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<Double>("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<Double>("foo.bar.spam", 2.3));
assertNull(this.reader.findOne("spam"));
}
@Test
public void incrementCounter() {
this.source.increment(new Delta<Long>("foo.bar.counter.spam", 2L));
this.source.increment(new Delta<Long>("oof.rab.counter.spam", 3L));
assertEquals(5L, this.reader.findOne("aggregate.counter.spam").getValue());
}
@Test
public void countGauges() {
this.source.set(new Metric<Double>("foo.bar.spam", 2.3));
this.source.set(new Metric<Double>("oof.rab.spam", 2.4));
assertEquals(1, this.reader.count());
}
@Test
public void countGaugesAndCounters() {
this.source.set(new Metric<Double>("foo.bar.spam", 2.3));
this.source.set(new Metric<Double>("oof.rab.spam", 2.4));
this.source.increment(new Delta<Long>("foo.bar.counter.spam", 2L));
this.source.increment(new Delta<Long>("oof.rab.counter.spam", 3L));
assertEquals(2, this.reader.count());
}
}

View File

@ -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`