/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cassandra.metrics;
import static com.scylladb.jmx.api.APIClient.getReader;
import static java.lang.Math.floor;
import static java.util.logging.Level.SEVERE;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Logger;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import com.scylladb.jmx.api.APIClient;
/**
* Makes integrating 3.0 metrics API with 2.0.
*
* The 3.0 API comes with poor JMX integration
*
*/
public class MetricsRegistry {
private static final long CACHE_DURATION = 1000;
private static final long UPDATE_INTERVAL = 50;
private static final Logger logger = Logger.getLogger(MetricsRegistry.class.getName());
private final APIClient client;
private final MBeanServer mBeanServer;
public MetricsRegistry(APIClient client, MBeanServer mBeanServer) {
this.client = client;
this.mBeanServer = mBeanServer;
}
public MetricsRegistry(MetricsRegistry other) {
this(other.client, other.mBeanServer);
}
public MetricMBean gauge(String url) {
return gauge(Long.class, url);
}
public MetricMBean gauge(Class type, final String url) {
return gauge(getReader(type), url);
}
public MetricMBean gauge(final BiFunction function, final String url) {
return gauge(c -> function.apply(c, url));
}
public MetricMBean gauge(final Function function) {
return gauge(() -> function.apply(client));
}
private class JmxGauge implements JmxGaugeMBean {
private final Supplier> function;
public JmxGauge(Supplier> function) {
this.function = function;
}
@Override
public Object getValue() {
return function.get();
}
}
public MetricMBean gauge(final Supplier function) {
return new JmxGauge(function);
}
/**
* Default approach to register is to actually register/add to
* {@link MBeanServer} For unbind phase, override here.
*
* @param bean
* @param objectNames
*/
public void register(Supplier f, ObjectName... objectNames) {
MetricMBean bean = f.get();
for (ObjectName name : objectNames) {
try {
mBeanServer.registerMBean(bean, name);
} catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) {
logger.log(SEVERE, "Could not register mbean", e);
}
}
}
private class JmxCounter implements JmxCounterMBean {
private final String url;
public JmxCounter(String url) {
super();
this.url = url;
}
@Override
public long getCount() {
return client.getLongValue(url);
}
}
public MetricMBean counter(final String url) {
return new JmxCounter(url);
}
private abstract class IntermediatelyUpdated {
private final long interval;
private final Supplier supplier;
private long lastUpdate;
public IntermediatelyUpdated(String url, long interval) {
this.supplier = () -> client.getJsonObj(url, null);
this.interval = interval;
}
public IntermediatelyUpdated(Supplier supplier, long interval) {
this.supplier = supplier;
this.interval = interval;
}
public abstract void update(JsonObject obj);
public final void update() {
long now = System.currentTimeMillis();
if (now - lastUpdate < interval) {
return;
}
try {
JsonObject obj = supplier.get();
update(obj);
} finally {
lastUpdate = now;
}
}
}
private static class Meter {
public final long count;
public final double oneMinuteRate;
public final double fiveMinuteRate;
public final double fifteenMinuteRate;
public final double meanRate;
public Meter(long count, double oneMinuteRate, double fiveMinuteRate, double fifteenMinuteRate,
double meanRate) {
this.count = count;
this.oneMinuteRate = oneMinuteRate;
this.fiveMinuteRate = fiveMinuteRate;
this.fifteenMinuteRate = fifteenMinuteRate;
this.meanRate = meanRate;
}
public Meter() {
this(0, 0, 0, 0, 0);
}
public Meter(JsonObject obj) {
JsonArray rates = obj.getJsonArray("rates");
oneMinuteRate = rates.getJsonNumber(0).doubleValue();
fiveMinuteRate = rates.getJsonNumber(1).doubleValue();
fifteenMinuteRate = rates.getJsonNumber(2).doubleValue();
meanRate = obj.getJsonNumber("mean_rate").doubleValue();
count = obj.getJsonNumber("count").longValue();
}
}
private static final TimeUnit RATE_UNIT = TimeUnit.SECONDS;
private static final TimeUnit DURATION_UNIT = TimeUnit.MICROSECONDS;
private static final TimeUnit API_DURATION_UNIT = TimeUnit.MICROSECONDS;
private static final double DURATION_FACTOR = 1.0 / API_DURATION_UNIT.convert(1, DURATION_UNIT);
private static double toDuration(double micro) {
return micro * DURATION_FACTOR;
}
private static String unitString(TimeUnit u) {
String s = u.toString().toLowerCase(Locale.US);
return s.substring(0, s.length() - 1);
}
private class JmxMeter extends IntermediatelyUpdated implements JmxMeterMBean {
private Meter meter = new Meter();
public JmxMeter(String url, long interval) {
super(url, interval);
}
public JmxMeter(Supplier supplier, long interval) {
super(supplier, interval);
}
@Override
public void update(JsonObject obj) {
meter = new Meter(obj);
}
@Override
public long getCount() {
update();
return meter.count;
}
@Override
public double getMeanRate() {
update();
return meter.meanRate;
}
@Override
public double getOneMinuteRate() {
update();
return meter.oneMinuteRate;
}
@Override
public double getFiveMinuteRate() {
update();
return meter.fiveMinuteRate;
}
@Override
public double getFifteenMinuteRate() {
update();
return meter.fifteenMinuteRate;
}
@Override
public String getRateUnit() {
return "event/" + unitString(RATE_UNIT);
}
}
public MetricMBean meter(String url) {
return new JmxMeter(url, CACHE_DURATION);
}
private static long[] asLongArray(JsonArray a) {
return a.getValuesAs(JsonNumber.class).stream().mapToLong(n -> n.longValue()).toArray();
}
private static interface Samples {
default double getValue(double quantile) {
return 0;
}
default long[] getValues() {
return new long[0];
}
}
private static class BufferSamples implements Samples {
private final long[] samples;
public BufferSamples(long[] samples) {
this.samples = samples;
Arrays.sort(this.samples);
}
@Override
public long[] getValues() {
return samples;
}
@Override
public double getValue(double quantile) {
if (quantile < 0.0 || quantile > 1.0) {
throw new IllegalArgumentException(quantile + " is not in [0..1]");
}
if (samples.length == 0) {
return 0.0;
}
final double pos = quantile * (samples.length + 1);
if (pos < 1) {
return samples[0];
}
if (pos >= samples.length) {
return samples[samples.length - 1];
}
final double lower = samples[(int) pos - 1];
final double upper = samples[(int) pos];
return lower + (pos - floor(pos)) * (upper - lower);
}
}
private static class Histogram {
private final long count;
private final long min;
private final long max;
private final double mean;
private final double stdDev;
private final Samples samples;
public Histogram(long count, long min, long max, double mean, double stdDev, Samples samples) {
this.count = count;
this.min = min;
this.max = max;
this.mean = mean;
this.stdDev = stdDev;
this.samples = samples;
}
public Histogram() {
this(0, 0, 0, 0, 0, new Samples() {
});
}
public Histogram(JsonObject obj) {
this(obj.getJsonNumber("count").longValue(), obj.getJsonNumber("min").longValue(),
obj.getJsonNumber("max").longValue(), obj.getJsonNumber("mean").doubleValue(),
obj.getJsonNumber("variance").doubleValue(), new BufferSamples(getValues(obj)));
}
public Histogram(EstimatedHistogram h) {
this(h.count(), h.min(), h.max(), h.mean(), 0, h);
}
private static long[] getValues(JsonObject obj) {
JsonArray arr = obj.getJsonArray("sample");
if (arr != null) {
return asLongArray(arr);
}
return new long[0];
}
public long[] getValues() {
return samples.getValues();
}
// Origin (and previous iterations of scylla-jxm)
// uses biased/ExponentiallyDecaying measurements
// for the history & quantile resolution.
// However, for use that is just gobbletigook, since
// we, at occasions of being asked, and when certain time
// has passed, ask the actual scylla server for a
// "values" buffer. A buffer with no information whatsoever
// on how said values correlate to actual sampling
// time.
// So, applying time weights at this level is just
// wrong. We can just as well treat this as a uniform
// distribution.
// Obvious improvement: Send time/value tuples instead.
public double getValue(double quantile) {
return samples.getValue(quantile);
}
public long getCount() {
return count;
}
public long getMin() {
return min;
}
public long getMax() {
return max;
}
public double getMean() {
return mean;
}
public double getStdDev() {
return stdDev;
}
}
private static class EstimatedHistogram implements Samples {
/**
* The series of values to which the counts in `buckets` correspond: 1,
* 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 17, 20, etc. Thus, a `buckets` of
* [0, 0, 1, 10] would mean we had seen one value of 3 and 10 values of
* 4.
*
* The series starts at 1 and grows by 1.2 each time (rounding and
* removing duplicates). It goes from 1 to around 36M by default
* (creating 90+1 buckets), which will give us timing resolution from
* microseconds to 36 seconds, with less precision as the numbers get
* larger.
*
* Each bucket represents values from (previous bucket offset, current
* offset].
*/
private final long[] bucketOffsets;
// buckets is one element longer than bucketOffsets -- the last element
// is
// values greater than the last offset
private long[] buckets;
public EstimatedHistogram(JsonObject obj) {
this(asLongArray(obj.getJsonArray("bucket_offsets")), asLongArray(obj.getJsonArray("buckets")));
}
public EstimatedHistogram(long[] offsets, long[] bucketData) {
assert bucketData.length == offsets.length + 1;
bucketOffsets = offsets;
buckets = bucketData;
}
/**
* @return the smallest value that could have been added to this
* histogram
*/
public long min() {
for (int i = 0; i < buckets.length; i++) {
if (buckets[i] > 0) {
return i == 0 ? 0 : 1 + bucketOffsets[i - 1];
}
}
return 0;
}
/**
* @return the largest value that could have been added to this
* histogram. If the histogram overflowed, returns
* Long.MAX_VALUE.
*/
public long max() {
int lastBucket = buckets.length - 1;
if (buckets[lastBucket] > 0) {
return Long.MAX_VALUE;
}
for (int i = lastBucket - 1; i >= 0; i--) {
if (buckets[i] > 0) {
return bucketOffsets[i];
}
}
return 0;
}
@Override
public long[] getValues() {
return buckets;
}
/**
* @param percentile
* @return estimated value at given percentile
*/
@Override
public double getValue(double percentile) {
assert percentile >= 0 && percentile <= 1.0;
int lastBucket = buckets.length - 1;
if (buckets[lastBucket] > 0) {
throw new IllegalStateException("Unable to compute when histogram overflowed");
}
long pcount = (long) Math.floor(count() * percentile);
if (pcount == 0) {
return 0;
}
long elements = 0;
for (int i = 0; i < lastBucket; i++) {
elements += buckets[i];
if (elements >= pcount) {
return bucketOffsets[i];
}
}
return 0;
}
/**
* @return the mean histogram value (average of bucket offsets, weighted
* by count)
* @throws IllegalStateException
* if any values were greater than the largest bucket
* threshold
*/
public long mean() {
int lastBucket = buckets.length - 1;
if (buckets[lastBucket] > 0) {
throw new IllegalStateException("Unable to compute ceiling for max when histogram overflowed");
}
long elements = 0;
long sum = 0;
for (int i = 0; i < lastBucket; i++) {
long bCount = buckets[i];
elements += bCount;
sum += bCount * bucketOffsets[i];
}
return (long) Math.ceil((double) sum / elements);
}
/**
* @return the total number of non-zero values
*/
public long count() {
return Arrays.stream(buckets).sum();
}
/**
* @return true if this histogram has overflowed -- that is, a value
* larger than our largest bucket could bound was added
*/
@SuppressWarnings("unused")
public boolean isOverflowed() {
return buckets[buckets.length - 1] > 0;
}
}
private class JmxHistogram extends IntermediatelyUpdated implements JmxHistogramMBean {
private Histogram histogram = new Histogram();
public JmxHistogram(String url, long interval) {
super(url, interval);
}
@Override
public void update(JsonObject obj) {
if (obj.containsKey("hist")) {
obj = obj.getJsonObject("hist");
}
if (obj.containsKey("buckets")) {
histogram = new Histogram(new EstimatedHistogram(obj));
} else {
histogram = new Histogram(obj);
}
}
@Override
public long getCount() {
update();
return histogram.getCount();
}
@Override
public long getMin() {
update();
return histogram.getMin();
}
@Override
public long getMax() {
update();
return histogram.getMax();
}
@Override
public double getMean() {
update();
return histogram.getMean();
}
@Override
public double getStdDev() {
update();
return histogram.getStdDev();
}
@Override
public double get50thPercentile() {
update();
return histogram.getValue(.5);
}
@Override
public double get75thPercentile() {
update();
return histogram.getValue(.75);
}
@Override
public double get95thPercentile() {
update();
return histogram.getValue(.95);
}
@Override
public double get98thPercentile() {
update();
return histogram.getValue(.98);
}
@Override
public double get99thPercentile() {
update();
return histogram.getValue(.99);
}
@Override
public double get999thPercentile() {
update();
return histogram.getValue(.999);
}
@Override
public long[] values() {
update();
return histogram.getValues();
}
}
public MetricMBean histogram(String url, boolean considerZeroes) {
return new JmxHistogram(url, UPDATE_INTERVAL);
}
private class JmxTimer extends JmxMeter implements JmxTimerMBean {
private Histogram histogram = new Histogram();
public JmxTimer(String url, long interval) {
super(url, interval);
}
@Override
public void update(JsonObject obj) {
// TODO: this is not atomic.
super.update(obj.getJsonObject("meter"));
histogram = new Histogram(obj.getJsonObject("hist"));
}
@Override
public double getMin() {
update();
return toDuration(histogram.getMin());
}
@Override
public double getMax() {
update();
return toDuration(histogram.getMax());
}
@Override
public double getMean() {
update();
return toDuration(histogram.getMean());
}
@Override
public double getStdDev() {
update();
return toDuration(histogram.getStdDev());
}
@Override
public double get50thPercentile() {
update();
return toDuration(histogram.getValue(.5));
}
@Override
public double get75thPercentile() {
update();
return toDuration(histogram.getValue(.75));
}
@Override
public double get95thPercentile() {
update();
return toDuration(histogram.getValue(.95));
}
@Override
public double get98thPercentile() {
update();
return toDuration(histogram.getValue(.98));
}
@Override
public double get99thPercentile() {
update();
return toDuration(histogram.getValue(.99));
}
@Override
public double get999thPercentile() {
update();
return toDuration(histogram.getValue(.999));
}
@Override
public long[] values() {
update();
return histogram.getValues();
}
@Override
public String getDurationUnit() {
update();
return DURATION_UNIT.toString().toLowerCase(Locale.US);
}
}
public MetricMBean timer(String url) {
return new JmxTimer(url, UPDATE_INTERVAL);
}
public interface MetricMBean {
}
public static interface JmxGaugeMBean extends MetricMBean {
Object getValue();
}
public interface JmxHistogramMBean extends MetricMBean {
long getCount();
long getMin();
long getMax();
double getMean();
double getStdDev();
double get50thPercentile();
double get75thPercentile();
double get95thPercentile();
double get98thPercentile();
double get99thPercentile();
double get999thPercentile();
long[] values();
}
public interface JmxCounterMBean extends MetricMBean {
long getCount();
}
public interface JmxMeterMBean extends MetricMBean {
long getCount();
double getMeanRate();
double getOneMinuteRate();
double getFiveMinuteRate();
double getFifteenMinuteRate();
String getRateUnit();
}
public interface JmxTimerMBean extends JmxMeterMBean {
double getMin();
double getMax();
double getMean();
double getStdDev();
double get50thPercentile();
double get75thPercentile();
double get95thPercentile();
double get98thPercentile();
double get99thPercentile();
double get999thPercentile();
long[] values();
String getDurationUnit();
}
}