com.netflix.servo.publish.CounterToRateMetricTransform Maven / Gradle / Ivy
/*
* Copyright 2014 Netflix, Inc.
*
* 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 com.netflix.servo.publish;
import com.netflix.servo.Metric;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.tag.Tag;
import com.netflix.servo.tag.TagList;
import com.netflix.servo.util.Clock;
import com.netflix.servo.util.ClockWithOffset;
import com.netflix.servo.util.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Converts counter metrics into a rate per second. The rate is calculated by
* comparing two samples of given metric and looking at the delta. Since two
* samples are needed to calculate the rate, no value will be sent to the
* wrapped observer until a second sample arrives. If a given metric is not
* updated within a given heartbeat interval, the previous cached value for the
* counter will be dropped such that if a new sample comes in it will be
* treated as the first sample for that metric.
*
* Counters should be monotonically increasing values. If a counter value
* decreases from one sample to the next, then we will assume the counter value
* was reset and send a rate of 0. This is similar to the RRD concept of
* type DERIVE with a min of 0.
*
* This class is not thread safe and should generally be wrapped by an async
* observer to prevent issues.
*/
public final class CounterToRateMetricTransform implements MetricObserver {
private static final Logger LOGGER =
LoggerFactory.getLogger(CounterToRateMetricTransform.class);
private static final String COUNTER_VALUE = DataSourceType.COUNTER.name();
private static final Tag RATE_TAG = DataSourceType.RATE;
private final MetricObserver observer;
private final Map cache;
private final long intervalMillis;
/**
* Creates a new instance with the specified heartbeat interval. The
* heartbeat should be some multiple of the sampling interval used when
* collecting the metrics.
*/
public CounterToRateMetricTransform(MetricObserver observer, long heartbeat, TimeUnit unit) {
this(observer, heartbeat, 0L, unit);
}
/**
* Creates a new instance with the specified heartbeat interval. The
* heartbeat should be some multiple of the sampling interval used when
* collecting the metrics.
*
* @param observer downstream observer to forward values to after the rate has
* been computed.
* @param heartbeat how long to remember a previous value before dropping it and
* treating new samples as the first report.
* @param estPollingInterval estimated polling interval in to use for the first call. If set
* to zero no values will be forwarded until the second sample for
* a given counter. The delta for the first interval will be the
* total value for the counter as it is assumed it started at 0 and
* was first created since the last polling interval. If this
* assumption is not true then this setting should be 0 so it waits
* for the next sample to compute an accurate delta, otherwise
* spikes will occur in the output.
* @param unit unit for the heartbeat and estPollingInterval params.
* @param clock Clock instance to use for getting the time.
*/
CounterToRateMetricTransform(
MetricObserver observer, long heartbeat, long estPollingInterval, TimeUnit unit,
final Clock clock) {
this.observer = observer;
this.intervalMillis = TimeUnit.MILLISECONDS.convert(estPollingInterval, unit);
final long heartbeatMillis = TimeUnit.MILLISECONDS.convert(heartbeat, unit);
this.cache = new LinkedHashMap(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
final long now = clock.now();
final long lastMod = eldest.getValue().getTimestamp();
final boolean expired = (now - lastMod) > heartbeatMillis;
if (expired) {
LOGGER.debug("heartbeat interval exceeded, expiring {}", eldest.getKey());
}
return expired;
}
};
}
/**
* Creates a new instance with the specified heartbeat interval. The
* heartbeat should be some multiple of the sampling interval used when
* collecting the metrics.
*
* @param observer downstream observer to forward values to after the rate has
* been computed.
* @param heartbeat how long to remember a previous value before dropping it and
* treating new samples as the first report.
* @param estPollingInterval estimated polling interval in to use for the first call. If set
* to zero no values will be forwarded until the second sample for
* a given counter. The delta for the first interval will be the
* total value for the counter as it is assumed it started at 0 and
* was first created since the last polling interval. If this
* assumption is not true then this setting should be 0 so it waits
* for the next sample to compute an accurate delta, otherwise
* spikes will occur in the output.
* @param unit unit for the heartbeat and estPollingInterval params.
*/
public CounterToRateMetricTransform(
MetricObserver observer, long heartbeat, long estPollingInterval, TimeUnit unit) {
this(observer, heartbeat, estPollingInterval, unit, ClockWithOffset.INSTANCE);
}
/**
* {@inheritDoc}
*/
public String getName() {
return observer.getName();
}
/**
* {@inheritDoc}
*/
public void update(List metrics) {
Preconditions.checkNotNull(metrics, "metrics");
LOGGER.debug("received {} metrics", metrics.size());
final List newMetrics = new ArrayList<>(metrics.size());
for (Metric m : metrics) {
if (isCounter(m)) {
final MonitorConfig rateConfig = toRateConfig(m.getConfig());
final CounterValue prev = cache.get(rateConfig);
if (prev != null) {
final double rate = prev.computeRate(m);
newMetrics.add(new Metric(rateConfig, m.getTimestamp(), rate));
} else {
CounterValue current = new CounterValue(m);
cache.put(rateConfig, current);
if (intervalMillis > 0L) {
final double delta = m.getNumberValue().doubleValue();
final double rate = current.computeRate(intervalMillis, delta);
newMetrics.add(new Metric(rateConfig, m.getTimestamp(), rate));
}
}
} else {
newMetrics.add(m);
}
}
LOGGER.debug("writing {} metrics to downstream observer", newMetrics.size());
observer.update(newMetrics);
}
/**
* Clear all cached state of previous counter values.
*/
public void reset() {
cache.clear();
}
/**
* Convert a MonitorConfig for a counter to one that is explicit about
* being a RATE.
*/
private MonitorConfig toRateConfig(MonitorConfig config) {
return config.withAdditionalTag(RATE_TAG);
}
private boolean isCounter(Metric m) {
final TagList tags = m.getConfig().getTags();
final String value = tags.getValue(DataSourceType.KEY);
return COUNTER_VALUE.equals(value);
}
private static class CounterValue {
private long timestamp;
private double value;
public CounterValue(long timestamp, double value) {
this.timestamp = timestamp;
this.value = value;
}
public CounterValue(Metric m) {
this(m.getTimestamp(), m.getNumberValue().doubleValue());
}
public long getTimestamp() {
return timestamp;
}
public double computeRate(Metric m) {
final long currentTimestamp = m.getTimestamp();
final double currentValue = m.getNumberValue().doubleValue();
final long durationMillis = currentTimestamp - timestamp;
final double delta = currentValue - value;
timestamp = currentTimestamp;
value = currentValue;
return computeRate(durationMillis, delta);
}
public double computeRate(long durationMillis, double delta) {
final double millisPerSecond = 1000.0;
final double duration = durationMillis / millisPerSecond;
return (duration <= 0.0 || delta <= 0.0) ? 0.0 : delta / duration;
}
}
}