diff --git a/src/main/java/com/scylladb/jmx/metrics/APIMBean.java b/src/main/java/com/scylladb/jmx/metrics/APIMBean.java new file mode 100644 index 0000000..23d5d3c --- /dev/null +++ b/src/main/java/com/scylladb/jmx/metrics/APIMBean.java @@ -0,0 +1,177 @@ +package com.scylladb.jmx.metrics; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.management.BadAttributeValueExpException; +import javax.management.BadBinaryOpValueExpException; +import javax.management.BadStringOperationException; +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.InvalidApplicationException; +import javax.management.MBeanRegistration; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.management.QueryExp; + +import com.scylladb.jmx.api.APIClient; + +/** + * Base type for MBeans in scylla-jmx. Wraps auto naming and {@link APIClient} + * holding. + * + * @author calle + * + */ +public class APIMBean implements MBeanRegistration { + protected final APIClient client; + protected final String mbeanName; + + public APIMBean(APIClient client) { + this(null, client); + } + + public APIMBean(String mbeanName, APIClient client) { + this.mbeanName = mbeanName; + this.client = client; + } + + /** + * Helper method to add/remove dynamically created MBeans from a server + * instance. + * + * @param server + * The {@link MBeanServer} to check + * @param all + * All {@link ObjectName}s that should be bound + * @param predicate + * {@link QueryExp} predicate to filter relevant object names. + * @param generator + * {@link Function} to create a new MBean instance for a given + * {@link ObjectName} + * + * @return + * @throws MalformedObjectNameException + */ + public static boolean checkRegistration(MBeanServer server, Set all, + final Predicate predicate, Function generator) + throws MalformedObjectNameException { + Set registered = queryNames(server, predicate); + for (ObjectName name : registered) { + if (!all.contains(name)) { + try { + server.unregisterMBean(name); + } catch (MBeanRegistrationException | InstanceNotFoundException e) { + } + } + } + + int added = 0; + for (ObjectName name : all) { + if (!registered.contains(name)) { + try { + server.registerMBean(generator.apply(name), name); + added++; + } catch (InstanceAlreadyExistsException | MBeanRegistrationException | NotCompliantMBeanException e) { + } + } + } + return added > 0; + } + + /** + * Helper method to query {@link ObjectName}s from an {@link MBeanServer} + * based on {@link Predicate} + * + * @param server + * @param predicate + * @return + */ + public static Set queryNames(MBeanServer server, final Predicate predicate) { + @SuppressWarnings("serial") + Set registered = server.queryNames(null, new QueryExp() { + @Override + public void setMBeanServer(MBeanServer s) { + } + + @Override + public boolean apply(ObjectName name) throws BadStringOperationException, BadBinaryOpValueExpException, + BadAttributeValueExpException, InvalidApplicationException { + return predicate.test(name); + } + }); + return registered; + } + + MBeanServer server; + ObjectName name; + + protected final ObjectName getBoundName() { + return name; + } + + /** + * Figure out an {@link ObjectName} for this object based on either + * contructor parameter, static field, or just package/class name. + * + * @return + * @throws MalformedObjectNameException + */ + protected ObjectName generateName() throws MalformedObjectNameException { + String mbeanName = this.mbeanName; + if (mbeanName == null) { + Field f; + try { + f = getClass().getDeclaredField("MBEAN_NAME"); + f.setAccessible(true); + mbeanName = (String) f.get(null); + } catch (Throwable t) { + } + } + if (mbeanName == null) { + String name = getClass().getName(); + int i = name.lastIndexOf('.'); + mbeanName = name.substring(0, i) + ":type=" + name.substring(i + 1); + } + return new ObjectName(mbeanName); + } + + /** + * Keeps track of bound server and optionally generates an + * {@link ObjectName} for this instance. + */ + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + if (this.server != null) { + throw new IllegalStateException("Can only exist in a single MBeanServer"); + } + this.server = server; + if (name == null) { + name = generateName(); + } + this.name = name; + + return name; + } + + @Override + public void postRegister(Boolean registrationDone) { + } + + @Override + public void preDeregister() throws Exception { + } + + @Override + public void postDeregister() { + assert server != null; + assert name != null; + this.server = null; + this.name = null; + } +} diff --git a/src/main/java/com/scylladb/jmx/metrics/MetricsMBean.java b/src/main/java/com/scylladb/jmx/metrics/MetricsMBean.java new file mode 100644 index 0000000..ae4a87f --- /dev/null +++ b/src/main/java/com/scylladb/jmx/metrics/MetricsMBean.java @@ -0,0 +1,91 @@ +package com.scylladb.jmx.metrics; + +import static java.util.Arrays.asList; + +import java.util.Collection; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import javax.management.InstanceNotFoundException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.apache.cassandra.metrics.Metrics; +import org.apache.cassandra.metrics.MetricsRegistry; + +import com.scylladb.jmx.api.APIClient; + +/** + * Base type for MBeans containing {@link Metrics}. + * + * @author calle + * + */ +public abstract class MetricsMBean extends APIMBean { + private final Collection metrics; + + public MetricsMBean(APIClient client, Metrics... metrics) { + this(null, client, metrics); + } + + public MetricsMBean(String mbeanName, APIClient client, Metrics... metrics) { + this(mbeanName, client, asList(metrics)); + } + + public MetricsMBean(String mbeanName, APIClient client, Collection metrics) { + super(mbeanName, client); + this.metrics = metrics; + } + + protected Predicate getTypePredicate() { + String domain = name.getDomain(); + String type = name.getKeyProperty("type"); + return n -> { + return domain.equals(n.getDomain()) && type.equals(n.getKeyProperty("type")); + }; + } + + private void register(MetricsRegistry registry, MBeanServer server) throws MalformedObjectNameException { + // Check if we're the first/last of our type bound/removed. + boolean empty = queryNames(server, getTypePredicate()).isEmpty(); + for (Metrics m : metrics) { + if (empty) { + m.registerGlobals(registry); + } + m.register(registry); + } + } + + @Override + public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { + // Get name etc. + name = super.preRegister(server, name); + // Register all metrics in server + register(new MetricsRegistry(client, server), server); + return name; + } + + @Override + public void postDeregister() { + // We're officially unbound. Remove all metrics we added. + try { + register(new MetricsRegistry(client, server) { + // Unbind instead of bind. Yes. + @Override + public void register(Supplier s, ObjectName... objectNames) { + for (ObjectName name : objectNames) { + try { + server.unregisterMBean(name); + } catch (MBeanRegistrationException | InstanceNotFoundException e) { + } + } + } + }, server); + } catch (MalformedObjectNameException e) { + // TODO : log? + } + super.postDeregister(); + } +} diff --git a/src/main/java/org/apache/cassandra/metrics/MetricNameFactory.java b/src/main/java/org/apache/cassandra/metrics/MetricNameFactory.java new file mode 100644 index 0000000..3b1de95 --- /dev/null +++ b/src/main/java/org/apache/cassandra/metrics/MetricNameFactory.java @@ -0,0 +1,40 @@ +/* + * 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 javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +/** + * Simplified version of {@link Metrics} naming factory paradigm, simply + * generating {@link ObjectName} and nothing more. + * + * @author calle + * + */ +public interface MetricNameFactory { + /** + * Create a qualified name from given metric name. + * + * @param metricName + * part of qualified name. + * @return new String with given metric name. + * @throws MalformedObjectNameException + */ + ObjectName createMetricName(String metricName) throws MalformedObjectNameException; +} diff --git a/src/main/java/org/apache/cassandra/metrics/Metrics.java b/src/main/java/org/apache/cassandra/metrics/Metrics.java new file mode 100644 index 0000000..80b8b3f --- /dev/null +++ b/src/main/java/org/apache/cassandra/metrics/Metrics.java @@ -0,0 +1,38 @@ +package org.apache.cassandra.metrics; + +import java.util.function.Function; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; + +/** + * Action interface for any type that encapsulates n metrics. + * + * @author calle + * + */ +public interface Metrics { + /** + * Implementors should issue + * {@link MetricsRegistry#register(java.util.function.Supplier, javax.management.ObjectName...)} + * for every {@link Metrics} they generate. This method is called in both + * bind (create) and unbind (remove) phase, so an appropriate use of + * {@link Function} binding is advisable. + * + * @param registry + * @throws MalformedObjectNameException + */ + void register(MetricsRegistry registry) throws MalformedObjectNameException; + + /** + * Same as {{@link #register(MetricsRegistry)}, but for {@link Metric}s that + * are "global" (i.e. static - not bound to an individual bean instance. + * This method is called whenever the first encapsulating MBean is + * added/removed from a {@link MBeanServer}. + * + * @param registry + * @throws MalformedObjectNameException + */ + default void registerGlobals(MetricsRegistry registry) throws MalformedObjectNameException { + } +} diff --git a/src/main/java/org/apache/cassandra/metrics/MetricsRegistry.java b/src/main/java/org/apache/cassandra/metrics/MetricsRegistry.java new file mode 100644 index 0000000..f23ef66 --- /dev/null +++ b/src/main/java/org/apache/cassandra/metrics/MetricsRegistry.java @@ -0,0 +1,792 @@ +/* + * 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.NANOSECONDS; + private static final double DURATION_FACTOR = 1.0 / API_DURATION_UNIT.convert(1, DURATION_UNIT); + + private static double toDuration(double nanos) { + return nanos * 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() { + return toDuration(histogram.getMin()); + } + + @Override + public double getMax() { + return toDuration(histogram.getMax()); + } + + @Override + public double getMean() { + return toDuration(histogram.getMean()); + } + + @Override + public double getStdDev() { + return toDuration(histogram.getStdDev()); + } + + @Override + public double get50thPercentile() { + return toDuration(histogram.getValue(.5)); + } + + @Override + public double get75thPercentile() { + return toDuration(histogram.getValue(.75)); + } + + @Override + public double get95thPercentile() { + return toDuration(histogram.getValue(.95)); + } + + @Override + public double get98thPercentile() { + return toDuration(histogram.getValue(.98)); + } + + @Override + public double get99thPercentile() { + return toDuration(histogram.getValue(.99)); + } + + @Override + public double get999thPercentile() { + return toDuration(histogram.getValue(.999)); + } + + @Override + public long[] values() { + return histogram.getValues(); + } + + @Override + public String getDurationUnit() { + 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(); + } +}