From 0fde04d3252cd456dbf9b0ea3b7b2f226594ecb2 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 23 Apr 2015 10:36:21 +0100 Subject: [PATCH] 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. --- .../aggregate/AggregateMetricReader.java | 142 ++++++++++++++++++ .../aggregate/AggregateMetricReaderTests.java | 87 +++++++++++ .../asciidoc/production-ready-features.adoc | 56 +++++-- 3 files changed, 275 insertions(+), 10 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java 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`