/*
* 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.addthis.hydra.util;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.TimeUnit;
import com.addthis.bundle.core.Bundle;
import com.addthis.bundle.util.AutoField;
import com.addthis.bundle.value.ValueObject;
import com.addthis.codec.annotations.Time;
import com.addthis.hydra.task.output.TaskDataOutput;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.Histogram;
import com.yammer.metrics.core.Metered;
import com.yammer.metrics.core.Metric;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.MetricProcessor;
import com.yammer.metrics.core.MetricsRegistry;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.VirtualMachineMetrics;
import com.yammer.metrics.reporting.AbstractPollingReporter;
import com.yammer.metrics.stats.Snapshot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.addthis.bundle.value.ValueFactory.create;
public class BundleReporter extends AbstractPollingReporter implements MetricProcessor<String>, AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(BundleReporter.class);
private final TaskDataOutput output;
private final AutoField name;
private final AutoField value;
private final AutoField group;
private final AutoField units;
private final long period;
private final VirtualMachineMetrics vm;
@JsonCreator
public BundleReporter(@JsonProperty("output") TaskDataOutput output,
@JsonProperty("name") AutoField name,
@JsonProperty("value") AutoField value,
@JsonProperty("group") AutoField group,
@JsonProperty("units") AutoField units,
@Time(TimeUnit.NANOSECONDS)
@JsonProperty("period") long period) {
this(output, name, value, group, units, period, TimeUnit.NANOSECONDS, Metrics.defaultRegistry());
}
public BundleReporter(TaskDataOutput output,
AutoField name,
AutoField value,
AutoField group,
AutoField units,
long period,
TimeUnit periodUnit,
MetricsRegistry metricsRegistry) {
super(metricsRegistry, "bundle-reporter");
this.output = output;
this.name = name;
this.value = value;
this.group = group;
this.units = units;
this.vm = VirtualMachineMetrics.getInstance();
this.period = periodUnit.toNanos(period);
}
public void start() {
log.info("starting bundle metric reporter");
this.output.init();
super.start(period, TimeUnit.NANOSECONDS);
}
@Override public void close() throws InterruptedException {
// would be nice if this shutdown API gave us more information, but it is just metrics so optimisim is fine
super.shutdown(30, TimeUnit.SECONDS);
output.sendComplete();
log.info("bundle metric reporter closed");
}
@Override
public void run() {
printVmMetrics();
printRegularMetrics();
}
private void printRegularMetrics() {
for (Map.Entry<String, SortedMap<MetricName, Metric>> entry :
getMetricsRegistry().groupedMetrics().entrySet()) {
for (Map.Entry<MetricName, Metric> subEntry : entry.getValue().entrySet()) {
final Metric metric = subEntry.getValue();
if (metric != null) {
try {
metric.processWith(this, subEntry.getKey(), null);
} catch (Exception suppressed) {
log.error("Error printing regular metrics:", suppressed);
}
}
}
}
}
private void sendMetricData(String metricName,
ValueObject metricValue,
String metricGroup,
String metricUnits) {
Bundle message = output.createBundle();
try {
name.setValue(message, create(metricName));
value.setValue(message, metricValue);
group.setValue(message, create(metricGroup));
units.setValue(message, create(metricUnits));
output.send(message);
} catch (Throwable t) {
log.error("Error while trying to report metric with name {}; constructed bundle was {}",
metricName, message, t);
}
}
@Override
public void processGauge(MetricName name, Gauge<?> gauge, String x) throws IOException {
final Object value = gauge.value();
final Class<?> klass = value.getClass();
if (klass == Integer.class || klass == Long.class) {
Number asNumber = (Number) gauge.value();
sendMetricData(sanitizeName(name), create(asNumber.longValue()), "gauge", "");
} else if (klass == Float.class || klass == Double.class) {
Number asNumber = (Number) gauge.value();
sendMetricData(sanitizeName(name), create(asNumber.doubleValue()), "gauge", "");
} else {
sendMetricData(sanitizeName(name), create(gauge.value().toString()), "gauge", "");
}
}
@Override
public void processCounter(MetricName name, Counter counter, String x) throws IOException {
sendMetricData(sanitizeName(name), create(counter.count()), "counter", "");
}
@Override
public void processMeter(MetricName name, Metered meter, String x) throws IOException {
final String sanitizedName = sanitizeName(name);
final String rateUnits = meter.rateUnit().name();
final String rateUnit = rateUnits.substring(0, rateUnits.length() - 1).toLowerCase(Locale.US);
final String unit = meter.eventType() + '/' + rateUnit;
printLongField(sanitizedName + ".count", meter.count(), "metered", meter.eventType());
printDoubleField(sanitizedName + ".meanRate", meter.meanRate(), "metered", unit);
printDoubleField(sanitizedName + ".1MinuteRate", meter.oneMinuteRate(), "metered", unit);
printDoubleField(sanitizedName + ".5MinuteRate", meter.fiveMinuteRate(), "metered", unit);
printDoubleField(sanitizedName + ".15MinuteRate", meter.fifteenMinuteRate(), "metered", unit);
}
@Override
public void processHistogram(MetricName name, Histogram histogram, String x) throws IOException {
final String sanitizedName = sanitizeName(name);
final Snapshot snapshot = histogram.getSnapshot();
printDoubleField(sanitizedName + ".min", histogram.min(), "histo");
printDoubleField(sanitizedName + ".max", histogram.max(), "histo");
printDoubleField(sanitizedName + ".mean", histogram.mean(), "histo");
printDoubleField(sanitizedName + ".stddev", histogram.stdDev(), "histo");
printDoubleField(sanitizedName + ".median", snapshot.getMedian(), "histo");
printDoubleField(sanitizedName + ".75percentile", snapshot.get75thPercentile(), "histo");
printDoubleField(sanitizedName + ".95percentile", snapshot.get95thPercentile(), "histo");
printDoubleField(sanitizedName + ".98percentile", snapshot.get98thPercentile(), "histo");
printDoubleField(sanitizedName + ".99percentile", snapshot.get99thPercentile(), "histo");
printDoubleField(sanitizedName + ".999percentile", snapshot.get999thPercentile(), "histo");
}
@Override
public void processTimer(MetricName name, Timer timer, String x) throws IOException {
processMeter(name, timer, x);
final String sanitizedName = sanitizeName(name);
final Snapshot snapshot = timer.getSnapshot();
final String durationUnit = timer.durationUnit().name();
printDoubleField(sanitizedName + ".min", timer.min(), "timer", durationUnit);
printDoubleField(sanitizedName + ".max", timer.max(), "timer", durationUnit);
printDoubleField(sanitizedName + ".mean", timer.mean(), "timer", durationUnit);
printDoubleField(sanitizedName + ".stddev", timer.stdDev(), "timer", durationUnit);
printDoubleField(sanitizedName + ".median", snapshot.getMedian(), "timer", durationUnit);
printDoubleField(sanitizedName + ".75percentile", snapshot.get75thPercentile(), "timer", durationUnit);
printDoubleField(sanitizedName + ".95percentile", snapshot.get95thPercentile(), "timer", durationUnit);
printDoubleField(sanitizedName + ".98percentile", snapshot.get98thPercentile(), "timer", durationUnit);
printDoubleField(sanitizedName + ".99percentile", snapshot.get99thPercentile(), "timer", durationUnit);
printDoubleField(sanitizedName + ".999percentile", snapshot.get999thPercentile(), "timer", durationUnit);
}
private static final double MIN_VAL = 1E-300;
private void printDoubleField(String name, double value, String groupName, String units) {
if (Math.abs(value) < MIN_VAL) value = 0.0;
sendMetricData(name, create(value), groupName, units);
}
private void printDoubleField(String name, double value, String groupName) {
printDoubleField(name, value, groupName, "");
}
private void printLongField(String name, long value, String groupName) {
printLongField(name, value, groupName, "");
}
private void printLongField(String name, long value, String groupName, String units) {
// TODO: ganglia does not support int64, what should we do here?
sendMetricData(name, create(value), groupName, units);
}
private void printVmMetrics() {
printDoubleField("jvm.memory.heap_usage", vm.heapUsage(), "jvm");
printDoubleField("jvm.memory.non_heap_usage", vm.nonHeapUsage(), "jvm");
for (Map.Entry<String, Double> pool : vm.memoryPoolUsage().entrySet()) {
printDoubleField("jvm.memory.memory_pool_usages." + pool.getKey(),
pool.getValue(),
"jvm");
}
printDoubleField("jvm.daemon_thread_count", vm.daemonThreadCount(), "jvm");
printDoubleField("jvm.thread_count", vm.threadCount(), "jvm");
printDoubleField("jvm.uptime", vm.uptime(), "jvm");
printDoubleField("jvm.fd_usage", vm.fileDescriptorUsage(), "jvm");
for (Map.Entry<Thread.State, Double> entry : vm.threadStatePercentages().entrySet()) {
printDoubleField("jvm.thread-states." + entry.getKey().toString().toLowerCase(),
entry.getValue(),
"jvm");
}
for (Map.Entry<String, VirtualMachineMetrics.GarbageCollectorStats> entry : vm.garbageCollectors().entrySet()) {
printLongField("jvm.gc." + entry.getKey() + ".time",
entry.getValue().getTime(TimeUnit.MILLISECONDS),
"jvm");
printLongField("jvm.gc." + entry.getKey() + ".runs", entry.getValue().getRuns(), "jvm");
}
}
protected String sanitizeName(MetricName name) {
if (name == null) {
return "";
}
final String qualifiedTypeName = name.getGroup() + "." + name.getType() + "." + name.getName();
final String metricName = name.hasScope() ? qualifiedTypeName + '.' + name.getScope() : qualifiedTypeName;
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < metricName.length(); i++) {
final char p = metricName.charAt(i);
if (!(p >= 'A' && p <= 'Z')
&& !(p >= 'a' && p <= 'z')
&& !(p >= '0' && p <= '9')
&& (p != '_')
&& (p != '-')
&& (p != '.')
&& (p != '\0')) {
sb.append('_');
} else {
sb.append(p);
}
}
return sb.toString();
}
}