Add metric/mbean base types + metrics JMX object factory
This commit is contained in:
parent
3e146845b4
commit
a44c18c621
177
src/main/java/com/scylladb/jmx/metrics/APIMBean.java
Normal file
177
src/main/java/com/scylladb/jmx/metrics/APIMBean.java
Normal file
@ -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<ObjectName> all,
|
||||
final Predicate<ObjectName> predicate, Function<ObjectName, Object> generator)
|
||||
throws MalformedObjectNameException {
|
||||
Set<ObjectName> 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<ObjectName> queryNames(MBeanServer server, final Predicate<ObjectName> predicate) {
|
||||
@SuppressWarnings("serial")
|
||||
Set<ObjectName> 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;
|
||||
}
|
||||
}
|
91
src/main/java/com/scylladb/jmx/metrics/MetricsMBean.java
Normal file
91
src/main/java/com/scylladb/jmx/metrics/MetricsMBean.java
Normal file
@ -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> 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> metrics) {
|
||||
super(mbeanName, client);
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
protected Predicate<ObjectName> 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<MetricMBean> s, ObjectName... objectNames) {
|
||||
for (ObjectName name : objectNames) {
|
||||
try {
|
||||
server.unregisterMBean(name);
|
||||
} catch (MBeanRegistrationException | InstanceNotFoundException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, server);
|
||||
} catch (MalformedObjectNameException e) {
|
||||
// TODO : log?
|
||||
}
|
||||
super.postDeregister();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
38
src/main/java/org/apache/cassandra/metrics/Metrics.java
Normal file
38
src/main/java/org/apache/cassandra/metrics/Metrics.java
Normal file
@ -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 {
|
||||
}
|
||||
}
|
792
src/main/java/org/apache/cassandra/metrics/MetricsRegistry.java
Normal file
792
src/main/java/org/apache/cassandra/metrics/MetricsRegistry.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* The 3.0 API comes with poor JMX integration
|
||||
* </p>
|
||||
*/
|
||||
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 <T> MetricMBean gauge(Class<T> type, final String url) {
|
||||
return gauge(getReader(type), url);
|
||||
}
|
||||
|
||||
public <T> MetricMBean gauge(final BiFunction<APIClient, String, T> function, final String url) {
|
||||
return gauge(c -> function.apply(c, url));
|
||||
}
|
||||
|
||||
public <T> MetricMBean gauge(final Function<APIClient, T> 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 <T> MetricMBean gauge(final Supplier<T> 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<MetricMBean> 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<JsonObject> supplier;
|
||||
private long lastUpdate;
|
||||
|
||||
public IntermediatelyUpdated(String url, long interval) {
|
||||
this.supplier = () -> client.getJsonObj(url, null);
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
public IntermediatelyUpdated(Supplier<JsonObject> 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<JsonObject> 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user