package org.stagemonitor.core.metrics.metrics2; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.codahale.metrics.Metered; import com.codahale.metrics.Snapshot; import com.codahale.metrics.Timer; import org.stagemonitor.core.CorePlugin; import org.stagemonitor.core.util.HttpClient; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import static org.stagemonitor.core.metrics.metrics2.MetricName.name; public class InfluxDbReporter extends ScheduledMetrics2Reporter { private static final int MAX_BATCH_SIZE = 5000; private static final Map<MetricName, String> metricNameToInfluxDBFormatCache = new ConcurrentHashMap<MetricName, String>(); private static final MetricName reportingTimeMetricName = name("reporting_time").tag("reporter", "influxdb").build(); private List<String> batchLines = new ArrayList<String>(MAX_BATCH_SIZE); private final String globalTags; private HttpClient httpClient; private final CorePlugin corePlugin; public static Builder forRegistry(Metric2Registry registry, CorePlugin corePlugin) { return new Builder(registry, corePlugin); } private InfluxDbReporter(Builder builder) { super(builder); this.globalTags = getInfluxDbTags(builder.getGlobalTags()); this.httpClient = builder.getHttpClient(); this.corePlugin = builder.getCorePlugin(); } @Override public void reportMetrics(Map<MetricName, Gauge> gauges, Map<MetricName, Counter> counters, Map<MetricName, Histogram> histograms, Map<MetricName, Meter> meters, Map<MetricName, Timer> timers) { final Timer.Context time = registry.timer(reportingTimeMetricName).time(); long timestamp = clock.getTime(); reportGauges(gauges, timestamp); reportCounter(counters, timestamp); reportHistograms(histograms, timestamp); reportMeters(meters, timestamp); reportTimers(timers, timestamp); flush(); time.stop(); } private void reportGauges(Map<MetricName, Gauge> gauges, long timestamp) { for (Map.Entry<MetricName, Gauge> entry : gauges.entrySet()) { final String value = getGaugeValueForInfluxDb(entry.getValue().getValue()); if (value != null) { reportLine(getInfluxDbLineProtocolString(entry.getKey()), value, timestamp); } } } private void reportCounter(Map<MetricName, Counter> counters, long timestamp) { for (Map.Entry<MetricName, Counter> entry : counters.entrySet()) { final Counter counter = entry.getValue(); reportLine(getInfluxDbLineProtocolString(entry.getKey()), "count=" + getIntegerValue(counter.getCount()), timestamp); } } private void reportHistograms(Map<MetricName, Histogram> histograms, long timestamp) { for (Map.Entry<MetricName, Histogram> entry : histograms.entrySet()) { final Histogram hist = entry.getValue(); final Snapshot snapshot = hist.getSnapshot(); reportLine(getInfluxDbLineProtocolString(entry.getKey()), "count=" + getIntegerValue(hist.getCount()) + "," + reportHistogramSnapshot(snapshot), timestamp); } } private void reportMeters(Map<MetricName, Meter> meters, long timestamp) { for (Map.Entry<MetricName, Meter> entry : meters.entrySet()) { final Meter meter = entry.getValue(); reportLine(getInfluxDbLineProtocolString(entry.getKey()), reportMetered(meter), timestamp); } } private void reportTimers(Map<MetricName, Timer> timers, long timestamp) { for (Map.Entry<MetricName, Timer> entry : timers.entrySet()) { final Timer timer = entry.getValue(); final Snapshot snapshot = timer.getSnapshot(); reportLine(getInfluxDbLineProtocolString(entry.getKey()), reportMetered(timer) + "," + reportTimerSnapshot(snapshot), timestamp); } } private String reportTimerSnapshot(Snapshot snapshot) { return "min=" + getDuration(snapshot.getMin()) + "," + "max=" + getDuration(snapshot.getMax()) + "," + "mean=" + getDuration(snapshot.getMean()) + "," + "p50=" + getDuration(snapshot.getMedian()) + "," + "std=" + getDuration(snapshot.getStdDev()) + "," + "p25=" + getDuration(snapshot.getValue(0.25)) + "," + "p75=" + getDuration(snapshot.get75thPercentile()) + "," + "p95=" + getDuration(snapshot.get95thPercentile()) + "," + "p98=" + getDuration(snapshot.get98thPercentile()) + "," + "p99=" + getDuration(snapshot.get99thPercentile()) + "," + "p999=" + getDuration(snapshot.get999thPercentile()); } private String reportHistogramSnapshot(Snapshot snapshot) { return "min=" + snapshot.getMin() + "," + "max=" + snapshot.getMax() + "," + "mean=" + snapshot.getMean() + "," + "p50=" + snapshot.getMedian() + "," + "std=" + snapshot.getStdDev() + "," + "p25=" + snapshot.getValue(0.25) + "," + "p75=" + snapshot.get75thPercentile() + "," + "p95=" + snapshot.get95thPercentile() + "," + "p98=" + snapshot.get98thPercentile() + "," + "p99=" + snapshot.get99thPercentile() + "," + "p999=" + snapshot.get999thPercentile(); } private String reportMetered(Metered metered) { return "count=" + getIntegerValue(metered.getCount()) + "," + "m1_rate=" + getRate(metered.getOneMinuteRate()) + "," + "m5_rate=" + getRate(metered.getFiveMinuteRate()) + "," + "m15_rate=" + getRate(metered.getFifteenMinuteRate()) + "," + "mean_rate=" + getRate(metered.getMeanRate()); } private void reportLine(String nameAndTags, String fields, long timestamp) { if (batchLines.size() >= MAX_BATCH_SIZE) { flush(); } batchLines.add(nameAndTags + globalTags + ' ' + fields + ' ' + timestamp); } private void flush() { httpClient.send("POST", corePlugin.getInfluxDbUrl() + "/write?precision=ms&db=" + corePlugin.getInfluxDbDb(), batchLines); batchLines = new ArrayList<String>(MAX_BATCH_SIZE); } private String getGaugeValueForInfluxDb(Object value) { if (value == null) { return null; } if (value instanceof Number) { final String floatValue = getFloatValue(value); if (floatValue == null) { return null; } return "value=" + floatValue; } else if (value instanceof Boolean) { return "value_boolean=" + value.toString(); } else { return "value_string=" + getStringValue(String.valueOf(value)); } } private String getIntegerValue(Object integer) { return integer.toString() + "i"; } private String getDuration(double duration) { return getFloatValue(convertDuration(duration)); } private String getRate(double rate) { return getFloatValue(convertRate(rate)); } private String getFloatValue(Object number) { String result = number.toString(); if (result.equals("NaN") || result.contains("Infinity")) { return null; } else { // InfluxDB wants the exponent to be in lower case return result.replace('E', 'e'); } } private String getStringValue(String value) { final String s = value; if (s.indexOf('"') == -1) { return new StringBuilder(s.length() + 2).append('"').append(s).append('"').toString(); } else { return new StringBuilder(s.length() + 6).append('"').append(s.replace("\"", "\\\"")).append('"').toString(); } } public static class Builder extends ScheduledMetrics2Reporter.Builder<InfluxDbReporter, Builder> { private HttpClient httpClient = new HttpClient(); private final CorePlugin corePlugin; private Builder(Metric2Registry registry, CorePlugin corePlugin) { super(registry, "stagemonitor-influxdb-reporter"); this.corePlugin = corePlugin; } public HttpClient getHttpClient() { return httpClient; } @Override public InfluxDbReporter build() { return new InfluxDbReporter(this); } public Builder httpClient(HttpClient httpClient) { this.httpClient = httpClient; return this; } public CorePlugin getCorePlugin() { return corePlugin; } } public static String getInfluxDbLineProtocolString(MetricName metricName) { String influxDbString = metricNameToInfluxDBFormatCache.get(metricName); if (influxDbString == null) { final StringBuilder sb = new StringBuilder(metricName.getName().length() + metricName.getTagKeys().size() * 16 + metricName.getTagKeys().size()); sb.append(escapeForInfluxDB(metricName.getName())); appendTags(sb, metricName.getTags()); influxDbString = sb.toString(); metricNameToInfluxDBFormatCache.put(metricName, influxDbString); } return influxDbString; } private static String getInfluxDbTags(Map<String, String> tags) { final StringBuilder sb = new StringBuilder(); appendTags(sb, tags); return sb.toString(); } private static void appendTags(StringBuilder sb, Map<String, String> tags) { for (String key : new TreeSet<String>(tags.keySet())) { sb.append(',').append(escapeForInfluxDB(key)).append('=').append(escapeForInfluxDB(tags.get(key))); } } private static String escapeForInfluxDB(String s) { if (s.indexOf(',') != -1 || s.indexOf(' ') != -1) { return s.replace(" ", "\\ ").replace(",", "\\,"); } return s; } }