diff --git a/pom.xml b/pom.xml index 212fe042d..2f9aa6dff 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,7 @@ prometheus-metrics-instrumentation-caffeine prometheus-metrics-instrumentation-jvm prometheus-metrics-instrumentation-dropwizard5 + prometheus-metrics-instrumentation-dropwizard prometheus-metrics-instrumentation-guava prometheus-metrics-simpleclient-bridge diff --git a/prometheus-metrics-instrumentation-dropwizard/pom.xml b/prometheus-metrics-instrumentation-dropwizard/pom.xml new file mode 100644 index 000000000..d6f6a62ae --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + io.prometheus + client_java + 10.0.0-SNAPSHOT + + + prometheus-metrics-instrumentation-dropwizard + bundle + + Prometheus Metrics Instrumentation - Dropwizard 4.x + + Instrumentation library for Dropwizard metrics 4.x + + + + io.prometheus.metrics.instrumentation.dropwizard + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + kingster + Kinshuk Bairagi + hi@kinsh.uk + + + + + + + io.prometheus + prometheus-metrics-core + ${project.version} + + + io.dropwizard.metrics + metrics-core + 4.2.0 + provided + + + + io.prometheus + prometheus-metrics-exporter-httpserver + ${project.version} + test + + + io.prometheus + prometheus-metrics-exposition-textformats + ${project.version} + test + + + io.prometheus + prometheus-metrics-instrumentation-dropwizard5 + ${project.version} + compile + + + io.dropwizard.metrics5 + metrics-core + + + + + + + diff --git a/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java new file mode 100644 index 000000000..dff657683 --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java @@ -0,0 +1,274 @@ +package io.prometheus.metrics.instrumentation.dropwizard; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.Meter; +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricFilter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Snapshot; +import com.codahale.metrics.Timer; +import io.prometheus.metrics.instrumentation.dropwizard5.labels.CustomLabelMapper; +import io.prometheus.metrics.model.registry.MultiCollector; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.CounterSnapshot; +import io.prometheus.metrics.model.snapshots.GaugeSnapshot; +import io.prometheus.metrics.model.snapshots.MetricMetadata; +import io.prometheus.metrics.model.snapshots.MetricSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; +import io.prometheus.metrics.model.snapshots.Quantiles; +import io.prometheus.metrics.model.snapshots.SummarySnapshot; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** Collect Dropwizard metrics from a MetricRegistry. */ +public class DropwizardExports implements MultiCollector { + private static final Logger logger = Logger.getLogger(DropwizardExports.class.getName()); + private final MetricRegistry registry; + private final MetricFilter metricFilter; + private final Optional labelMapper; + + /** + * Creates a new DropwizardExports and {@link MetricFilter#ALL}. + * + * @param registry a metric registry to export in prometheus. + */ + public DropwizardExports(MetricRegistry registry) { + super(); + this.registry = registry; + this.metricFilter = MetricFilter.ALL; + this.labelMapper = Optional.empty(); + } + + /** + * Creates a new DropwizardExports with a custom {@link MetricFilter}. + * + * @param registry a metric registry to export in prometheus. + * @param metricFilter a custom metric filter. + */ + public DropwizardExports(MetricRegistry registry, MetricFilter metricFilter) { + this.registry = registry; + this.metricFilter = metricFilter; + this.labelMapper = Optional.empty(); + } + + /** + * @param registry a metric registry to export in prometheus. + * @param metricFilter a custom metric filter. + * @param labelMapper a labelMapper to use to map labels. + */ + public DropwizardExports( + MetricRegistry registry, MetricFilter metricFilter, CustomLabelMapper labelMapper) { + this.registry = registry; + this.metricFilter = metricFilter; + this.labelMapper = Optional.ofNullable(labelMapper); + } + + private static String getHelpMessage(String metricName, Metric metric) { + return String.format( + "Generated from Dropwizard metric import (metric=%s, type=%s)", + metricName, metric.getClass().getName()); + } + + private MetricMetadata getMetricMetaData(String metricName, Metric metric) { + String name = labelMapper.isPresent() ? labelMapper.get().getName(metricName) : metricName; + return new MetricMetadata( + PrometheusNaming.sanitizeMetricName(name), getHelpMessage(metricName, metric)); + } + + /** + * Export counter as Prometheus Gauge. + */ + MetricSnapshot fromCounter(String dropwizardName, Counter counter) { + MetricMetadata metadata = getMetricMetaData(dropwizardName, counter); + CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = + CounterSnapshot.CounterDataPointSnapshot.builder() + .value(Long.valueOf(counter.getCount()).doubleValue()); + labelMapper.ifPresent( + mapper -> + dataPointBuilder.labels( + mapper.getLabels( + dropwizardName, Collections.emptyList(), Collections.emptyList()))); + return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** Export gauge as a prometheus gauge. */ + MetricSnapshot fromGauge(String dropwizardName, Gauge gauge) { + Object obj = gauge.getValue(); + double value; + if (obj instanceof Number) { + value = ((Number) obj).doubleValue(); + } else if (obj instanceof Boolean) { + value = ((Boolean) obj) ? 1 : 0; + } else { + logger.log( + Level.FINE, + String.format( + "Invalid type for Gauge %s: %s", + PrometheusNaming.sanitizeMetricName(dropwizardName), + obj == null ? "null" : obj.getClass().getName())); + return null; + } + MetricMetadata metadata = getMetricMetaData(dropwizardName, gauge); + GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPointBuilder = + GaugeSnapshot.GaugeDataPointSnapshot.builder().value(value); + labelMapper.ifPresent( + mapper -> + dataPointBuilder.labels( + mapper.getLabels( + dropwizardName, Collections.emptyList(), Collections.emptyList()))); + return new GaugeSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** + * Export a histogram snapshot as a prometheus SUMMARY. + * + * @param dropwizardName metric name. + * @param snapshot the histogram snapshot. + * @param count the total sample count for this snapshot. + * @param factor a factor to apply to histogram values. + */ + MetricSnapshot fromSnapshotAndCount( + String dropwizardName, Snapshot snapshot, long count, double factor, String helpMessage) { + Quantiles quantiles = + Quantiles.builder() + .quantile(0.5, snapshot.getMedian() * factor) + .quantile(0.75, snapshot.get75thPercentile() * factor) + .quantile(0.95, snapshot.get95thPercentile() * factor) + .quantile(0.98, snapshot.get98thPercentile() * factor) + .quantile(0.99, snapshot.get99thPercentile() * factor) + .quantile(0.999, snapshot.get999thPercentile() * factor) + .build(); + + MetricMetadata metadata = + new MetricMetadata(PrometheusNaming.sanitizeMetricName(dropwizardName), helpMessage); + SummarySnapshot.SummaryDataPointSnapshot.Builder dataPointBuilder = + SummarySnapshot.SummaryDataPointSnapshot.builder().quantiles(quantiles).count(count); + labelMapper.ifPresent( + mapper -> + dataPointBuilder.labels( + mapper.getLabels( + dropwizardName, Collections.emptyList(), Collections.emptyList()))); + return new SummarySnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** Convert histogram snapshot. */ + MetricSnapshot fromHistogram(String dropwizardName, Histogram histogram) { + return fromSnapshotAndCount( + dropwizardName, + histogram.getSnapshot(), + histogram.getCount(), + 1.0, + getHelpMessage(dropwizardName, histogram)); + } + + /** Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. */ + MetricSnapshot fromTimer(String dropwizardName, Timer timer) { + return fromSnapshotAndCount( + dropwizardName, + timer.getSnapshot(), + timer.getCount(), + 1.0D / TimeUnit.SECONDS.toNanos(1L), + getHelpMessage(dropwizardName, timer)); + } + + /** Export a Meter as a prometheus COUNTER. */ + MetricSnapshot fromMeter(String dropwizardName, Meter meter) { + MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", meter); + CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = + CounterSnapshot.CounterDataPointSnapshot.builder().value(meter.getCount()); + labelMapper.ifPresent( + mapper -> + dataPointBuilder.labels( + mapper.getLabels( + dropwizardName, Collections.emptyList(), Collections.emptyList()))); + return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + @Override + public MetricSnapshots collect() { + MetricSnapshots.Builder metricSnapshots = MetricSnapshots.builder(); + + registry + .getGauges(metricFilter) + .forEach( + (name, gauge) -> { + MetricSnapshot snapshot = fromGauge(name, gauge); + if (snapshot != null) { + metricSnapshots.metricSnapshot(snapshot); + } + }); + + registry + .getCounters(metricFilter) + .forEach((name, counter) -> metricSnapshots.metricSnapshot(fromCounter(name, counter))); + registry + .getHistograms(metricFilter) + .forEach( + (name, histogram) -> metricSnapshots.metricSnapshot(fromHistogram(name, histogram))); + registry + .getTimers(metricFilter) + .forEach((name, timer) -> metricSnapshots.metricSnapshot(fromTimer(name, timer))); + registry + .getMeters(metricFilter) + .forEach((name, meter) -> metricSnapshots.metricSnapshot(fromMeter(name, meter))); + + return metricSnapshots.build(); + } + + public static Builder builder() { + return new Builder(); + } + + // Builder class for DropwizardExports + public static class Builder { + private MetricRegistry registry; + private MetricFilter metricFilter; + private CustomLabelMapper labelMapper; + + private Builder() { + this.metricFilter = MetricFilter.ALL; + } + + public Builder dropwizardRegistry(MetricRegistry registry) { + this.registry = registry; + return this; + } + + public Builder metricFilter(MetricFilter metricFilter) { + this.metricFilter = metricFilter; + return this; + } + + public Builder customLabelMapper(CustomLabelMapper labelMapper) { + this.labelMapper = labelMapper; + return this; + } + + DropwizardExports build() { + if (registry == null) { + throw new IllegalArgumentException("MetricRegistry must be set"); + } + if (labelMapper == null) { + return new DropwizardExports(registry, metricFilter); + } else { + return new DropwizardExports(registry, metricFilter, labelMapper); + } + } + + public void register() { + register(PrometheusRegistry.defaultRegistry); + } + + public void register(PrometheusRegistry registry) { + DropwizardExports dropwizardExports = build(); + registry.register(dropwizardExports); + } + } +} diff --git a/prometheus-metrics-instrumentation-dropwizard/src/test/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExportsTest.java b/prometheus-metrics-instrumentation-dropwizard/src/test/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExportsTest.java new file mode 100644 index 000000000..ff658ad41 --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard/src/test/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExportsTest.java @@ -0,0 +1,302 @@ +package io.prometheus.metrics.instrumentation.dropwizard; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.codahale.metrics.*; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.SummarySnapshot; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DropwizardExportsTest { + + private final PrometheusRegistry registry = new PrometheusRegistry(); + private MetricRegistry metricRegistry; + + @BeforeEach + public void setUp() { + metricRegistry = new MetricRegistry(); + DropwizardExports.builder() + .dropwizardRegistry(metricRegistry) + .metricFilter(MetricFilter.ALL) + .register(registry); + } + + @Test + public void testBuilderThrowsErrorOnNullRegistry() { + assertThatThrownBy( + () -> DropwizardExports.builder().dropwizardRegistry(null).register(registry)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testBuilderCreatesOkay() { + assertThatCode( + () -> DropwizardExports.builder().dropwizardRegistry(metricRegistry).register(registry)) + .doesNotThrowAnyException(); + } + + @Test + public void testCounter() { + metricRegistry.counter("foo.bar").inc(1); + String expected = + """ + # TYPE foo_bar counter + # HELP foo_bar Generated from Dropwizard metric import (metric=foo.bar, type=com.codahale.metrics.Counter) + foo_bar_total 1.0 + # EOF + """; + + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + @Test + public void testGauge() { + // don't convert to lambda, as we need to test the type + Gauge integerGauge = + new Gauge() { + @Override + public Integer getValue() { + return 1234; + } + }; + Gauge doubleGauge = + new Gauge() { + @Override + public Double getValue() { + return 1.234D; + } + }; + Gauge longGauge = + new Gauge() { + @Override + public Long getValue() { + return 1234L; + } + }; + Gauge floatGauge = + new Gauge() { + @Override + public Float getValue() { + return 0.1234F; + } + }; + Gauge booleanGauge = + new Gauge() { + @Override + public Boolean getValue() { + return true; + } + }; + + metricRegistry.register("double.gauge", doubleGauge); + metricRegistry.register("long.gauge", longGauge); + metricRegistry.register("integer.gauge", integerGauge); + metricRegistry.register("float.gauge", floatGauge); + metricRegistry.register("boolean.gauge", booleanGauge); + + String expected = + """ + # TYPE boolean_gauge gauge + # HELP boolean_gauge Generated from Dropwizard metric import (metric=boolean.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$5) + boolean_gauge 1.0 + # TYPE double_gauge gauge + # HELP double_gauge Generated from Dropwizard metric import (metric=double.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$2) + double_gauge 1.234 + # TYPE float_gauge gauge + # HELP float_gauge Generated from Dropwizard metric import (metric=float.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$4) + float_gauge 0.1234000027179718 + # TYPE integer_gauge gauge + # HELP integer_gauge Generated from Dropwizard metric import (metric=integer.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$1) + integer_gauge 1234.0 + # TYPE long_gauge gauge + # HELP long_gauge Generated from Dropwizard metric import (metric=long.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$3) + long_gauge 1234.0 + # EOF + """; + + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + @Test + public void testInvalidGaugeType() { + Gauge invalidGauge = () -> "foobar"; + + metricRegistry.register("invalid_gauge", invalidGauge); + + String expected = "# EOF\n"; + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + @Test + public void testGaugeReturningNullValue() { + Gauge invalidGauge = () -> null; + metricRegistry.register("invalid_gauge", invalidGauge); + String expected = "# EOF\n"; + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + @Test + public void testHistogram() { + // just test the standard mapper + final MetricRegistry metricRegistry = new MetricRegistry(); + PrometheusRegistry pmRegistry = new PrometheusRegistry(); + DropwizardExports.builder().dropwizardRegistry(metricRegistry).register(pmRegistry); + + Histogram hist = metricRegistry.histogram("hist"); + int i = 0; + while (i < 100) { + hist.update(i); + i += 1; + } + + // The result should look like this + String expected1 = + """ + # TYPE hist summary + # HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram) + hist{quantile="0.5"} 49.0 + hist{quantile="0.75"} 74.0 + hist{quantile="0.95"} 94.0 + hist{quantile="0.98"} 97.0 + hist{quantile="0.99"} 98.0 + hist{quantile="0.999"} 99.0 + hist_count 100 + # EOF + """; + + // However, Dropwizard uses a random reservoir sampling algorithm, so the values could as well + // be off-by-one + String expected2 = + """ + # TYPE hist summary + # HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram) + hist{quantile="0.5"} 50.0 + hist{quantile="0.75"} 75.0 + hist{quantile="0.95"} 95.0 + hist{quantile="0.98"} 98.0 + hist{quantile="0.99"} 99.0 + hist{quantile="0.999"} 99.0 + hist_count 100 + # EOF + """; + + // The following asserts the values matches either of the expected value. + String textFormat = convertToOpenMetricsFormat(pmRegistry); + assertThat(textFormat) + .satisfiesAnyOf( + text -> assertThat(text).isEqualTo(expected1), + text -> assertThat(text).isEqualTo(expected2)); + } + + @Test + public void testMeter() { + Meter meter = metricRegistry.meter("meter"); + meter.mark(); + meter.mark(); + + String expected = + """ + # TYPE meter counter + # HELP meter Generated from Dropwizard metric import (metric=meter_total, type=com.codahale.metrics.Meter) + meter_total 2.0 + # EOF + """; + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + @Test + public void testTimer() throws InterruptedException { + final MetricRegistry metricRegistry = new MetricRegistry(); + DropwizardExports exports = new DropwizardExports(metricRegistry); + Timer t = metricRegistry.timer("timer"); + Timer.Context time = t.time(); + Thread.sleep(100L); + long timeSpentNanos = time.stop(); + double timeSpentMillis = TimeUnit.NANOSECONDS.toMillis(timeSpentNanos); + + assertThat(exports.collect().stream().flatMap(i1 -> i1.getDataPoints().stream()).findFirst()) + .containsInstanceOf(SummarySnapshot.SummaryDataPointSnapshot.class) + .hasValueSatisfying( + snapshot -> { + var dataPointSnapshot = (SummarySnapshot.SummaryDataPointSnapshot) snapshot; + // We slept for 1Ms so we ensure that all timers are above 1ms: + assertThat(dataPointSnapshot.getQuantiles().size()).isGreaterThan(1); + dataPointSnapshot + .getQuantiles() + .forEach(i -> assertThat(i.getValue()).isGreaterThan(timeSpentMillis / 1000d)); + assertThat(dataPointSnapshot.getCount()).isOne(); + }); + } + + @Test + public void testThatMetricHelpUsesOriginalDropwizardName() { + metricRegistry.timer("my.application.namedTimer1"); + metricRegistry.counter("my.application.namedCounter1"); + metricRegistry.meter("my.application.namedMeter1"); + metricRegistry.histogram("my.application.namedHistogram1"); + metricRegistry.register("my.application.namedGauge1", new ExampleDoubleGauge()); + + String expected = + """ + # TYPE my_application_namedCounter1 counter + # HELP my_application_namedCounter1 Generated from Dropwizard metric import (metric=my.application.namedCounter1, type=com.codahale.metrics.Counter) + my_application_namedCounter1_total 0.0 + # TYPE my_application_namedGauge1 gauge + # HELP my_application_namedGauge1 Generated from Dropwizard metric import (metric=my.application.namedGauge1, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$ExampleDoubleGauge) + my_application_namedGauge1 0.0 + # TYPE my_application_namedHistogram1 summary + # HELP my_application_namedHistogram1 Generated from Dropwizard metric import (metric=my.application.namedHistogram1, type=com.codahale.metrics.Histogram) + my_application_namedHistogram1{quantile="0.5"} 0.0 + my_application_namedHistogram1{quantile="0.75"} 0.0 + my_application_namedHistogram1{quantile="0.95"} 0.0 + my_application_namedHistogram1{quantile="0.98"} 0.0 + my_application_namedHistogram1{quantile="0.99"} 0.0 + my_application_namedHistogram1{quantile="0.999"} 0.0 + my_application_namedHistogram1_count 0 + # TYPE my_application_namedMeter1 counter + # HELP my_application_namedMeter1 Generated from Dropwizard metric import (metric=my.application.namedMeter1_total, type=com.codahale.metrics.Meter) + my_application_namedMeter1_total 0.0 + # TYPE my_application_namedTimer1 summary + # HELP my_application_namedTimer1 Generated from Dropwizard metric import (metric=my.application.namedTimer1, type=com.codahale.metrics.Timer) + my_application_namedTimer1{quantile="0.5"} 0.0 + my_application_namedTimer1{quantile="0.75"} 0.0 + my_application_namedTimer1{quantile="0.95"} 0.0 + my_application_namedTimer1{quantile="0.98"} 0.0 + my_application_namedTimer1{quantile="0.99"} 0.0 + my_application_namedTimer1{quantile="0.999"} 0.0 + my_application_namedTimer1_count 0 + # EOF + """; + assertThat(convertToOpenMetricsFormat()).isEqualTo(expected); + } + + private static class ExampleDoubleGauge implements Gauge { + @Override + public Double getValue() { + return 0.0; + } + } + + private String convertToOpenMetricsFormat(PrometheusRegistry _registry) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); + try { + writer.write(out, _registry.scrape()); + return out.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String convertToOpenMetricsFormat() { + return convertToOpenMetricsFormat(registry); + } +} diff --git a/prometheus-metrics-instrumentation-dropwizard/version-rules.xml b/prometheus-metrics-instrumentation-dropwizard/version-rules.xml new file mode 100644 index 000000000..5c6d39593 --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard/version-rules.xml @@ -0,0 +1,6 @@ + + + +