001/*
002 * Simplified BSD License
003 *
004 *  Copyright (c) 2014, Vistar Media
005 *  All rights reserved.
006 *
007 *  Redistribution and use in source and binary forms, with or without
008 *  modification, are permitted provided that the following conditions are met:
009 *
010 *      * Redistributions of source code must retain the above copyright notice,
011 *        this list of conditions and the following disclaimer.
012 *      * Redistributions in binary form must reproduce the above copyright notice,
013 *        this list of conditions and the following disclaimer in the documentation
014 *        and/or other materials provided with the distribution.
015 *      * Neither the name of Vistar Media nor the names of its contributors
016 *        may be used to endorse or promote products derived from this software
017 *        without specific prior written permission.
018 *
019 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
020 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
021 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
023 *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
024 *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
025 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
026 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
027 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
028 *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 *
030 * Contributors:
031 *     coursera https://github.com/coursera/metrics-datadog/blob/master/metrics-datadog/src/main/java/org/coursera/metrics/datadog/DatadogReporter.java
032 *     bdelbosc
033 */
034
035package org.nuxeo.runtime.metrics.reporter.patch;
036
037import java.io.IOException;
038import java.util.ArrayList;
039import java.util.EnumSet;
040import java.util.List;
041import java.util.Map;
042import java.util.SortedMap;
043import java.util.concurrent.TimeUnit;
044
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047import org.coursera.metrics.datadog.AwsHelper;
048import org.coursera.metrics.datadog.DefaultMetricNameFormatter;
049import org.coursera.metrics.datadog.MetricNameFormatter;
050import org.coursera.metrics.datadog.model.DatadogGauge;
051import org.coursera.metrics.datadog.transport.Transport;
052
053import io.dropwizard.metrics5.Clock;
054import io.dropwizard.metrics5.Counter;
055import io.dropwizard.metrics5.Gauge;
056import io.dropwizard.metrics5.Histogram;
057import io.dropwizard.metrics5.Meter;
058import io.dropwizard.metrics5.Metered;
059import io.dropwizard.metrics5.MetricFilter;
060import io.dropwizard.metrics5.MetricName;
061import io.dropwizard.metrics5.MetricRegistry;
062import io.dropwizard.metrics5.ScheduledReporter;
063import io.dropwizard.metrics5.Snapshot;
064import io.dropwizard.metrics5.Timer;
065
066/**
067 * A copy of Coursera DatadogReporter with minor adaptation to handle metric with tags.
068 *
069 * @since 11.1
070 */
071public class NuxeoDatadogReporter extends ScheduledReporter {
072    protected static final Log log = LogFactory.getLog(NuxeoDatadogReporter.class);
073
074    private static final Expansion[] STATS_EXPANSIONS = { Expansion.MAX, Expansion.MEAN, Expansion.MIN,
075            Expansion.STD_DEV, Expansion.MEDIAN, Expansion.P75, Expansion.P95, Expansion.P98, Expansion.P99,
076            Expansion.P999 };
077
078    private static final Expansion[] RATE_EXPANSIONS = { Expansion.RATE_1_MINUTE, Expansion.RATE_5_MINUTE,
079            Expansion.RATE_15_MINUTE, Expansion.RATE_MEAN };
080
081    private final Transport transport;
082
083    private final Clock clock;
084
085    private final String host;
086
087    private final EnumSet<Expansion> expansions;
088
089    private final MetricNameFormatter metricNameFormatter;
090
091    private final List<String> tags;
092
093    private final String prefix;
094
095    private Transport.Request request;
096
097    private NuxeoDatadogReporter(MetricRegistry metricRegistry, Transport transport, MetricFilter filter, Clock clock,
098                                 String host, EnumSet<Expansion> expansions, TimeUnit rateUnit, TimeUnit durationUnit,
099                                 MetricNameFormatter metricNameFormatter, List<String> tags, String prefix) {
100        super(metricRegistry, "datadog-reporter", filter, rateUnit, durationUnit);
101        this.clock = clock;
102        this.host = host;
103        this.expansions = expansions;
104        this.metricNameFormatter = metricNameFormatter;
105        this.tags = (tags == null) ? new ArrayList<>() : tags;
106        this.transport = transport;
107        this.prefix = prefix;
108    }
109
110    @Override
111    public void report(SortedMap<MetricName, Gauge> gauges, SortedMap<MetricName, Counter> counters,
112            SortedMap<MetricName, Histogram> histograms, SortedMap<MetricName, Meter> meters,
113            SortedMap<MetricName, Timer> timers) {
114        final long timestamp = clock.getTime() / 1000;
115
116        try {
117            request = transport.prepare();
118
119            for (Map.Entry<MetricName, Gauge> entry : gauges.entrySet()) {
120                reportGauge(prefix(entry.getKey().getKey()), entry.getValue(), timestamp,
121                        getTags(entry.getKey().getTags()));
122            }
123
124            for (Map.Entry<MetricName, Counter> entry : counters.entrySet()) {
125                reportCounter(prefix(entry.getKey().getKey()), entry.getValue(), timestamp,
126                        getTags(entry.getKey().getTags()));
127            }
128
129            for (Map.Entry<MetricName, Histogram> entry : histograms.entrySet()) {
130                reportHistogram(prefix(entry.getKey().getKey()), entry.getValue(), timestamp,
131                        getTags(entry.getKey().getTags()));
132            }
133
134            for (Map.Entry<MetricName, Meter> entry : meters.entrySet()) {
135                reportMetered(prefix(entry.getKey().getKey()), entry.getValue(), timestamp,
136                        getTags(entry.getKey().getTags()));
137            }
138
139            for (Map.Entry<MetricName, Timer> entry : timers.entrySet()) {
140                reportTimer(prefix(entry.getKey().getKey()), entry.getValue(), timestamp,
141                        getTags(entry.getKey().getTags()));
142            }
143            request.send();
144        } catch (Throwable e) {
145            log.error("Error reporting metrics to Datadog", e);
146        }
147    }
148
149    protected List<String> getTags(Map<String, String> metricTags) {
150        List<String> ret = new ArrayList<>(tags);
151        metricTags.forEach((k, v) -> ret.add(k + ":" + v));
152        return ret;
153    }
154
155    private void reportTimer(String name, Timer timer, long timestamp, List<String> tags) throws IOException {
156        final Snapshot snapshot = timer.getSnapshot();
157
158        double[] values = { snapshot.getMax(), snapshot.getMean(), snapshot.getMin(), snapshot.getStdDev(),
159                snapshot.getMedian(), snapshot.get75thPercentile(), snapshot.get95thPercentile(),
160                snapshot.get98thPercentile(), snapshot.get99thPercentile(), snapshot.get999thPercentile() };
161
162        for (int i = 0; i < STATS_EXPANSIONS.length; i++) {
163            if (expansions.contains(STATS_EXPANSIONS[i])) {
164                request.addGauge(new DatadogGauge(appendExpansionSuffix(name, STATS_EXPANSIONS[i]),
165                        toNumber(convertDuration(values[i])), timestamp, host, tags));
166            }
167        }
168
169        reportMetered(name, timer, timestamp, tags);
170    }
171
172    private void reportMetered(String name, Metered meter, long timestamp, List<String> tags) throws IOException {
173        if (expansions.contains(Expansion.COUNT)) {
174            request.addGauge(new DatadogGauge(appendExpansionSuffix(name, Expansion.COUNT), meter.getCount(), timestamp,
175                    host, tags));
176        }
177
178        double[] values = { meter.getOneMinuteRate(), meter.getFiveMinuteRate(), meter.getFifteenMinuteRate(),
179                meter.getMeanRate() };
180
181        for (int i = 0; i < RATE_EXPANSIONS.length; i++) {
182            if (expansions.contains(RATE_EXPANSIONS[i])) {
183                request.addGauge(new DatadogGauge(appendExpansionSuffix(name, RATE_EXPANSIONS[i]),
184                        toNumber(convertRate(values[i])), timestamp, host, tags));
185            }
186        }
187    }
188
189    private void reportHistogram(String name, Histogram histogram, long timestamp, List<String> tags)
190            throws IOException {
191        final Snapshot snapshot = histogram.getSnapshot();
192
193        if (expansions.contains(Expansion.COUNT)) {
194            request.addGauge(new DatadogGauge(appendExpansionSuffix(name, Expansion.COUNT), histogram.getCount(),
195                    timestamp, host, tags));
196        }
197
198        Number[] values = { snapshot.getMax(), snapshot.getMean(), snapshot.getMin(), snapshot.getStdDev(),
199                snapshot.getMedian(), snapshot.get75thPercentile(), snapshot.get95thPercentile(),
200                snapshot.get98thPercentile(), snapshot.get99thPercentile(), snapshot.get999thPercentile() };
201
202        for (int i = 0; i < STATS_EXPANSIONS.length; i++) {
203            if (expansions.contains(STATS_EXPANSIONS[i])) {
204                request.addGauge(new DatadogGauge(appendExpansionSuffix(name, STATS_EXPANSIONS[i]), toNumber(values[i]),
205                        timestamp, host, tags));
206            }
207        }
208    }
209
210    private void reportCounter(String name, Counter counter, long timestamp, List<String> tags) throws IOException {
211        // A Metrics counter is actually a Datadog Gauge. Datadog Counters are for rates which is
212        // similar to the Metrics Meter type. Metrics counters have increment and decrement
213        // functionality, which implies they are instantaneously measurable, which implies they are
214        // actually a gauge. The Metrics documentation agrees, stating:
215        // "A counter is just a gauge for an AtomicLong instance. You can increment or decrement its
216        // value. For example, we may want a more efficient way of measuring the pending job in a queue"
217        request.addGauge(new DatadogGauge(metricNameFormatter.format(name), counter.getCount(), timestamp, host, tags));
218    }
219
220    /**
221     * Gauges are the only metrics which can throw exceptions. With a thrown exception all other metrics will not be
222     * reported to Datadog.
223     */
224    private void reportGauge(String name, Gauge gauge, long timestamp, List<String> tags) {
225        try {
226            final Number value = toNumber(gauge.getValue());
227            if (value != null) {
228                request.addGauge(new DatadogGauge(metricNameFormatter.format(name), value, timestamp, host, tags));
229            }
230        } catch (Exception e) {
231            String errorMessage = String.format("Error reporting gauge metric (name: %s, tags: %s) to Datadog, "
232                    + "continuing reporting other metrics.", name, tags);
233            log.error(errorMessage, e);
234        }
235    }
236
237    private Number toNumber(Object o) {
238        if (o instanceof Number) {
239            return (Number) o;
240        }
241        return null;
242    }
243
244    private String appendExpansionSuffix(String name, Expansion expansion) {
245        return metricNameFormatter.format(name, expansion.toString());
246    }
247
248    private String prefix(String name) {
249        if (prefix == null) {
250            return name;
251        } else {
252            return String.format("%s.%s", prefix, name);
253        }
254    }
255
256    public static enum Expansion {
257        COUNT("count"), RATE_MEAN("meanRate"), RATE_1_MINUTE("1MinuteRate"), RATE_5_MINUTE(
258                "5MinuteRate"), RATE_15_MINUTE("15MinuteRate"), MIN("min"), MEAN("mean"), MAX("max"), STD_DEV(
259                        "stddev"), MEDIAN("median"), P75("p75"), P95("p95"), P98("p98"), P99("p99"), P999("p999");
260
261        public static EnumSet<Expansion> ALL = EnumSet.allOf(Expansion.class);
262
263        private final String displayName;
264
265        private Expansion(String displayName) {
266            this.displayName = displayName;
267        }
268
269        @Override
270        public String toString() {
271            return displayName;
272        }
273    }
274
275    public static Builder forRegistry(MetricRegistry registry) {
276        return new Builder(registry);
277    }
278
279    public static class Builder {
280        private final MetricRegistry registry;
281
282        private String host;
283
284        private EnumSet<Expansion> expansions;
285
286        private Clock clock;
287
288        private TimeUnit rateUnit;
289
290        private TimeUnit durationUnit;
291
292        private MetricFilter filter;
293
294        private MetricNameFormatter metricNameFormatter;
295
296        private List<String> tags;
297
298        private Transport transport;
299
300        private String prefix;
301
302        public Builder(MetricRegistry registry) {
303            this.registry = registry;
304            this.expansions = Expansion.ALL;
305            this.clock = Clock.defaultClock();
306            this.rateUnit = TimeUnit.SECONDS;
307            this.durationUnit = TimeUnit.MILLISECONDS;
308            this.filter = MetricFilter.ALL;
309            this.metricNameFormatter = new DefaultMetricNameFormatter();
310            this.tags = new ArrayList<>();
311        }
312
313        public Builder withHost(String host) {
314            this.host = host;
315            return this;
316        }
317
318        public Builder withEC2Host() throws IOException {
319            this.host = AwsHelper.getEc2InstanceId();
320            return this;
321        }
322
323        public Builder withExpansions(EnumSet<Expansion> expansions) {
324            this.expansions = expansions;
325            return this;
326        }
327
328        public Builder convertRatesTo(TimeUnit rateUnit) {
329            this.rateUnit = rateUnit;
330            return this;
331        }
332
333        /**
334         * Tags that would be sent to datadog with each and every metrics. This could be used to set global metrics like
335         * version of the app, environment etc.
336         *
337         * @param tags List of tags eg: [env:prod, version:1.0.1, name:kafka_client] etc
338         */
339        public Builder withTags(List<String> tags) {
340            this.tags = tags;
341            return this;
342        }
343
344        /**
345         * Prefix all metric names with the given string.
346         *
347         * @param prefix The prefix for all metric names.
348         */
349        public Builder withPrefix(String prefix) {
350            this.prefix = prefix;
351            return this;
352        }
353
354        public Builder withClock(Clock clock) {
355            this.clock = clock;
356            return this;
357        }
358
359        public Builder filter(MetricFilter filter) {
360            this.filter = filter;
361            return this;
362        }
363
364        public Builder withMetricNameFormatter(MetricNameFormatter formatter) {
365            this.metricNameFormatter = formatter;
366            return this;
367        }
368
369        public Builder convertDurationsTo(TimeUnit durationUnit) {
370            this.durationUnit = durationUnit;
371            return this;
372        }
373
374        /**
375         * The transport mechanism to push metrics to datadog. Supports http webservice and UDP dogstatsd protocol as of
376         * now.
377         *
378         * @see org.coursera.metrics.datadog.transport.HttpTransport
379         * @see org.coursera.metrics.datadog.transport.UdpTransport
380         */
381        public Builder withTransport(Transport transport) {
382            this.transport = transport;
383            return this;
384        }
385
386        public NuxeoDatadogReporter build() {
387            if (transport == null) {
388                throw new IllegalArgumentException(
389                        "Transport for datadog reporter is null. " + "Please set a valid transport");
390            }
391            return new NuxeoDatadogReporter(this.registry, this.transport, this.filter, this.clock, this.host,
392                    this.expansions, this.rateUnit, this.durationUnit, this.metricNameFormatter, this.tags,
393                    this.prefix);
394        }
395    }
396}