/*
* Copyright 2012-2017 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.**} and
* {@code *.*.[anything].**}, and the result has metric names in the form
* {@code aggregate.count.**} and {@code aggregate.[anything].**}. Counters are summed and
* anything else (i.e. gauges) are aggregated by choosing the most recent value.
*
* @author Dave Syer
* @since 1.3.0
*/
public class AggregateMetricReader implements MetricReader {
private MetricReader source;
private String keyPattern = "d.d";
private String prefix = "aggregate.";
public AggregateMetricReader(MetricReader source) {
this.source = source;
}
/**
* Pattern that tells the aggregator what to do with the keys from the source
* repository. The keys in the source repository are assumed to be period separated,
* and the pattern is in the same format, e.g. "d.d.k.d". The pattern segments are
* matched against the source keys and a rule is applied:
* <ul>
* <li>"d" means "discard" this key segment (useful for global prefixes like system
* identifiers, or aggregate keys a.k.a. physical identifiers)</li>
* <li>"k" means "keep" it with no change (useful for logical identifiers like app
* names)</li>
* </ul>
* Default is "d.d" (we assume there is a global prefix of length 2).
* @param keyPattern the keyPattern to set
*/
public void setKeyPattern(String keyPattern) {
this.keyPattern = keyPattern;
}
/**
* Prefix to apply to all output metrics. A period will be appended if not present in
* the provided value.
* @param prefix the prefix to use (default "aggregator.")
*/
public void setPrefix(String prefix) {
if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) {
prefix = prefix + ".";
}
this.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<>();
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.contains("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, ".");
String[] patterns = StringUtils.delimitedListToStringArray(this.keyPattern, ".");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < patterns.length; i++) {
if ("k".equals(patterns[i])) {
builder.append(builder.length() > 0 ? "." : "");
builder.append(keys[i]);
}
}
for (int i = patterns.length; i < keys.length; i++) {
builder.append(builder.length() > 0 ? "." : "");
builder.append(keys[i]);
}
return builder.toString();
}
}