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}