diff --git a/implementation/pom.xml b/implementation/pom.xml index e4c948fb..1e42e897 100644 --- a/implementation/pom.xml +++ b/implementation/pom.xml @@ -85,5 +85,13 @@ assertj-core test + + + org.glassfish + javax.json + 1.0.4 + test + + diff --git a/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusExporter.java b/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusExporter.java index caba8f8e..1353db31 100644 --- a/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusExporter.java +++ b/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusExporter.java @@ -176,14 +176,14 @@ private void writeTimerValues(StringBuffer sb, MetricRegistry.Type scope, TimerI writeMeterRateValues(sb, scope, timer.getMeter(), md); Snapshot snapshot = timer.getSnapshot(); - writeSnapshotBasics(sb, scope, md, snapshot, theUnit); + writeSnapshotBasics(sb, scope, md, snapshot, theUnit, true); String suffix = USCORE + PrometheusUnit.getBaseUnitAsPrometheusString(md.getUnit()); writeHelpLine(sb, scope, md.getName(), md, suffix); writeTypeLine(sb,scope,md.getName(),md, suffix,SUMMARY); writeValueLine(sb,scope,suffix + "_count",timer.getCount(),md, null, false); - writeSnapshotQuantiles(sb, scope, md, snapshot, theUnit); + writeSnapshotQuantiles(sb, scope, md, snapshot, theUnit, true); } private void writeHistogramValues(StringBuffer sb, MetricRegistry.Type scope, HistogramImpl histogram, Metadata md) { @@ -195,47 +195,47 @@ private void writeHistogramValues(StringBuffer sb, MetricRegistry.Type scope, Hi String theUnit = unit.equals("none") ? "" : USCORE + unit; writeHelpLine(sb, scope, md.getName(), md, SUMMARY); - writeSnapshotBasics(sb, scope, md, snapshot, theUnit); + writeSnapshotBasics(sb, scope, md, snapshot, theUnit, true); writeTypeLine(sb,scope,md.getName(),md, theUnit,SUMMARY); writeValueLine(sb,scope,theUnit + "_count",histogram.getCount(),md, null, false); - writeSnapshotQuantiles(sb, scope, md, snapshot, theUnit); + writeSnapshotQuantiles(sb, scope, md, snapshot, theUnit, true); } - private void writeSnapshotBasics(StringBuffer sb, MetricRegistry.Type scope, Metadata md, Snapshot snapshot, String unit) { + private void writeSnapshotBasics(StringBuffer sb, MetricRegistry.Type scope, Metadata md, Snapshot snapshot, String unit, boolean performScaling) { - writeTypeAndValue(sb, scope, "_min" + unit, snapshot.getMin(), GAUGE, md); - writeTypeAndValue(sb, scope, "_max" + unit, snapshot.getMax(), GAUGE, md); - writeTypeAndValue(sb, scope, "_mean" + unit, snapshot.getMean(), GAUGE, md); - writeTypeAndValue(sb, scope, "_stddev" + unit, snapshot.getStdDev(), GAUGE, md); + writeTypeAndValue(sb, scope, "_min" + unit, snapshot.getMin(), GAUGE, md, performScaling); + writeTypeAndValue(sb, scope, "_max" + unit, snapshot.getMax(), GAUGE, md, performScaling); + writeTypeAndValue(sb, scope, "_mean" + unit, snapshot.getMean(), GAUGE, md, performScaling); + writeTypeAndValue(sb, scope, "_stddev" + unit, snapshot.getStdDev(), GAUGE, md, performScaling); } - private void writeSnapshotQuantiles(StringBuffer sb, MetricRegistry.Type scope, Metadata md, Snapshot snapshot, String unit) { - writeValueLine(sb, scope, unit, snapshot.getMedian(), md, new Tag(QUANTILE, "0.5")); - writeValueLine(sb, scope, unit, snapshot.get75thPercentile(), md, new Tag(QUANTILE, "0.75")); - writeValueLine(sb, scope, unit, snapshot.get95thPercentile(), md, new Tag(QUANTILE, "0.95")); - writeValueLine(sb, scope, unit, snapshot.get98thPercentile(), md, new Tag(QUANTILE, "0.98")); - writeValueLine(sb, scope, unit, snapshot.get99thPercentile(), md, new Tag(QUANTILE, "0.99")); - writeValueLine(sb, scope, unit, snapshot.get999thPercentile(), md, new Tag(QUANTILE, "0.999")); + private void writeSnapshotQuantiles(StringBuffer sb, MetricRegistry.Type scope, Metadata md, Snapshot snapshot, String unit, boolean performScaling) { + writeValueLine(sb, scope, unit, snapshot.getMedian(), md, new Tag(QUANTILE, "0.5"), performScaling); + writeValueLine(sb, scope, unit, snapshot.get75thPercentile(), md, new Tag(QUANTILE, "0.75"), performScaling); + writeValueLine(sb, scope, unit, snapshot.get95thPercentile(), md, new Tag(QUANTILE, "0.95"), performScaling); + writeValueLine(sb, scope, unit, snapshot.get98thPercentile(), md, new Tag(QUANTILE, "0.98"), performScaling); + writeValueLine(sb, scope, unit, snapshot.get99thPercentile(), md, new Tag(QUANTILE, "0.99"), performScaling); + writeValueLine(sb, scope, unit, snapshot.get999thPercentile(), md, new Tag(QUANTILE, "0.999"), performScaling); } private void writeMeterValues(StringBuffer sb, MetricRegistry.Type scope, Metered metric, Metadata md) { writeHelpLine(sb, scope, md.getName(), md, "_total"); - writeTypeAndValue(sb, scope, "_total", metric.getCount(), COUNTER, md); + writeTypeAndValue(sb, scope, "_total", metric.getCount(), COUNTER, md, false); writeMeterRateValues(sb, scope, metric, md); } private void writeMeterRateValues(StringBuffer sb, MetricRegistry.Type scope, Metered metric, Metadata md) { - writeTypeAndValue(sb, scope, "_rate_per_second", metric.getMeanRate(), GAUGE, md); - writeTypeAndValue(sb, scope, "_one_min_rate_per_second", metric.getOneMinuteRate(), GAUGE, md); - writeTypeAndValue(sb, scope, "_five_min_rate_per_second", metric.getFiveMinuteRate(), GAUGE, md); - writeTypeAndValue(sb, scope, "_fifteen_min_rate_per_second", metric.getFifteenMinuteRate(), GAUGE, md); + writeTypeAndValue(sb, scope, "_rate_per_second", metric.getMeanRate(), GAUGE, md, false); + writeTypeAndValue(sb, scope, "_one_min_rate_per_second", metric.getOneMinuteRate(), GAUGE, md, false); + writeTypeAndValue(sb, scope, "_five_min_rate_per_second", metric.getFiveMinuteRate(), GAUGE, md, false); + writeTypeAndValue(sb, scope, "_fifteen_min_rate_per_second", metric.getFifteenMinuteRate(), GAUGE, md, false); } - private void writeTypeAndValue(StringBuffer sb, MetricRegistry.Type scope, String suffix, double valueRaw, String type, Metadata md) { + private void writeTypeAndValue(StringBuffer sb, MetricRegistry.Type scope, String suffix, double valueRaw, String type, Metadata md, boolean performScaling) { String key = md.getName(); writeTypeLine(sb, scope, key, md, suffix, type); - writeValueLine(sb, scope, suffix, valueRaw, md); + writeValueLine(sb, scope, suffix, valueRaw, md, null, performScaling); } private void writeValueLine(StringBuffer sb, MetricRegistry.Type scope, String suffix, double valueRaw, Metadata md) { @@ -252,7 +252,7 @@ private void writeValueLine(StringBuffer sb, double valueRaw, Metadata md, Tag extraTag, - boolean scaled) { + boolean performScaling) { String name = md.getName(); name = getPrometheusMetricName(name); fillBaseName(sb, scope, name); @@ -270,9 +270,18 @@ private void writeValueLine(StringBuffer sb, } sb.append(SPACE); - Double value = scaled ? PrometheusUnit.scaleToBase(md.getUnit(), valueRaw) : valueRaw; - sb.append(value).append(LF); + Double value; + if(performScaling) { + String scaleFrom = "nanoseconds"; + if(md.getTypeRaw() == MetricType.HISTOGRAM) + // for histograms, internally the data is stored using the metric's unit + scaleFrom = md.getUnit(); + value = PrometheusUnit.scaleToBase(scaleFrom, valueRaw); + } else { + value = valueRaw; + } + sb.append(value).append(LF); } private void addTags(StringBuffer sb, Map tags) { diff --git a/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusUnit.java b/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusUnit.java index 4f5c7b2f..69c61dbd 100644 --- a/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusUnit.java +++ b/implementation/src/main/java/io/smallrye/metrics/exporters/PrometheusUnit.java @@ -36,6 +36,13 @@ private PrometheusUnit() { } + /** + * Determines the basic unit to be used by Prometheus exporter based on the input unit from parameter. + * That is: + * - for memory size units, returns "bytes" + * - for time units, returns "seconds" + * - for any other unit, returns the input unit itself + */ public static String getBaseUnitAsPrometheusString(String unit) { String out; @@ -88,11 +95,18 @@ public static String getBaseUnitAsPrometheusString(String unit) { return out; } - public static Double scaleToBase(String unit, Double value) { + /** + * Scales the value (time or memory size) interpreted using inputUnit to the base unit for Prometheus exporter + * That means: + * - values for memory size units are scaled to bytes + * - values for time units are scaled to seconds + * - values for other units are returned unchanged + */ + public static Double scaleToBase(String inputUnit, Double value) { Double out; - switch (unit) { + switch (inputUnit) { case MetricUnits.BITS: out = value / 8; diff --git a/implementation/src/test/java/io/smallrye/metrics/exporters/ExportersMetricScalingTest.java b/implementation/src/test/java/io/smallrye/metrics/exporters/ExportersMetricScalingTest.java new file mode 100644 index 00000000..65dd9f07 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/metrics/exporters/ExportersMetricScalingTest.java @@ -0,0 +1,286 @@ +package io.smallrye.metrics.exporters; + +import io.smallrye.metrics.MetricRegistries; +import org.eclipse.microprofile.metrics.Counter; +import org.eclipse.microprofile.metrics.Gauge; +import org.eclipse.microprofile.metrics.Histogram; +import org.eclipse.microprofile.metrics.Metadata; +import org.eclipse.microprofile.metrics.Meter; +import org.eclipse.microprofile.metrics.MetricFilter; +import org.eclipse.microprofile.metrics.MetricRegistry; +import org.eclipse.microprofile.metrics.MetricType; +import org.eclipse.microprofile.metrics.MetricUnits; +import org.eclipse.microprofile.metrics.Timer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import javax.json.Json; +import javax.json.JsonObject; +import java.io.StringReader; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; + +public class ExportersMetricScalingTest { + + @After + public void cleanup() { + MetricRegistries.get(MetricRegistry.Type.APPLICATION).removeMatching(MetricFilter.ALL); + } + + /** + * Given a Timer with unit=MINUTES, + * check that the statistics from PrometheusExporter will be correctly converted to SECONDS. + */ + @Test + public void timer_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("timer1", MetricType.TIMER, MetricUnits.MINUTES); + Timer metric = registry.timer(metadata); + metric.update(1, TimeUnit.HOURS); + metric.update(2, TimeUnit.HOURS); + metric.update(3, TimeUnit.HOURS); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "timer1").toString(); + + Assert.assertThat(exported, containsString("application:timer1_seconds{quantile=\"0.5\"} 7200.0")); + Assert.assertThat(exported, containsString("application:timer1_mean_seconds 7200.0")); + Assert.assertThat(exported, containsString("application:timer1_min_seconds 3600.0")); + Assert.assertThat(exported, containsString("application:timer1_max_seconds 10800.0")); + } + + /** + * Given a Timer with unit=MINUTES, + * check that the statistics from JsonExporter will be presented in MINUTES. + */ + @Test + public void timer_json() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("timer1", MetricType.TIMER, MetricUnits.MINUTES); + Timer metric = registry.timer(metadata); + metric.update(1, TimeUnit.HOURS); + metric.update(2, TimeUnit.HOURS); + metric.update(3, TimeUnit.HOURS); + + JsonExporter exporter = new JsonExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "timer1").toString(); + + JsonObject json = Json.createReader(new StringReader(exported)).read().asJsonObject().getJsonObject("timer1"); + assertEquals(120.0, json.getJsonNumber("p50").doubleValue(), 0.001); + assertEquals(120.0, json.getJsonNumber("mean").doubleValue(), 0.001); + assertEquals(60.0, json.getJsonNumber("min").doubleValue(), 0.001); + assertEquals(180.0, json.getJsonNumber("max").doubleValue(), 0.001); + } + + /** + * Given a Histogram with unit=MINUTES, + * check that the statistics from PrometheusExporter will be presented in SECONDS. + */ + @Test + public void histogram_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("histogram1", MetricType.HISTOGRAM, MetricUnits.MINUTES); + Histogram metric = registry.histogram(metadata); + metric.update(30); + metric.update(40); + metric.update(50); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "histogram1").toString(); + + Assert.assertThat(exported, containsString("application:histogram1_min_seconds 1800.0")); + Assert.assertThat(exported, containsString("application:histogram1_max_seconds 3000.0")); + Assert.assertThat(exported, containsString("application:histogram1_mean_seconds 2400.0")); + Assert.assertThat(exported, containsString("application:histogram1_seconds{quantile=\"0.5\"} 2400.0")); + } + + /** + * Given a Histogram with unit=dollars (custom unit), + * check that the statistics from PrometheusExporter will be presented in dollars. + */ + @Test + public void histogram_customunit_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("histogram1", MetricType.HISTOGRAM, "dollars"); + Histogram metric = registry.histogram(metadata); + metric.update(30); + metric.update(40); + metric.update(50); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "histogram1").toString(); + + Assert.assertThat(exported, containsString("application:histogram1_min_dollars 30.0")); + Assert.assertThat(exported, containsString("application:histogram1_max_dollars 50.0")); + Assert.assertThat(exported, containsString("application:histogram1_mean_dollars 40.0")); + Assert.assertThat(exported, containsString("application:histogram1_dollars{quantile=\"0.5\"} 40.0")); + } + + /** + * Given a Histogram with unit=MINUTES, + * check that the statistics from JsonExporter will be presented in MINUTES. + */ + @Test + public void histogram_json() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("timer1", MetricType.TIMER, MetricUnits.MINUTES); + Timer metric = registry.timer(metadata); + metric.update(1, TimeUnit.HOURS); + metric.update(2, TimeUnit.HOURS); + metric.update(3, TimeUnit.HOURS); + + JsonExporter exporter = new JsonExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "timer1").toString(); + + + JsonObject json = Json.createReader(new StringReader(exported)).read().asJsonObject().getJsonObject("timer1"); + assertEquals(120.0, json.getJsonNumber("p50").doubleValue(), 0.001); + assertEquals(120.0, json.getJsonNumber("mean").doubleValue(), 0.001); + assertEquals(60.0, json.getJsonNumber("min").doubleValue(), 0.001); + assertEquals(180.0, json.getJsonNumber("max").doubleValue(), 0.001); + } + + /** + * Given a Counter, + * check that the statistics from PrometheusExporter will not be scaled in any way. + */ + @Test + public void counter_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("counter1", MetricType.COUNTER); + Counter metric = registry.counter(metadata); + metric.inc(30); + metric.inc(40); + metric.inc(50); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "counter1").toString(); + + Assert.assertThat(exported, containsString("application:counter1 120.0")); + } + + /** + * Given a Counter, + * check that the statistics from JsonExporter will not be scaled in any way. + */ + @Test + public void counter_json() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("counter1", MetricType.COUNTER); + Counter metric = registry.counter(metadata); + metric.inc(10); + metric.inc(20); + metric.inc(30); + + JsonExporter exporter = new JsonExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "counter1").toString(); + + JsonObject json = Json.createReader(new StringReader(exported)).read().asJsonObject(); + assertEquals(60, json.getInt("counter1")); + } + + /** + * Given a Meter, + * check that the statistics from PrometheusExporter will be presented as per_second. + */ + @Test + public void meter_prometheus() throws InterruptedException { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("meter1", MetricType.METERED); + Meter metric = registry.meter(metadata); + metric.mark(10); + TimeUnit.SECONDS.sleep(1); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "meter1").toString(); + + Assert.assertThat(exported, containsString("application:meter1_total 10.0")); + double ratePerSecond = Double.parseDouble(Arrays.stream(exported.split("\\n")) + .filter(line -> line.contains("application:meter1_rate_per_second")) + .filter(line -> !line.contains("TYPE") && !line.contains("HELP")) + .findFirst() + .get() + .split(" ")[1]); + Assert.assertTrue("Rate per second should be between 1 and 10 but is " + ratePerSecond, + ratePerSecond > 1 && ratePerSecond < 10); + } + + /** + * Given a Meter, + * check that the statistics from JsonExporter will be presented as per_second. + */ + @Test + public void meter_json() throws InterruptedException { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("meter1", MetricType.METERED); + Meter metric = registry.meter(metadata); + metric.mark(10); + TimeUnit.SECONDS.sleep(1); + + JsonExporter exporter = new JsonExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "meter1").toString(); + + JsonObject json = Json.createReader(new StringReader(exported)).read().asJsonObject().getJsonObject("meter1"); + assertEquals(10, json.getInt("count")); + double meanRate = json.getJsonNumber("meanRate").doubleValue(); + Assert.assertTrue("meanRate should be between 1 and 10 but is " + meanRate, + meanRate > 1 && meanRate < 10); + } + + /** + * Given a Gauge with unit=MINUTES, + * check that the statistics from PrometheusExporter will be presented in SECONDS. + */ + @Test + public void gauge_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("gauge1", MetricType.GAUGE, MetricUnits.MINUTES); + Gauge gaugeInstance = () -> 3L; + registry.register("gauge1", gaugeInstance, metadata); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "gauge1").toString(); + + Assert.assertThat(exported, containsString("application:gauge1_seconds 180.0")); + } + + /** + * Given a Gauge with unit=dollars (custom unit), + * check that the statistics from PrometheusExporter will be presented in dollars. + */ + @Test + public void gauge_customUnit_prometheus() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("gauge1", MetricType.GAUGE, "dollars"); + Gauge gaugeInstance = () -> 3L; + registry.register("gauge1", gaugeInstance, metadata); + + PrometheusExporter exporter = new PrometheusExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "gauge1").toString(); + + Assert.assertThat(exported, containsString("application:gauge1_dollars 3.0")); + } + + /** + * Given a Gauge with unit=MINUTES, + * check that the statistics from PrometheusExporter will be presented in MINUTES. + */ + @Test + public void gauge_json() { + MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION); + Metadata metadata = new Metadata("gauge1", MetricType.GAUGE, MetricUnits.MINUTES); + Gauge gaugeInstance = () -> 3L; + registry.register("gauge1", gaugeInstance, metadata); + + JsonExporter exporter = new JsonExporter(); + String exported = exporter.exportOneMetric(MetricRegistry.Type.APPLICATION, "gauge1").toString(); + + JsonObject json = Json.createReader(new StringReader(exported)).read().asJsonObject(); + assertEquals(3, json.getInt("gauge1")); + } + +}