/*
* 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.export;
import java.io.Flushable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.writer.CompositeMetricWriter;
import org.springframework.boot.actuate.metrics.writer.CounterWriter;
import org.springframework.boot.actuate.metrics.writer.Delta;
import org.springframework.boot.actuate.metrics.writer.GaugeWriter;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@link Exporter} that "exports" by copying metric data from a source
* {@link MetricReader} to a destination {@link MetricWriter}. Actually the output writer
* can be a {@link GaugeWriter}, in which case all metrics are simply output as their
* current value. If the output writer is also a {@link CounterWriter} then metrics whose
* names begin with "counter." are special: instead of writing them out as simple gauges
* the writer will increment the counter value. This involves the exporter storing the
* previous value of the counter so the delta can be computed. For best results with the
* counters, do not use the exporter concurrently in multiple threads (normally it will
* only be used periodically and sequentially, even if it is in a background thread, and
* this is fine).
*
* @author Dave Syer
* @since 1.3.0
*/
public class MetricCopyExporter extends AbstractMetricExporter {
private static final Log logger = LogFactory.getLog(MetricCopyExporter.class);
private final MetricReader reader;
private final GaugeWriter writer;
private final CounterWriter counter;
private ConcurrentMap<String, Long> counts = new ConcurrentHashMap<>();
private String[] includes = new String[0];
private String[] excludes = new String[0];
/**
* Create a new {@link MetricCopyExporter} instance.
* @param reader the metric reader
* @param writer the metric writer
*/
public MetricCopyExporter(MetricReader reader, GaugeWriter writer) {
this(reader, writer, "");
}
/**
* Create a new {@link MetricCopyExporter} instance.
* @param reader the metric reader
* @param writer the metric writer
* @param prefix the name prefix
*/
public MetricCopyExporter(MetricReader reader, GaugeWriter writer, String prefix) {
super(prefix);
this.reader = reader;
this.writer = writer;
if (writer instanceof CounterWriter) {
this.counter = (CounterWriter) writer;
}
else {
this.counter = null;
}
}
/**
* Set the include patterns used to filter metrics.
* @param includes the include patterns
*/
public void setIncludes(String... includes) {
if (includes != null) {
this.includes = includes;
}
}
/**
* Set the exclude patterns used to filter metrics.
* @param excludes the exclude patterns
*/
public void setExcludes(String... excludes) {
if (excludes != null) {
this.excludes = excludes;
}
}
@Override
protected Iterable<Metric<?>> next(String group) {
if (ObjectUtils.isEmpty(this.includes) && ObjectUtils.isEmpty(this.excludes)) {
return this.reader.findAll();
}
return new PatternMatchingIterable(MetricCopyExporter.this.reader);
}
@Override
protected void write(String group, Collection<Metric<?>> values) {
for (Metric<?> value : values) {
if (value.getName().startsWith("counter.") && this.counter != null) {
this.counter.increment(calculateDelta(value));
}
else {
this.writer.set(value);
}
}
}
private Delta<?> calculateDelta(Metric<?> value) {
long delta = value.getValue().longValue();
Long old = this.counts.replace(value.getName(), delta);
if (old != null) {
delta = delta - old;
}
else {
this.counts.putIfAbsent(value.getName(), delta);
}
return new Delta<>(value.getName(), delta, value.getTimestamp());
}
@Override
public void flush() {
flush(this.writer);
}
private void flush(GaugeWriter writer) {
if (writer instanceof CompositeMetricWriter) {
for (MetricWriter child : (CompositeMetricWriter) writer) {
flush(child);
}
}
try {
if (ClassUtils.isPresent("java.io.Flushable", null)) {
if (writer instanceof Flushable) {
((Flushable) writer).flush();
return;
}
}
Method method = ReflectionUtils.findMethod(writer.getClass(), "flush");
if (method != null) {
ReflectionUtils.invokeMethod(method, writer);
}
}
catch (Exception ex) {
logger.warn("Could not flush MetricWriter: " + ex.getClass() + ": "
+ ex.getMessage());
}
}
private class PatternMatchingIterable implements Iterable<Metric<?>> {
private final MetricReader reader;
PatternMatchingIterable(MetricReader reader) {
this.reader = reader;
}
@Override
public Iterator<Metric<?>> iterator() {
return new PatternMatchingIterator(this.reader.findAll().iterator());
}
}
private class PatternMatchingIterator implements Iterator<Metric<?>> {
private Metric<?> buffer = null;
private Iterator<Metric<?>> iterator;
PatternMatchingIterator(Iterator<Metric<?>> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
if (this.buffer != null) {
return true;
}
this.buffer = findNext();
return this.buffer != null;
}
private Metric<?> findNext() {
while (this.iterator.hasNext()) {
Metric<?> metric = this.iterator.next();
if (isMatch(metric)) {
return metric;
}
}
return null;
}
private boolean isMatch(Metric<?> metric) {
String[] includes = MetricCopyExporter.this.includes;
String[] excludes = MetricCopyExporter.this.excludes;
String name = metric.getName();
if (ObjectUtils.isEmpty(includes)
|| PatternMatchUtils.simpleMatch(includes, name)) {
return !PatternMatchUtils.simpleMatch(excludes, name);
}
return false;
}
@Override
public Metric<?> next() {
Metric<?> metric = this.buffer;
this.buffer = null;
return metric;
}
};
}