From 529676eb883f15e8ddb3edf75154cca643919c71 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Wed, 11 Dec 2024 23:12:01 +0530 Subject: [PATCH 1/3] Add support for dw-metrics 4.x Signed-off-by: Kinshuk Bairagi --- pom.xml | 1 + .../pom.xml | 79 +++++ .../dropwizard/DropwizardExports.java | 274 +++++++++++++++ .../dropwizard/DropwizardExportsTest.java | 322 ++++++++++++++++++ .../version-rules.xml | 6 + 5 files changed, 682 insertions(+) create mode 100644 prometheus-metrics-instrumentation-dropwizard/pom.xml create mode 100644 prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java create mode 100644 prometheus-metrics-instrumentation-dropwizard/src/test/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExportsTest.java create mode 100644 prometheus-metrics-instrumentation-dropwizard/version-rules.xml 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..d177a4e86 --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard/src/test/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExportsTest.java @@ -0,0 +1,322 @@ +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 static org.assertj.core.data.Offset.offset; + +import com.codahale.metrics.*; +import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.Quantiles; +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 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\n" + + "# HELP foo_bar Generated from Dropwizard metric import (metric=foo.bar, type=com.codahale.metrics.Counter)\n" + + "foo_bar_total 1.0\n" + + "# EOF\n"; + + 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\n" + + "# HELP boolean_gauge Generated from Dropwizard metric import (metric=boolean.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$5)\n" + + "boolean_gauge 1.0\n" + + "# TYPE double_gauge gauge\n" + + "# HELP double_gauge Generated from Dropwizard metric import (metric=double.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$2)\n" + + "double_gauge 1.234\n" + + "# TYPE float_gauge gauge\n" + + "# HELP float_gauge Generated from Dropwizard metric import (metric=float.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$4)\n" + + "float_gauge 0.1234000027179718\n" + + "# TYPE integer_gauge gauge\n" + + "# HELP integer_gauge Generated from Dropwizard metric import (metric=integer.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$1)\n" + + "integer_gauge 1234.0\n" + + "# TYPE long_gauge gauge\n" + + "# HELP long_gauge Generated from Dropwizard metric import (metric=long.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$3)\n" + + "long_gauge 1234.0\n" + + "# EOF\n"; + + 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 + // + // # 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 + // + // # 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, but allows an error of 1.0 for quantile values. + + MetricSnapshots snapshots = pmRegistry.scrape(name -> name.equals("hist")); + assertThat(snapshots.size()).isOne(); + SummarySnapshot snapshot = (SummarySnapshot) snapshots.get(0); + assertThat(snapshot.getMetadata().getName()).isEqualTo("hist"); + assertThat(snapshot.getMetadata().getHelp()) + .isEqualTo( + "Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)"); + assertThat(snapshot.getDataPoints().size()).isOne(); + SummarySnapshot.SummaryDataPointSnapshot dataPoint = snapshot.getDataPoints().get(0); + assertThat(dataPoint.hasCount()).isTrue(); + assertThat(dataPoint.getCount()).isEqualTo(100); + assertThat(dataPoint.hasSum()).isFalse(); + Quantiles quantiles = dataPoint.getQuantiles(); + assertThat(quantiles.size()).isEqualTo(6); + assertThat(quantiles.get(0).getQuantile()).isEqualTo(0.5); + assertThat(quantiles.get(0).getValue()).isCloseTo(49.0, offset(1.0)); + assertThat(quantiles.get(1).getQuantile()).isEqualTo(0.75); + assertThat(quantiles.get(1).getValue()).isCloseTo(74.0, offset(1.0)); + assertThat(quantiles.get(2).getQuantile()).isEqualTo(0.95); + assertThat(quantiles.get(2).getValue()).isCloseTo(94.0, offset(1.0)); + assertThat(quantiles.get(3).getQuantile()).isEqualTo(0.98); + assertThat(quantiles.get(3).getValue()).isCloseTo(97.0, offset(1.0)); + assertThat(quantiles.get(4).getQuantile()).isEqualTo(0.99); + assertThat(quantiles.get(4).getValue()).isCloseTo(98.0, offset(1.0)); + assertThat(quantiles.get(5).getQuantile()).isEqualTo(0.999); + assertThat(quantiles.get(5).getValue()).isCloseTo(99.0, offset(1.0)); + } + + @Test + public void testMeter() { + Meter meter = metricRegistry.meter("meter"); + meter.mark(); + meter.mark(); + + String expected = + "# TYPE meter counter\n" + + "# HELP meter Generated from Dropwizard metric import (metric=meter_total, type=com.codahale.metrics.Meter)\n" + + "meter_total 2.0\n" + + "# EOF\n"; + 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); + System.out.println(timeSpentMillis); + + SummarySnapshot.SummaryDataPointSnapshot dataPointSnapshot = + (SummarySnapshot.SummaryDataPointSnapshot) + exports.collect().stream().flatMap(i -> i.getDataPoints().stream()).findFirst().get(); + // We slept for 1Ms so we ensure that all timers are above 1ms: + assertThat(dataPointSnapshot.getQuantiles().size()).isGreaterThan(1); + dataPointSnapshot + .getQuantiles() + .forEach( + i -> { + System.out.println(i.getQuantile() + " : " + i.getValue()); + 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\n" + + "# HELP my_application_namedCounter1 Generated from Dropwizard metric import (metric=my.application.namedCounter1, type=com.codahale.metrics.Counter)\n" + + "my_application_namedCounter1_total 0.0\n" + + "# TYPE my_application_namedGauge1 gauge\n" + + "# HELP my_application_namedGauge1 Generated from Dropwizard metric import (metric=my.application.namedGauge1, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$ExampleDoubleGauge)\n" + + "my_application_namedGauge1 0.0\n" + + "# TYPE my_application_namedHistogram1 summary\n" + + "# HELP my_application_namedHistogram1 Generated from Dropwizard metric import (metric=my.application.namedHistogram1, type=com.codahale.metrics.Histogram)\n" + + "my_application_namedHistogram1{quantile=\"0.5\"} 0.0\n" + + "my_application_namedHistogram1{quantile=\"0.75\"} 0.0\n" + + "my_application_namedHistogram1{quantile=\"0.95\"} 0.0\n" + + "my_application_namedHistogram1{quantile=\"0.98\"} 0.0\n" + + "my_application_namedHistogram1{quantile=\"0.99\"} 0.0\n" + + "my_application_namedHistogram1{quantile=\"0.999\"} 0.0\n" + + "my_application_namedHistogram1_count 0\n" + + "# TYPE my_application_namedMeter1 counter\n" + + "# HELP my_application_namedMeter1 Generated from Dropwizard metric import (metric=my.application.namedMeter1_total, type=com.codahale.metrics.Meter)\n" + + "my_application_namedMeter1_total 0.0\n" + + "# TYPE my_application_namedTimer1 summary\n" + + "# HELP my_application_namedTimer1 Generated from Dropwizard metric import (metric=my.application.namedTimer1, type=com.codahale.metrics.Timer)\n" + + "my_application_namedTimer1{quantile=\"0.5\"} 0.0\n" + + "my_application_namedTimer1{quantile=\"0.75\"} 0.0\n" + + "my_application_namedTimer1{quantile=\"0.95\"} 0.0\n" + + "my_application_namedTimer1{quantile=\"0.98\"} 0.0\n" + + "my_application_namedTimer1{quantile=\"0.99\"} 0.0\n" + + "my_application_namedTimer1{quantile=\"0.999\"} 0.0\n" + + "my_application_namedTimer1_count 0\n" + + "# EOF\n"; + 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.name()); + } 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 @@ + + + + From 9fda9439bb016b059f8635e29a64e6719f3d33f6 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Fri, 13 Dec 2024 09:59:24 +0530 Subject: [PATCH 2/3] Update DropwizardExportsTest.java Signed-off-by: Kinshuk Bairagi --- .../dropwizard/DropwizardExportsTest.java | 87 +++++++------------ 1 file changed, 30 insertions(+), 57 deletions(-) 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 index d177a4e86..5b5c94b0c 100644 --- 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 @@ -3,13 +3,10 @@ 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 static org.assertj.core.data.Offset.offset; import com.codahale.metrics.*; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; import io.prometheus.metrics.model.registry.PrometheusRegistry; -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.Quantiles; import io.prometheus.metrics.model.snapshots.SummarySnapshot; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -159,62 +156,38 @@ public void testHistogram() { } // The result should look like this - // - // # 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 - // + String expected1 = + "# TYPE hist summary\n" + + "# HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)\n" + + "hist{quantile=\"0.5\"} 49.0\n" + + "hist{quantile=\"0.75\"} 74.0\n" + + "hist{quantile=\"0.95\"} 94.0\n" + + "hist{quantile=\"0.98\"} 97.0\n" + + "hist{quantile=\"0.99\"} 98.0\n" + + "hist{quantile=\"0.999\"} 99.0\n" + + "hist_count 100\n" + + "# EOF\n"; + // However, Dropwizard uses a random reservoir sampling algorithm, so the values could as well // be off-by-one - // - // # 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, but allows an error of 1.0 for quantile values. - - MetricSnapshots snapshots = pmRegistry.scrape(name -> name.equals("hist")); - assertThat(snapshots.size()).isOne(); - SummarySnapshot snapshot = (SummarySnapshot) snapshots.get(0); - assertThat(snapshot.getMetadata().getName()).isEqualTo("hist"); - assertThat(snapshot.getMetadata().getHelp()) - .isEqualTo( - "Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)"); - assertThat(snapshot.getDataPoints().size()).isOne(); - SummarySnapshot.SummaryDataPointSnapshot dataPoint = snapshot.getDataPoints().get(0); - assertThat(dataPoint.hasCount()).isTrue(); - assertThat(dataPoint.getCount()).isEqualTo(100); - assertThat(dataPoint.hasSum()).isFalse(); - Quantiles quantiles = dataPoint.getQuantiles(); - assertThat(quantiles.size()).isEqualTo(6); - assertThat(quantiles.get(0).getQuantile()).isEqualTo(0.5); - assertThat(quantiles.get(0).getValue()).isCloseTo(49.0, offset(1.0)); - assertThat(quantiles.get(1).getQuantile()).isEqualTo(0.75); - assertThat(quantiles.get(1).getValue()).isCloseTo(74.0, offset(1.0)); - assertThat(quantiles.get(2).getQuantile()).isEqualTo(0.95); - assertThat(quantiles.get(2).getValue()).isCloseTo(94.0, offset(1.0)); - assertThat(quantiles.get(3).getQuantile()).isEqualTo(0.98); - assertThat(quantiles.get(3).getValue()).isCloseTo(97.0, offset(1.0)); - assertThat(quantiles.get(4).getQuantile()).isEqualTo(0.99); - assertThat(quantiles.get(4).getValue()).isCloseTo(98.0, offset(1.0)); - assertThat(quantiles.get(5).getQuantile()).isEqualTo(0.999); - assertThat(quantiles.get(5).getValue()).isCloseTo(99.0, offset(1.0)); + String expected2 = + "# TYPE hist summary\n" + + "# HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)\n" + + "hist{quantile=\"0.5\"} 50.0\n" + + "hist{quantile=\"0.75\"} 75.0\n" + + "hist{quantile=\"0.95\"} 95.0\n" + + "hist{quantile=\"0.98\"} 98.0\n" + + "hist{quantile=\"0.99\"} 99.0\n" + + "hist{quantile=\"0.999\"} 99.0\n" + + "hist_count 100\n" + + "# EOF\n"; + + // 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 From 657788c3be56cc8f173cbcfb50009fdb061961c2 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 13 Dec 2024 10:31:02 +0100 Subject: [PATCH 3/3] cleanup Signed-off-by: Gregor Zeitlinger --- .../dropwizard/DropwizardExportsTest.java | 189 +++++++++--------- 1 file changed, 98 insertions(+), 91 deletions(-) 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 index 5b5c94b0c..ff658ad41 100644 --- 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 @@ -17,7 +17,7 @@ class DropwizardExportsTest { - private PrometheusRegistry registry = new PrometheusRegistry(); + private final PrometheusRegistry registry = new PrometheusRegistry(); private MetricRegistry metricRegistry; @BeforeEach @@ -39,9 +39,7 @@ public void testBuilderThrowsErrorOnNullRegistry() { @Test public void testBuilderCreatesOkay() { assertThatCode( - () -> { - DropwizardExports.builder().dropwizardRegistry(metricRegistry).register(registry); - }) + () -> DropwizardExports.builder().dropwizardRegistry(metricRegistry).register(registry)) .doesNotThrowAnyException(); } @@ -49,10 +47,12 @@ public void testBuilderCreatesOkay() { public void testCounter() { metricRegistry.counter("foo.bar").inc(1); String expected = - "# TYPE foo_bar counter\n" - + "# HELP foo_bar Generated from Dropwizard metric import (metric=foo.bar, type=com.codahale.metrics.Counter)\n" - + "foo_bar_total 1.0\n" - + "# EOF\n"; + """ + # 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); } @@ -103,22 +103,24 @@ public Boolean getValue() { metricRegistry.register("boolean.gauge", booleanGauge); String expected = - "# TYPE boolean_gauge gauge\n" - + "# HELP boolean_gauge Generated from Dropwizard metric import (metric=boolean.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$5)\n" - + "boolean_gauge 1.0\n" - + "# TYPE double_gauge gauge\n" - + "# HELP double_gauge Generated from Dropwizard metric import (metric=double.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$2)\n" - + "double_gauge 1.234\n" - + "# TYPE float_gauge gauge\n" - + "# HELP float_gauge Generated from Dropwizard metric import (metric=float.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$4)\n" - + "float_gauge 0.1234000027179718\n" - + "# TYPE integer_gauge gauge\n" - + "# HELP integer_gauge Generated from Dropwizard metric import (metric=integer.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$1)\n" - + "integer_gauge 1234.0\n" - + "# TYPE long_gauge gauge\n" - + "# HELP long_gauge Generated from Dropwizard metric import (metric=long.gauge, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$3)\n" - + "long_gauge 1234.0\n" - + "# EOF\n"; + """ + # 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); } @@ -157,30 +159,34 @@ public void testHistogram() { // The result should look like this String expected1 = - "# TYPE hist summary\n" - + "# HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)\n" - + "hist{quantile=\"0.5\"} 49.0\n" - + "hist{quantile=\"0.75\"} 74.0\n" - + "hist{quantile=\"0.95\"} 94.0\n" - + "hist{quantile=\"0.98\"} 97.0\n" - + "hist{quantile=\"0.99\"} 98.0\n" - + "hist{quantile=\"0.999\"} 99.0\n" - + "hist_count 100\n" - + "# EOF\n"; + """ + # 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\n" - + "# HELP hist Generated from Dropwizard metric import (metric=hist, type=com.codahale.metrics.Histogram)\n" - + "hist{quantile=\"0.5\"} 50.0\n" - + "hist{quantile=\"0.75\"} 75.0\n" - + "hist{quantile=\"0.95\"} 95.0\n" - + "hist{quantile=\"0.98\"} 98.0\n" - + "hist{quantile=\"0.99\"} 99.0\n" - + "hist{quantile=\"0.999\"} 99.0\n" - + "hist_count 100\n" - + "# EOF\n"; + """ + # 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); @@ -197,10 +203,12 @@ public void testMeter() { meter.mark(); String expected = - "# TYPE meter counter\n" - + "# HELP meter Generated from Dropwizard metric import (metric=meter_total, type=com.codahale.metrics.Meter)\n" - + "meter_total 2.0\n" - + "# EOF\n"; + """ + # 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); } @@ -213,26 +221,23 @@ public void testTimer() throws InterruptedException { Thread.sleep(100L); long timeSpentNanos = time.stop(); double timeSpentMillis = TimeUnit.NANOSECONDS.toMillis(timeSpentNanos); - System.out.println(timeSpentMillis); - SummarySnapshot.SummaryDataPointSnapshot dataPointSnapshot = - (SummarySnapshot.SummaryDataPointSnapshot) - exports.collect().stream().flatMap(i -> i.getDataPoints().stream()).findFirst().get(); - // We slept for 1Ms so we ensure that all timers are above 1ms: - assertThat(dataPointSnapshot.getQuantiles().size()).isGreaterThan(1); - dataPointSnapshot - .getQuantiles() - .forEach( - i -> { - System.out.println(i.getQuantile() + " : " + i.getValue()); - assertThat(i.getValue()).isGreaterThan(timeSpentMillis / 1000d); + 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(); }); - assertThat(dataPointSnapshot.getCount()).isOne(); } @Test public void testThatMetricHelpUsesOriginalDropwizardName() { - metricRegistry.timer("my.application.namedTimer1"); metricRegistry.counter("my.application.namedCounter1"); metricRegistry.meter("my.application.namedMeter1"); @@ -240,34 +245,36 @@ public void testThatMetricHelpUsesOriginalDropwizardName() { metricRegistry.register("my.application.namedGauge1", new ExampleDoubleGauge()); String expected = - "# TYPE my_application_namedCounter1 counter\n" - + "# HELP my_application_namedCounter1 Generated from Dropwizard metric import (metric=my.application.namedCounter1, type=com.codahale.metrics.Counter)\n" - + "my_application_namedCounter1_total 0.0\n" - + "# TYPE my_application_namedGauge1 gauge\n" - + "# HELP my_application_namedGauge1 Generated from Dropwizard metric import (metric=my.application.namedGauge1, type=io.prometheus.metrics.instrumentation.dropwizard.DropwizardExportsTest$ExampleDoubleGauge)\n" - + "my_application_namedGauge1 0.0\n" - + "# TYPE my_application_namedHistogram1 summary\n" - + "# HELP my_application_namedHistogram1 Generated from Dropwizard metric import (metric=my.application.namedHistogram1, type=com.codahale.metrics.Histogram)\n" - + "my_application_namedHistogram1{quantile=\"0.5\"} 0.0\n" - + "my_application_namedHistogram1{quantile=\"0.75\"} 0.0\n" - + "my_application_namedHistogram1{quantile=\"0.95\"} 0.0\n" - + "my_application_namedHistogram1{quantile=\"0.98\"} 0.0\n" - + "my_application_namedHistogram1{quantile=\"0.99\"} 0.0\n" - + "my_application_namedHistogram1{quantile=\"0.999\"} 0.0\n" - + "my_application_namedHistogram1_count 0\n" - + "# TYPE my_application_namedMeter1 counter\n" - + "# HELP my_application_namedMeter1 Generated from Dropwizard metric import (metric=my.application.namedMeter1_total, type=com.codahale.metrics.Meter)\n" - + "my_application_namedMeter1_total 0.0\n" - + "# TYPE my_application_namedTimer1 summary\n" - + "# HELP my_application_namedTimer1 Generated from Dropwizard metric import (metric=my.application.namedTimer1, type=com.codahale.metrics.Timer)\n" - + "my_application_namedTimer1{quantile=\"0.5\"} 0.0\n" - + "my_application_namedTimer1{quantile=\"0.75\"} 0.0\n" - + "my_application_namedTimer1{quantile=\"0.95\"} 0.0\n" - + "my_application_namedTimer1{quantile=\"0.98\"} 0.0\n" - + "my_application_namedTimer1{quantile=\"0.99\"} 0.0\n" - + "my_application_namedTimer1{quantile=\"0.999\"} 0.0\n" - + "my_application_namedTimer1_count 0\n" - + "# EOF\n"; + """ + # 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); } @@ -283,7 +290,7 @@ private String convertToOpenMetricsFormat(PrometheusRegistry _registry) { OpenMetricsTextFormatWriter writer = new OpenMetricsTextFormatWriter(true, true); try { writer.write(out, _registry.scrape()); - return out.toString(StandardCharsets.UTF_8.name()); + return out.toString(StandardCharsets.UTF_8); } catch (IOException e) { throw new RuntimeException(e); }