diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java index 3863b5b74..feabd89b8 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java @@ -69,9 +69,9 @@ public ExpositionFormatWriter findWriter(@Nullable String acceptHeader) { if ("2.0.0".equals(version)) { return openMetrics2TextFormatWriter; } - // version=1.0.0 or no version: fall through to OM1 + // version=1.0.0 or no version: fall through to OM1. } else { - // contentNegotiation=false: OM2 handles all OpenMetrics requests + // contentNegotiation=false: OM2 handles all OpenMetrics requests. return openMetrics2TextFormatWriter; } } diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java index 3ccdc80d8..4406ef7c4 100644 --- a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriter.java @@ -15,6 +15,7 @@ import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.DataPointSnapshot; import io.prometheus.metrics.model.snapshots.Exemplar; +import io.prometheus.metrics.model.snapshots.Exemplars; import io.prometheus.metrics.model.snapshots.GaugeSnapshot; import io.prometheus.metrics.model.snapshots.HistogramSnapshot; import io.prometheus.metrics.model.snapshots.InfoSnapshot; @@ -22,6 +23,7 @@ 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.NativeHistogramBuckets; import io.prometheus.metrics.model.snapshots.PrometheusNaming; import io.prometheus.metrics.model.snapshots.Quantile; import io.prometheus.metrics.model.snapshots.SnapshotEscaper; @@ -126,8 +128,8 @@ public boolean accepts(@Nullable String acceptHeader) { @Override public String getContentType() { - // When contentNegotiation=false (default), masquerade as OM1 for compatibility - // When contentNegotiation=true, use proper OM2 version + // When contentNegotiation=false (default), masquerade as OM1 for compatibility. + // When contentNegotiation=true, use proper OM2 version. if (openMetrics2Properties.getContentNegotiation()) { return CONTENT_TYPE; } else { @@ -177,8 +179,16 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) { writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme); writeDouble(writer, data.getValue()); - writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme); - writeCreated(writer, counterName, data, scheme); + if (data.hasScrapeTimestamp()) { + writer.write(' '); + writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); + } + if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { + writer.write(" st@"); + writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); + } + writeExemplar(writer, data.getExemplar(), scheme); + writer.write('\n'); } } @@ -200,8 +210,9 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme) throws IOException { - if (!openMetrics2Properties.getCompositeValues() - && !openMetrics2Properties.getExemplarCompliance()) { + boolean compositeHistogram = + openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms(); + if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) { om1Writer.writeHistogram(writer, snapshot, scheme); return; } @@ -210,12 +221,20 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS if (snapshot.isGaugeHistogram()) { writeMetadataWithName(writer, name, "gaugehistogram", metadata); for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { - writeCompositeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme); + if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) { + writeNativeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme, false); + } else { + writeCompositeHistogramDataPoint(writer, name, "gcount", "gsum", data, scheme, false); + } } } else { writeMetadataWithName(writer, name, "histogram", metadata); for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) { - writeCompositeHistogramDataPoint(writer, name, "count", "sum", data, scheme); + if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) { + writeNativeHistogramDataPoint(writer, name, "count", "sum", data, scheme, true); + } else { + writeCompositeHistogramDataPoint(writer, name, "count", "sum", data, scheme, true); + } } } } @@ -226,7 +245,8 @@ private void writeCompositeHistogramDataPoint( String countKey, String sumKey, HistogramSnapshot.HistogramDataPointSnapshot data, - EscapingScheme scheme) + EscapingScheme scheme, + boolean includeStartTimestamp) throws IOException { writeNameAndLabels(writer, name, null, data.getLabels(), scheme); writer.write('{'); @@ -237,28 +257,59 @@ private void writeCompositeHistogramDataPoint( writer.write(sumKey); writer.write(':'); writeDouble(writer, data.getSum()); - writer.write(",bucket:["); - ClassicHistogramBuckets buckets = getClassicBuckets(data); - long cumulativeCount = 0; - for (int i = 0; i < buckets.size(); i++) { - if (i > 0) { - writer.write(','); - } - cumulativeCount += buckets.getCount(i); - writeDouble(writer, buckets.getUpperBound(i)); - writer.write(':'); - writeLong(writer, cumulativeCount); + writeClassicBucketsField(writer, data); + writer.write('}'); + if (data.hasScrapeTimestamp()) { + writer.write(' '); + writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); + } + if (includeStartTimestamp && data.hasCreatedTimestamp()) { + writer.write(" st@"); + writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); } - writer.write("]}"); + writeExemplars(writer, data.getExemplars(), scheme); + writer.write('\n'); + } + + private void writeNativeHistogramDataPoint( + Writer writer, + String name, + String countKey, + String sumKey, + HistogramSnapshot.HistogramDataPointSnapshot data, + EscapingScheme scheme, + boolean includeStartTimestamp) + throws IOException { + writeNameAndLabels(writer, name, null, data.getLabels(), scheme); + writer.write('{'); + writer.write(countKey); + writer.write(':'); + writeLong(writer, data.getCount()); + writer.write(','); + writer.write(sumKey); + writer.write(':'); + writeDouble(writer, data.getSum()); + writer.write(",schema:"); + writer.write(Integer.toString(data.getNativeSchema())); + writer.write(",zero_threshold:"); + writeDouble(writer, data.getNativeZeroThreshold()); + writer.write(",zero_count:"); + writeLong(writer, data.getNativeZeroCount()); + writeNativeBucketFields(writer, "negative", data.getNativeBucketsForNegativeValues()); + writeNativeBucketFields(writer, "positive", data.getNativeBucketsForPositiveValues()); + if (data.hasClassicHistogramData()) { + writeClassicBucketsField(writer, data); + } + writer.write('}'); if (data.hasScrapeTimestamp()) { writer.write(' '); writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); } - if (data.hasCreatedTimestamp()) { + if (includeStartTimestamp && data.hasCreatedTimestamp()) { writer.write(" st@"); writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); } - writeExemplar(writer, data.getExemplars().getLatest(), scheme); + writeExemplars(writer, data.getExemplars(), scheme); writer.write('\n'); } @@ -272,6 +323,75 @@ private ClassicHistogramBuckets getClassicBuckets( } } + private void writeClassicBucketsField( + Writer writer, HistogramSnapshot.HistogramDataPointSnapshot data) throws IOException { + writer.write(",bucket:["); + ClassicHistogramBuckets buckets = getClassicBuckets(data); + long cumulativeCount = 0; + for (int i = 0; i < buckets.size(); i++) { + if (i > 0) { + writer.write(','); + } + cumulativeCount += buckets.getCount(i); + writeDouble(writer, buckets.getUpperBound(i)); + writer.write(':'); + writeLong(writer, cumulativeCount); + } + writer.write(']'); + } + + private void writeNativeBucketFields(Writer writer, String prefix, NativeHistogramBuckets buckets) + throws IOException { + if (buckets.size() == 0) { + return; + } + writer.write(','); + writer.write(prefix); + writer.write("_spans:["); + writeNativeBucketSpans(writer, buckets); + writer.write("],"); + writer.write(prefix); + writer.write("_buckets:["); + for (int i = 0; i < buckets.size(); i++) { + if (i > 0) { + writer.write(','); + } + writeLong(writer, buckets.getCount(i)); + } + writer.write(']'); + } + + private void writeNativeBucketSpans(Writer writer, NativeHistogramBuckets buckets) + throws IOException { + int spanOffset = buckets.getBucketIndex(0); + int spanLength = 1; + int previousIndex = buckets.getBucketIndex(0); + boolean firstSpan = true; + for (int i = 1; i < buckets.size(); i++) { + int bucketIndex = buckets.getBucketIndex(i); + if (bucketIndex == previousIndex + 1) { + spanLength++; + } else { + firstSpan = writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan); + spanOffset = bucketIndex - previousIndex - 1; + spanLength = 1; + } + previousIndex = bucketIndex; + } + writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan); + } + + private boolean writeNativeBucketSpan(Writer writer, int offset, int length, boolean firstSpan) + throws IOException { + if (!firstSpan) { + writer.write(','); + } + writer.write(Integer.toString(offset)); + writer.write(':'); + writer.write(Integer.toString(length)); + return false; + } + private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme) throws IOException { if (!openMetrics2Properties.getCompositeValues() @@ -316,22 +436,20 @@ private void writeCompositeSummaryDataPoint( writeDouble(writer, data.getSum()); first = false; } - if (data.getQuantiles().size() > 0) { - if (!first) { + if (!first) { + writer.write(','); + } + writer.write("quantile:["); + for (int i = 0; i < data.getQuantiles().size(); i++) { + if (i > 0) { writer.write(','); } - writer.write("quantile:["); - for (int i = 0; i < data.getQuantiles().size(); i++) { - if (i > 0) { - writer.write(','); - } - Quantile q = data.getQuantiles().get(i); - writeDouble(writer, q.getQuantile()); - writer.write(':'); - writeDouble(writer, q.getValue()); - } - writer.write(']'); + Quantile q = data.getQuantiles().get(i); + writeDouble(writer, q.getQuantile()); + writer.write(':'); + writeDouble(writer, q.getValue()); } + writer.write(']'); writer.write('}'); if (data.hasScrapeTimestamp()) { writer.write(' '); @@ -341,7 +459,7 @@ private void writeCompositeSummaryDataPoint( writer.write(" st@"); writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); } - writeExemplar(writer, data.getExemplars().getLatest(), scheme); + writeExemplars(writer, data.getExemplars(), scheme); writer.write('\n'); } @@ -411,20 +529,6 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem } } - private void writeCreated( - Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme) - throws IOException { - if (createdTimestampsEnabled && data.hasCreatedTimestamp()) { - writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme); - writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis()); - if (data.hasScrapeTimestamp()) { - writer.write(' '); - writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis()); - } - writer.write('\n'); - } - } - private void writeNameAndLabels( Writer writer, String name, @@ -496,6 +600,13 @@ private void writeExemplar(Writer writer, @Nullable Exemplar exemplar, EscapingS } } + private void writeExemplars(Writer writer, Exemplars exemplars, EscapingScheme scheme) + throws IOException { + for (Exemplar exemplar : exemplars) { + writeExemplar(writer, exemplar, scheme); + } + } + private void writeMetadataWithName( Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException { writer.write("# TYPE "); diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index e0782251e..454b3ef7b 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -160,6 +160,47 @@ void testOM2EnabledWithFeatureFlags() { assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); } + @Test + void testOM2ContentNegotiationWithNativeHistogramOutput() throws IOException { + PrometheusProperties props = + PrometheusProperties.builder() + .openMetrics2Properties( + OpenMetrics2Properties.builder() + .enabled(true) + .contentNegotiation(true) + .nativeHistograms(true) + .build()) + .build(); + ExpositionFormats formats = ExpositionFormats.init(props); + ExpositionFormatWriter writer = + formats.findWriter("application/openmetrics-text; version=2.0.0"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write( + out, + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("latency_seconds") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(1.5) + .nativeSchema(5) + .nativeZeroCount(1) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(2, 3).build()) + .build()) + .build()), + EscapingScheme.ALLOW_UTF8); + + assertThat(writer).isInstanceOf(OpenMetrics2TextFormatWriter.class); + assertThat(out.toString(UTF_8)) + .isEqualTo( + "# TYPE latency_seconds histogram\n" + + "latency_seconds {count:4,sum:1.5,schema:5,zero_threshold:0.0,zero_count:1," + + "positive_spans:[2:1],positive_buckets:[3]}\n" + + "# EOF\n"); + } + @Test void testProtobufWriterTakesPrecedence() { PrometheusProperties props = diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java index 483bf1339..64da453bb 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/OpenMetrics2TextFormatWriterTest.java @@ -13,6 +13,7 @@ import io.prometheus.metrics.model.snapshots.InfoSnapshot; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; import io.prometheus.metrics.model.snapshots.Quantiles; import io.prometheus.metrics.model.snapshots.StateSetSnapshot; import io.prometheus.metrics.model.snapshots.SummarySnapshot; @@ -32,7 +33,6 @@ void testContentTypeWithContentNegotiationDisabled() { OpenMetrics2Properties.builder().contentNegotiation(false).build()) .build(); - // Should masquerade as OM1 when contentNegotiation is disabled assertThat(writer.getContentType()) .isEqualTo("application/openmetrics-text; version=1.0.0; charset=utf-8"); } @@ -53,7 +53,6 @@ void testContentTypeWithContentNegotiationEnabled() { void testContentTypeDefault() { OpenMetrics2TextFormatWriter writer = OpenMetrics2TextFormatWriter.create(); - // Default should masquerade as OM1 for compatibility assertThat(writer.getContentType()) .isEqualTo("application/openmetrics-text; version=1.0.0; charset=utf-8"); } @@ -307,13 +306,12 @@ void testCounterWithCreatedTimestamps() throws IOException { String om2Output = write(snapshots, om2Writer); - // OM2: no _total, _created uses the counter name directly + // OM2: no _total, start timestamp uses st@ inline. assertThat(om2Output) .isEqualTo( "# TYPE my_counter counter\n" + "# HELP my_counter Test counter\n" - + "my_counter 42.0\n" - + "my_counter_created 1672850385.800\n" + + "my_counter 42.0 st@1672850385.800\n" + "# EOF\n"); } @@ -392,8 +390,10 @@ void testCompositeHistogramWithLabelsTimestampAndCreated() throws IOException { @Test void testCompositeHistogramWithExemplar() throws IOException { - Exemplar exemplar = + Exemplar exemplar1 = Exemplar.builder().value(0.67).traceId("shaZ8oxi").timestampMillis(1520879607789L).build(); + Exemplar exemplar2 = + Exemplar.builder().value(1.2).traceId("ookahn0M").timestampMillis(1520879608589L).build(); MetricSnapshots snapshots = MetricSnapshots.of( @@ -407,7 +407,7 @@ void testCompositeHistogramWithExemplar() throws IOException { .bucket(1.0, 1) .bucket(Double.POSITIVE_INFINITY, 0) .build()) - .exemplars(Exemplars.of(exemplar)) + .exemplars(Exemplars.of(exemplar1, exemplar2)) .build()) .build()); @@ -417,7 +417,8 @@ void testCompositeHistogramWithExemplar() throws IOException { .isEqualTo( "# TYPE foo histogram\n" + "foo {count:1,sum:1.5,bucket:[1.0:1,+Inf:1]}" - + " # {trace_id=\"shaZ8oxi\"} 0.67 1520879607.789\n" + + " # {trace_id=\"shaZ8oxi\"} 0.67 1520879607.789" + + " # {trace_id=\"ookahn0M\"} 1.2 1520879608.589\n" + "# EOF\n"); } @@ -450,6 +451,245 @@ void testCompositeGaugeHistogram() throws IOException { + "# EOF\n"); } + @Test + void testNativeHistogram() throws IOException { + Exemplar exemplar1 = + Exemplar.builder().value(0.67).traceId("shaZ8oxi").timestampMillis(1520879607789L).build(); + Exemplar exemplar2 = + Exemplar.builder().value(1.2).traceId("ookahn0M").timestampMillis(1520879608589L).build(); + + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("foo") + .help("Native histogram") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(324789.3) + .nativeSchema(0) + .nativeZeroThreshold(0.0001) + .nativeZeroCount(0) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(0, 5).bucket(1, 12).build()) + .scrapeTimestampMillis(1520879609000L) + .createdTimestampMillis(1520430000000L) + .exemplars(Exemplars.of(exemplar1, exemplar2)) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE foo histogram\n" + + "# HELP foo Native histogram\n" + + "foo {count:17,sum:324789.3,schema:0,zero_threshold:1.0E-4,zero_count:0," + + "positive_spans:[0:2],positive_buckets:[5,12]} 1520879609.000" + + " st@1520430000.000 # {trace_id=\"shaZ8oxi\"} 0.67 1520879607.789" + + " # {trace_id=\"ookahn0M\"} 1.2 1520879608.589\n" + + "# EOF\n"); + } + + @Test + void testNativeHistogramWithClassicBuckets() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("http_request_duration_seconds") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(4.0) + .classicHistogramBuckets( + ClassicHistogramBuckets.builder() + .bucket(0.5, 1) + .bucket(1.0, 7) + .bucket(Double.POSITIVE_INFINITY, 14) + .build()) + .nativeSchema(3) + .nativeZeroThreshold(0.0) + .nativeZeroCount(1) + .nativeBucketsForNegativeValues( + NativeHistogramBuckets.builder().bucket(1, 2).bucket(2, 4).build()) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder() + .bucket(-1, 5) + .bucket(0, 7) + .bucket(4, 3) + .build()) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE http_request_duration_seconds histogram\n" + + "http_request_duration_seconds" + + " {count:22,sum:4.0,schema:3,zero_threshold:0.0,zero_count:1," + + "negative_spans:[1:2],negative_buckets:[2,4]," + + "positive_spans:[-1:2,3:1],positive_buckets:[5,7,3]," + + "bucket:[0.5:1,1.0:8,+Inf:22]}\n" + + "# EOF\n"); + } + + @Test + void testNativeGaugeHistogram() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("queue_size") + .gaugeHistogram(true) + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(4.5) + .nativeSchema(2) + .nativeZeroThreshold(0.0) + .nativeZeroCount(1) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(0, 1).bucket(1, 1).build()) + .createdTimestampMillis(1520430000000L) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE queue_size gaugehistogram\n" + + "queue_size {gcount:3,gsum:4.5,schema:2,zero_threshold:0.0,zero_count:1," + + "positive_spans:[0:2],positive_buckets:[1,1]}\n" + + "# EOF\n"); + } + + @Test + void testNativeHistogramExemplarComplianceSkipsExemplarWithoutTimestamp() throws IOException { + Exemplar exemplarWithoutTs = Exemplar.builder().value(2.0).traceId("bbb").build(); + OpenMetrics2TextFormatWriter complianceWriter = + OpenMetrics2TextFormatWriter.builder() + .setOpenMetrics2Properties( + OpenMetrics2Properties.builder() + .nativeHistograms(true) + .exemplarCompliance(true) + .build()) + .build(); + + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("requests") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(2.0) + .nativeSchema(5) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(0, 1).build()) + .exemplars(Exemplars.of(exemplarWithoutTs)) + .build()) + .build()); + + String output = write(snapshots, complianceWriter); + assertThat(output) + .isEqualTo( + "# TYPE requests histogram\n" + + "requests {count:1,sum:2.0,schema:5,zero_threshold:0.0,zero_count:0," + + "positive_spans:[0:1],positive_buckets:[1]}\n" + + "# EOF\n"); + } + + @Test + void testNativeHistogramMinimalOm2() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("latency_seconds") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(0.0) + .nativeSchema(5) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE latency_seconds histogram\n" + + "latency_seconds {count:0,sum:0.0,schema:5,zero_threshold:0.0,zero_count:0}\n" + + "# EOF\n"); + } + + @Test + void testNativeGaugeHistogramWithNegativeAndPositiveSpans() throws IOException { + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("temperature_delta") + .gaugeHistogram(true) + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(0.5) + .nativeSchema(1) + .nativeZeroThreshold(0.25) + .nativeZeroCount(3) + .nativeBucketsForNegativeValues( + NativeHistogramBuckets.builder().bucket(0, 2).bucket(2, 4).build()) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(1, 1).bucket(2, 5).build()) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE temperature_delta gaugehistogram\n" + + "temperature_delta {gcount:15,gsum:0.5,schema:1,zero_threshold:0.25," + + "zero_count:3,negative_spans:[0:1,1:1],negative_buckets:[2,4]," + + "positive_spans:[1:2],positive_buckets:[1,5]}\n" + + "# EOF\n"); + } + + @Test + void testNativeHistogramWithDots() throws IOException { + Exemplar exemplar = + Exemplar.builder() + .labels(Labels.of("some.exemplar.key", "some value")) + .value(3.0) + .timestampMillis(1690298864383L) + .build(); + + MetricSnapshots snapshots = + MetricSnapshots.of( + HistogramSnapshot.builder() + .name("my.request.duration.seconds") + .help("Request duration in seconds") + .unit(Unit.SECONDS) + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .labels(Labels.builder().label("http.path", "/hello").build()) + .sum(3.2) + .nativeSchema(5) + .nativeZeroCount(1) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(2, 3).build()) + .exemplars(Exemplars.of(exemplar)) + .build()) + .build()); + + String output = writeWithNativeHistograms(snapshots); + + assertThat(output) + .isEqualTo( + "# TYPE \"my.request.duration.seconds\" histogram\n" + + "# UNIT \"my.request.duration.seconds\" seconds\n" + + "# HELP \"my.request.duration.seconds\" Request duration in seconds\n" + + "{\"my.request.duration.seconds\",\"http.path\"=\"/hello\"}" + + " {count:4,sum:3.2,schema:5,zero_threshold:0.0,zero_count:1," + + "positive_spans:[2:1],positive_buckets:[3]}" + + " # {\"some.exemplar.key\"=\"some value\"} 3.0 1690298864.383\n" + + "# EOF\n"); + } + @Test void testCompositeSummary() throws IOException { MetricSnapshots snapshots = @@ -479,8 +719,10 @@ void testCompositeSummary() throws IOException { @Test void testCompositeSummaryWithCreatedAndExemplar() throws IOException { - Exemplar exemplar = + Exemplar exemplar1 = Exemplar.builder().value(0.5).traceId("abc123").timestampMillis(1520879607000L).build(); + Exemplar exemplar2 = + Exemplar.builder().value(1.5).traceId("def456").timestampMillis(1520879608000L).build(); MetricSnapshots snapshots = MetricSnapshots.of( @@ -491,7 +733,7 @@ void testCompositeSummaryWithCreatedAndExemplar() throws IOException { .count(10) .sum(100.0) .createdTimestampMillis(1520430000000L) - .exemplars(Exemplars.of(exemplar)) + .exemplars(Exemplars.of(exemplar1, exemplar2)) .build()) .build()); @@ -500,8 +742,9 @@ void testCompositeSummaryWithCreatedAndExemplar() throws IOException { assertThat(output) .isEqualTo( "# TYPE rpc_duration_seconds summary\n" - + "rpc_duration_seconds {count:10,sum:100.0} st@1520430000.000" - + " # {trace_id=\"abc123\"} 0.5 1520879607.000\n" + + "rpc_duration_seconds {count:10,sum:100.0,quantile:[]} st@1520430000.000" + + " # {trace_id=\"abc123\"} 0.5 1520879607.000" + + " # {trace_id=\"def456\"} 1.5 1520879608.000\n" + "# EOF\n"); } @@ -610,6 +853,15 @@ private String writeWithCompositeValues(MetricSnapshots snapshots) throws IOExce return write(snapshots, writer); } + private String writeWithNativeHistograms(MetricSnapshots snapshots) throws IOException { + OpenMetrics2TextFormatWriter writer = + OpenMetrics2TextFormatWriter.builder() + .setOpenMetrics2Properties( + OpenMetrics2Properties.builder().nativeHistograms(true).build()) + .build(); + return write(snapshots, writer); + } + private String writeWithOM1(MetricSnapshots snapshots) throws IOException { OpenMetricsTextFormatWriter writer = OpenMetricsTextFormatWriter.create(); return write(snapshots, writer);