mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
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:
parent
710423d176
commit
0fde04d325
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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`
|
||||
|
Loading…
Reference in New Issue
Block a user