/*- * -\-\- * Helios Services * -- * Copyright (C) 2016 Spotify AB * -- * 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.spotify.helios.servicescommon.statistics; 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.MetricRegistry; import com.codahale.metrics.Snapshot; import com.codahale.metrics.Timer; import com.google.common.annotations.VisibleForTesting; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.spotify.helios.common.Version; import eu.toolchain.ffwd.FastForward; import eu.toolchain.ffwd.Metric; import io.dropwizard.lifecycle.Managed; import java.io.IOException; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Sends the metrics in a v3 MetricRegistry to * <a href="https://github.com/spotify/ffwd">FastForward</a>. * * <p>The String "name" of the Metric is sent as the 'what' attribute in the FastForward metric. No * translation of the (possibly long, Java classname-ish) name is done. The type of metric is * included in the 'metric_type' attribute.</p> * * <p>For meters, timers, and histograms, the various component values of the * com.codahale.metrics.Metric (m1/m5/m15, p50, p99 etc) are sent as individual {@link Metric} * instances. The 'stat' attribute is filled out with the corresponding statistic. </p> * * <p>Note that not every statistic from Meters/Timers/Histograms is sent to FastForward in order * to limit the amount of data being sent and needing to be stored downstream. For Metered * instances, only 1m and 5m is sent. For Histograms, just median, p75 and p99 (plus * min/max/mean/stddev). * </p> */ public class FastForwardReporter implements Managed { private static final Logger log = LoggerFactory.getLogger(FastForwardReporter.class); /** * Create a new FastForwardReporter instance. * * @param registry MetricRegistry to report to ffwd * @param address Optional HostAndPort to override the defaults that ffwd client uses * @param metricKey key to use for all metrics * @param intervalSeconds how often to report metrics to ffwd */ public static FastForwardReporter create( MetricRegistry registry, Optional<HostAndPort> address, String metricKey, int intervalSeconds) throws SocketException, UnknownHostException { return create(registry, address, metricKey, intervalSeconds, Collections::emptyMap); } /** * Overload of {@link #create(MetricRegistry, Optional, String, int)} which allows for setting * additional attributes in each reported metric. * * <p>The additional attributes are modeled as a Supplier to allow for attributes that might * change values at runtime. */ public static FastForwardReporter create( MetricRegistry registry, Optional<HostAndPort> address, String metricKey, int intervalSeconds, Supplier<Map<String, String>> additionalAttributes) throws SocketException, UnknownHostException { final FastForward ff; if (address.isPresent()) { ff = FastForward.setup(address.get().getHostText(), address.get().getPort()); } else { ff = FastForward.setup(); } final ThreadFactory threadFactory = new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("fast-forward-reporter-%d") .build(); final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(threadFactory); return new FastForwardReporter(ff, registry, executorService, metricKey, intervalSeconds, TimeUnit.SECONDS, additionalAttributes); } private final FastForward fastForward; private final MetricRegistry metricRegistry; private final ScheduledExecutorService executor; private final String key; private final long interval; private final TimeUnit intervalTimeUnit; private final Supplier<Map<String, String>> additionalAttributesSupplier; private Map<String, String> additionalAttributes; FastForwardReporter(FastForward fastForward, MetricRegistry metricRegistry, ScheduledExecutorService executor, String key, long interval, TimeUnit intervalTimeUnit, Supplier<Map<String, String>> additionalAttributes) { this.fastForward = fastForward; this.metricRegistry = metricRegistry; this.executor = executor; this.key = key; this.interval = interval; this.intervalTimeUnit = intervalTimeUnit; this.additionalAttributesSupplier = additionalAttributes; } @Override public void start() throws Exception { log.info("Scheduling reporting of metrics every {} {}", interval, intervalTimeUnit.name().toLowerCase()); // wrap the runnable in a try-catch as uncaught exceptions will prevent subsequent executions executor.scheduleAtFixedRate(() -> { try { reportOnce(); } catch (Exception e) { log.error("Exception in reporting loop", e); } }, interval, interval, intervalTimeUnit); } @Override public void stop() throws Exception { executor.shutdown(); } @VisibleForTesting void reportOnce() { // resolve the additional attributes once per report additionalAttributes = additionalAttributesSupplier.get(); if (additionalAttributes == null) { additionalAttributes = Collections.emptyMap(); } metricRegistry.getGauges().forEach(this::reportGauge); metricRegistry.getCounters().forEach(this::reportCounter); metricRegistry.getMeters().forEach(this::reportMeter); metricRegistry.getHistograms().forEach(this::reportHistogram); metricRegistry.getTimers().forEach(this::reportTimer); } private void reportGauge(String name, Gauge gauge) { final Metric metric = createMetric(name, "gauge") .value(convert(gauge.getValue())); send(metric); } private void reportCounter(String name, Counter counter) { final Metric metric = createMetric(name, "counter") .value(counter.getCount()); send(metric); } private void reportMeter(String name, Meter meter) { final Metric metric = createMetric(name, "meter") .attribute("unit", "n/s"); reportMetered(metric, meter); } private void reportMetered(Metric metric, Metered metered) { //purposefully don't emit 15m as it is not very useful send(metric.attribute("stat", "1m").value(metered.getOneMinuteRate())); send(metric.attribute("stat", "5m").value(metered.getOneMinuteRate())); } private void reportHistogram(String name, Histogram histogram) { final Metric metric = createMetric(name, "histogram"); reportHistogram(metric, histogram.getSnapshot()); } private void reportHistogram(Metric metric, Snapshot snapshot) { send(metric.attribute("stat", "min").value(snapshot.getMin())); send(metric.attribute("stat", "max").value(snapshot.getMax())); send(metric.attribute("stat", "mean").value(snapshot.getMean())); send(metric.attribute("stat", "stddev").value(snapshot.getStdDev())); send(metric.attribute("stat", "median").value(snapshot.getMedian())); send(metric.attribute("stat", "p75").value(snapshot.get75thPercentile())); send(metric.attribute("stat", "p99").value(snapshot.get99thPercentile())); } private void reportTimer(String name, Timer timer) { final Metric metric = createMetric(name, "timer") .attribute("unit", "ns"); reportHistogram(metric, timer.getSnapshot()); reportMetered(metric, timer); } private Metric createMetric(String metricName, String metricType) { return FastForward.metric(key) .attributes(additionalAttributes) .attribute("helios_version", Version.POM_VERSION) .attribute("metric_type", metricType) .attribute("what", metricName); } private double convert(Object value) { if (value instanceof Number) { return Number.class.cast(value).doubleValue(); } if (value instanceof Boolean) { return (Boolean) value ? 1 : 0; } return 0; } private void send(Metric metric) { try { fastForward.send(metric); } catch (IOException e) { log.error("Error sending metric to FastForward", e); } } }