From 5db7aa3f7561c3a0d90d42dc201ae03351015fc4 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Thu, 25 Jun 2026 00:50:15 +0530 Subject: [PATCH 1/6] fix: avoid protobuf debug reflection in native images Signed-off-by: Arnab Nandy Signed-off-by: Gregor Zeitlinger --- .../PrometheusProtobufDebugFormat.java | 310 ++++++++++++++++++ .../PrometheusProtobufWriterImpl.java | 6 +- .../ProtobufExpositionFormatsTest.java | 45 +++ 3 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java new file mode 100644 index 000000000..99b094efa --- /dev/null +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java @@ -0,0 +1,310 @@ +package io.prometheus.metrics.expositionformats.internal; + +import com.google.protobuf.ByteString; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import io.prometheus.metrics.expositionformats.generated.Metrics; + +@SuppressWarnings("NonCanonicalType") +class PrometheusProtobufDebugFormat { + + private static final String INDENT = " "; + + static String toDebugString(Metrics.MetricFamily metricFamily) { + StringBuilder result = new StringBuilder(); + appendMetricFamily(result, "", metricFamily); + return result.toString(); + } + + private static void appendMetricFamily( + StringBuilder result, String indent, Metrics.MetricFamily metricFamily) { + appendString(result, indent, "name", metricFamily.hasName(), metricFamily.getName()); + appendString(result, indent, "help", metricFamily.hasHelp(), metricFamily.getHelp()); + if (metricFamily.hasType()) { + appendScalar(result, indent, "type", metricFamily.getType().name()); + } + for (Metrics.Metric metric : metricFamily.getMetricList()) { + appendMessage(result, indent, "metric", () -> appendMetric(result, indent + INDENT, metric)); + } + appendString(result, indent, "unit", metricFamily.hasUnit(), metricFamily.getUnit()); + } + + private static void appendMetric(StringBuilder result, String indent, Metrics.Metric metric) { + for (Metrics.LabelPair label : metric.getLabelList()) { + appendMessage(result, indent, "label", () -> appendLabel(result, indent + INDENT, label)); + } + if (metric.hasGauge()) { + appendMessage( + result, indent, "gauge", () -> appendGauge(result, indent + INDENT, metric.getGauge())); + } + if (metric.hasCounter()) { + appendMessage( + result, + indent, + "counter", + () -> appendCounter(result, indent + INDENT, metric.getCounter())); + } + if (metric.hasSummary()) { + appendMessage( + result, + indent, + "summary", + () -> appendSummary(result, indent + INDENT, metric.getSummary())); + } + if (metric.hasUntyped()) { + appendMessage( + result, + indent, + "untyped", + () -> appendUntyped(result, indent + INDENT, metric.getUntyped())); + } + if (metric.hasHistogram()) { + appendMessage( + result, + indent, + "histogram", + () -> appendHistogram(result, indent + INDENT, metric.getHistogram())); + } + appendScalar(result, indent, "timestamp_ms", metric.hasTimestampMs(), metric.getTimestampMs()); + } + + private static void appendLabel(StringBuilder result, String indent, Metrics.LabelPair label) { + appendString(result, indent, "name", label.hasName(), label.getName()); + appendString(result, indent, "value", label.hasValue(), label.getValue()); + } + + private static void appendGauge(StringBuilder result, String indent, Metrics.Gauge gauge) { + appendScalar(result, indent, "value", gauge.hasValue(), gauge.getValue()); + } + + private static void appendCounter(StringBuilder result, String indent, Metrics.Counter counter) { + appendScalar(result, indent, "value", counter.hasValue(), counter.getValue()); + if (counter.hasExemplar()) { + appendMessage( + result, + indent, + "exemplar", + () -> appendExemplar(result, indent + INDENT, counter.getExemplar())); + } + appendTimestamp( + result, + indent, + "created_timestamp", + counter.hasCreatedTimestamp(), + counter.getCreatedTimestamp()); + } + + private static void appendSummary(StringBuilder result, String indent, Metrics.Summary summary) { + appendScalar( + result, indent, "sample_count", summary.hasSampleCount(), summary.getSampleCount()); + appendScalar(result, indent, "sample_sum", summary.hasSampleSum(), summary.getSampleSum()); + for (Metrics.Quantile quantile : summary.getQuantileList()) { + appendMessage( + result, indent, "quantile", () -> appendQuantile(result, indent + INDENT, quantile)); + } + appendTimestamp( + result, + indent, + "created_timestamp", + summary.hasCreatedTimestamp(), + summary.getCreatedTimestamp()); + } + + private static void appendQuantile( + StringBuilder result, String indent, Metrics.Quantile quantile) { + appendScalar(result, indent, "quantile", quantile.hasQuantile(), quantile.getQuantile()); + appendScalar(result, indent, "value", quantile.hasValue(), quantile.getValue()); + } + + private static void appendUntyped(StringBuilder result, String indent, Metrics.Untyped untyped) { + appendScalar(result, indent, "value", untyped.hasValue(), untyped.getValue()); + } + + private static void appendHistogram( + StringBuilder result, String indent, Metrics.Histogram histogram) { + appendScalar( + result, indent, "sample_count", histogram.hasSampleCount(), histogram.getSampleCount()); + appendScalar( + result, + indent, + "sample_count_float", + histogram.hasSampleCountFloat(), + histogram.getSampleCountFloat()); + appendScalar(result, indent, "sample_sum", histogram.hasSampleSum(), histogram.getSampleSum()); + for (Metrics.Bucket bucket : histogram.getBucketList()) { + appendMessage(result, indent, "bucket", () -> appendBucket(result, indent + INDENT, bucket)); + } + appendTimestamp( + result, + indent, + "created_timestamp", + histogram.hasCreatedTimestamp(), + histogram.getCreatedTimestamp()); + appendScalar(result, indent, "schema", histogram.hasSchema(), histogram.getSchema()); + appendScalar( + result, + indent, + "zero_threshold", + histogram.hasZeroThreshold(), + histogram.getZeroThreshold()); + appendScalar(result, indent, "zero_count", histogram.hasZeroCount(), histogram.getZeroCount()); + appendScalar( + result, + indent, + "zero_count_float", + histogram.hasZeroCountFloat(), + histogram.getZeroCountFloat()); + for (Metrics.BucketSpan span : histogram.getNegativeSpanList()) { + appendMessage( + result, + indent, + "negative_span", + () -> appendBucketSpan(result, indent + INDENT, span)); + } + for (int i = 0; i < histogram.getNegativeDeltaCount(); i++) { + appendScalar(result, indent, "negative_delta", histogram.getNegativeDelta(i)); + } + for (int i = 0; i < histogram.getNegativeCountCount(); i++) { + appendScalar(result, indent, "negative_count", histogram.getNegativeCount(i)); + } + for (Metrics.BucketSpan span : histogram.getPositiveSpanList()) { + appendMessage( + result, + indent, + "positive_span", + () -> appendBucketSpan(result, indent + INDENT, span)); + } + for (int i = 0; i < histogram.getPositiveDeltaCount(); i++) { + appendScalar(result, indent, "positive_delta", histogram.getPositiveDelta(i)); + } + for (int i = 0; i < histogram.getPositiveCountCount(); i++) { + appendScalar(result, indent, "positive_count", histogram.getPositiveCount(i)); + } + for (Metrics.Exemplar exemplar : histogram.getExemplarsList()) { + appendMessage( + result, + indent, + "exemplars", + () -> appendExemplar(result, indent + INDENT, exemplar)); + } + } + + private static void appendBucket(StringBuilder result, String indent, Metrics.Bucket bucket) { + appendScalar( + result, + indent, + "cumulative_count", + bucket.hasCumulativeCount(), + bucket.getCumulativeCount()); + appendScalar( + result, + indent, + "cumulative_count_float", + bucket.hasCumulativeCountFloat(), + bucket.getCumulativeCountFloat()); + appendScalar(result, indent, "upper_bound", bucket.hasUpperBound(), bucket.getUpperBound()); + if (bucket.hasExemplar()) { + appendMessage( + result, + indent, + "exemplar", + () -> appendExemplar(result, indent + INDENT, bucket.getExemplar())); + } + } + + private static void appendBucketSpan( + StringBuilder result, String indent, Metrics.BucketSpan span) { + appendScalar(result, indent, "offset", span.hasOffset(), span.getOffset()); + appendScalar(result, indent, "length", span.hasLength(), span.getLength()); + } + + private static void appendExemplar( + StringBuilder result, String indent, Metrics.Exemplar exemplar) { + for (Metrics.LabelPair label : exemplar.getLabelList()) { + appendMessage(result, indent, "label", () -> appendLabel(result, indent + INDENT, label)); + } + appendScalar(result, indent, "value", exemplar.hasValue(), exemplar.getValue()); + appendTimestamp(result, indent, "timestamp", exemplar.hasTimestamp(), exemplar.getTimestamp()); + } + + private static void appendTimestamp( + StringBuilder result, String indent, String fieldName, boolean present, Timestamp timestamp) { + if (present) { + appendMessage( + result, + indent, + fieldName, + () -> { + appendScalar(result, indent + INDENT, "seconds", timestamp.getSeconds()); + appendScalar(result, indent + INDENT, "nanos", timestamp.getNanos()); + }); + } + } + + private static void appendString( + StringBuilder result, String indent, String fieldName, boolean present, String value) { + if (present) { + appendScalar(result, indent, fieldName, quote(value)); + } + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, boolean present, double value) { + if (present) { + appendScalar(result, indent, fieldName, formatDouble(value)); + } + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, boolean present, long value) { + if (present) { + appendScalar(result, indent, fieldName, Long.toString(value)); + } + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, boolean present, int value) { + if (present) { + appendScalar(result, indent, fieldName, Integer.toString(value)); + } + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, double value) { + appendScalar(result, indent, fieldName, formatDouble(value)); + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, long value) { + appendScalar(result, indent, fieldName, Long.toString(value)); + } + + private static void appendScalar( + StringBuilder result, String indent, String fieldName, String value) { + result.append(indent).append(fieldName).append(": ").append(value).append('\n'); + } + + private static void appendMessage( + StringBuilder result, String indent, String fieldName, Runnable appendBody) { + result.append(indent).append(fieldName).append(" {\n"); + appendBody.run(); + result.append(indent).append("}\n"); + } + + private static String quote(String value) { + return "\"" + TextFormat.escapeBytes(ByteString.copyFromUtf8(value)) + "\""; + } + + private static String formatDouble(double value) { + if (Double.isNaN(value)) { + return "nan"; + } + if (value == Double.POSITIVE_INFINITY) { + return "inf"; + } + if (value == Double.NEGATIVE_INFINITY) { + return "-inf"; + } + return Double.toString(value); + } +} diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java index 9c3f74943..734dc5b1d 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java @@ -3,7 +3,6 @@ import static io.prometheus.metrics.expositionformats.internal.ProtobufUtil.timestampFromMillis; import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName; -import com.google.protobuf.TextFormat; import io.prometheus.metrics.config.EscapingScheme; import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; import io.prometheus.metrics.expositionformats.TextFormatUtil; @@ -51,9 +50,8 @@ public String toDebugString(MetricSnapshots metricSnapshots, EscapingScheme esca MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, escapingScheme); if (!snapshot.getDataPoints().isEmpty()) { stringBuilder.append( - TextFormat.printer() - .printToString( - convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme))); + PrometheusProtobufDebugFormat.toDebugString( + convert(snapshot, s.getMetadata().getOriginalName(), escapingScheme))); } } return stringBuilder.toString(); diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java index 5f871c585..bb5608c92 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java @@ -6,7 +6,11 @@ import io.prometheus.metrics.expositionformats.generated.Metrics; import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; import io.prometheus.metrics.expositionformats.internal.ProtobufUtil; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; +import org.junit.jupiter.api.Test; @SuppressWarnings("NonCanonicalType") class ProtobufExpositionFormatsTest extends ExpositionFormatsTest { @@ -19,4 +23,45 @@ protected void assertPrometheusProtobuf(String expected, MetricSnapshot snapshot String actual = ProtobufUtil.shortDebugString(protobufData); assertThat(actual).isEqualTo(expected); } + + @Test + void testNativeHistogramDebugString() { + HistogramSnapshot histogram = + HistogramSnapshot.builder() + .name("request_latency_seconds") + .help("request latency") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(0.123) + .nativeSchema(5) + .nativeZeroThreshold(2.938735877055719E-39) + .nativeZeroCount(0) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(-96, 1).build()) + .build()) + .build(); + + PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl(); + + assertThat( + writer.toDebugString(MetricSnapshots.of(histogram), EscapingScheme.UNDERSCORE_ESCAPING)) + .isEqualTo( + "name: \"request_latency_seconds\"\n" + + "help: \"request latency\"\n" + + "type: HISTOGRAM\n" + + "metric {\n" + + " histogram {\n" + + " sample_count: 1\n" + + " sample_sum: 0.123\n" + + " schema: 5\n" + + " zero_threshold: 2.938735877055719E-39\n" + + " zero_count: 0\n" + + " positive_span {\n" + + " offset: -96\n" + + " length: 1\n" + + " }\n" + + " positive_delta: 1\n" + + " }\n" + + "}\n"); + } } From c9c70bb8b555d818e412ded60da175531a2b161e Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 1 Jul 2026 03:09:23 +0530 Subject: [PATCH 2/6] fix: align protobuf debug formatting with text format order Signed-off-by: Arnab Nandy Signed-off-by: Gregor Zeitlinger --- ...-metrics-exporter-opentelemetry-shaded.txt | 4 +- pom.xml | 3 + .../PrometheusProtobufDebugFormat.java | 49 +++---- .../ProtobufExpositionFormatsTest.java | 22 +--- .../PrometheusProtobufDebugFormatTest.java | 124 ++++++++++++++++++ 5 files changed, 152 insertions(+), 50 deletions(-) create mode 100644 prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormatTest.java diff --git a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt index 1b6d81f72..27cf12278 100644 --- a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt +++ b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt @@ -1,7 +1,6 @@ Comparing source compatibility of prometheus-metrics-exporter-opentelemetry-1.8.1-SNAPSHOT.jar against prometheus-metrics-exporter-opentelemetry-1.8.0.jar -***! MODIFIED CLASS: PUBLIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter (not serializable) +*** MODIFIED CLASS: PUBLIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 - ---! REMOVED CONSTRUCTOR: PUBLIC(-) OpenTelemetryExporter(io.prometheus.metrics.shaded.io_opentelemetry_2_28_1_alpha.sdk.metrics.export.MetricReader) *** MODIFIED CLASS: PUBLIC STATIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter buildAndStart() @@ -17,4 +16,3 @@ Comparing source compatibility of prometheus-metrics-exporter-opentelemetry-1.8. +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder serviceNamespace(java.lang.String) +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder serviceVersion(java.lang.String) +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder timeoutSeconds(int) - diff --git a/pom.xml b/pom.xml index c9ce08081..9c195aea7 100644 --- a/pom.xml +++ b/pom.xml @@ -440,6 +440,9 @@ io.prometheus.metrics.expositionformats.generated io.prometheus.metrics.shaded + + io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter#OpenTelemetryExporter(io.prometheus.metrics.shaded.io_opentelemetry_*.sdk.metrics.export.MetricReader) + false diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java index 99b094efa..27056f93c 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormat.java @@ -58,6 +58,7 @@ private static void appendMetric(StringBuilder result, String indent, Metrics.Me "untyped", () -> appendUntyped(result, indent + INDENT, metric.getUntyped())); } + appendScalar(result, indent, "timestamp_ms", metric.hasTimestampMs(), metric.getTimestampMs()); if (metric.hasHistogram()) { appendMessage( result, @@ -65,7 +66,6 @@ private static void appendMetric(StringBuilder result, String indent, Metrics.Me "histogram", () -> appendHistogram(result, indent + INDENT, metric.getHistogram())); } - appendScalar(result, indent, "timestamp_ms", metric.hasTimestampMs(), metric.getTimestampMs()); } private static void appendLabel(StringBuilder result, String indent, Metrics.LabelPair label) { @@ -124,22 +124,16 @@ private static void appendHistogram( StringBuilder result, String indent, Metrics.Histogram histogram) { appendScalar( result, indent, "sample_count", histogram.hasSampleCount(), histogram.getSampleCount()); - appendScalar( - result, - indent, - "sample_count_float", - histogram.hasSampleCountFloat(), - histogram.getSampleCountFloat()); appendScalar(result, indent, "sample_sum", histogram.hasSampleSum(), histogram.getSampleSum()); for (Metrics.Bucket bucket : histogram.getBucketList()) { appendMessage(result, indent, "bucket", () -> appendBucket(result, indent + INDENT, bucket)); } - appendTimestamp( + appendScalar( result, indent, - "created_timestamp", - histogram.hasCreatedTimestamp(), - histogram.getCreatedTimestamp()); + "sample_count_float", + histogram.hasSampleCountFloat(), + histogram.getSampleCountFloat()); appendScalar(result, indent, "schema", histogram.hasSchema(), histogram.getSchema()); appendScalar( result, @@ -156,10 +150,7 @@ private static void appendHistogram( histogram.getZeroCountFloat()); for (Metrics.BucketSpan span : histogram.getNegativeSpanList()) { appendMessage( - result, - indent, - "negative_span", - () -> appendBucketSpan(result, indent + INDENT, span)); + result, indent, "negative_span", () -> appendBucketSpan(result, indent + INDENT, span)); } for (int i = 0; i < histogram.getNegativeDeltaCount(); i++) { appendScalar(result, indent, "negative_delta", histogram.getNegativeDelta(i)); @@ -169,10 +160,7 @@ private static void appendHistogram( } for (Metrics.BucketSpan span : histogram.getPositiveSpanList()) { appendMessage( - result, - indent, - "positive_span", - () -> appendBucketSpan(result, indent + INDENT, span)); + result, indent, "positive_span", () -> appendBucketSpan(result, indent + INDENT, span)); } for (int i = 0; i < histogram.getPositiveDeltaCount(); i++) { appendScalar(result, indent, "positive_delta", histogram.getPositiveDelta(i)); @@ -180,12 +168,15 @@ private static void appendHistogram( for (int i = 0; i < histogram.getPositiveCountCount(); i++) { appendScalar(result, indent, "positive_count", histogram.getPositiveCount(i)); } + appendTimestamp( + result, + indent, + "created_timestamp", + histogram.hasCreatedTimestamp(), + histogram.getCreatedTimestamp()); for (Metrics.Exemplar exemplar : histogram.getExemplarsList()) { appendMessage( - result, - indent, - "exemplars", - () -> appendExemplar(result, indent + INDENT, exemplar)); + result, indent, "exemplars", () -> appendExemplar(result, indent + INDENT, exemplar)); } } @@ -196,12 +187,6 @@ private static void appendBucket(StringBuilder result, String indent, Metrics.Bu "cumulative_count", bucket.hasCumulativeCount(), bucket.getCumulativeCount()); - appendScalar( - result, - indent, - "cumulative_count_float", - bucket.hasCumulativeCountFloat(), - bucket.getCumulativeCountFloat()); appendScalar(result, indent, "upper_bound", bucket.hasUpperBound(), bucket.getUpperBound()); if (bucket.hasExemplar()) { appendMessage( @@ -210,6 +195,12 @@ private static void appendBucket(StringBuilder result, String indent, Metrics.Bu "exemplar", () -> appendExemplar(result, indent + INDENT, bucket.getExemplar())); } + appendScalar( + result, + indent, + "cumulative_count_float", + bucket.hasCumulativeCountFloat(), + bucket.getCumulativeCountFloat()); } private static void appendBucketSpan( diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java index bb5608c92..0a3dc3f69 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ProtobufExpositionFormatsTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.google.protobuf.TextFormat; import io.prometheus.metrics.config.EscapingScheme; import io.prometheus.metrics.expositionformats.generated.Metrics; import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; @@ -42,26 +43,11 @@ void testNativeHistogramDebugString() { .build(); PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl(); + Metrics.MetricFamily protobufData = + writer.convert(histogram, EscapingScheme.UNDERSCORE_ESCAPING); assertThat( writer.toDebugString(MetricSnapshots.of(histogram), EscapingScheme.UNDERSCORE_ESCAPING)) - .isEqualTo( - "name: \"request_latency_seconds\"\n" - + "help: \"request latency\"\n" - + "type: HISTOGRAM\n" - + "metric {\n" - + " histogram {\n" - + " sample_count: 1\n" - + " sample_sum: 0.123\n" - + " schema: 5\n" - + " zero_threshold: 2.938735877055719E-39\n" - + " zero_count: 0\n" - + " positive_span {\n" - + " offset: -96\n" - + " length: 1\n" - + " }\n" - + " positive_delta: 1\n" - + " }\n" - + "}\n"); + .isEqualTo(TextFormat.printer().printToString(protobufData)); } } diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormatTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormatTest.java new file mode 100644 index 000000000..8aaab257b --- /dev/null +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufDebugFormatTest.java @@ -0,0 +1,124 @@ +package io.prometheus.metrics.expositionformats.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import io.prometheus.metrics.expositionformats.generated.Metrics; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonCanonicalType") +class PrometheusProtobufDebugFormatTest { + + @Test + void testHistogramFieldsAreEmittedInProtobufFieldNumberOrder() throws Exception { + Metrics.MetricFamily metricFamily = + Metrics.MetricFamily.newBuilder() + .setName("request_latency_seconds") + .setHelp("request latency") + .setType(Metrics.MetricType.HISTOGRAM) + .addMetric( + Metrics.Metric.newBuilder() + .setTimestampMs(123) + .setHistogram( + Metrics.Histogram.newBuilder() + .setSampleCountFloat(3.0) + .setSampleSum(4.0) + .addBucket( + Metrics.Bucket.newBuilder() + .setCumulativeCount(1) + .setUpperBound(2.0) + .setCumulativeCountFloat(1.0)) + .setSchema(5) + .setZeroThreshold(2.938735877055719E-39) + .setZeroCount(0) + .addPositiveSpan( + Metrics.BucketSpan.newBuilder().setOffset(-96).setLength(1)) + .addPositiveDelta(1) + .setCreatedTimestamp( + Timestamp.newBuilder().setSeconds(1000).setNanos(123000000)))) + .build(); + + assertThat(PrometheusProtobufDebugFormat.toDebugString(metricFamily)) + .isEqualTo(TextFormat.printer().printToString(metricFamily)); + } + + @Test + void testAllMetricTypesMatchProtobufTextFormat() throws Exception { + Metrics.MetricFamily metricFamily = + Metrics.MetricFamily.newBuilder() + .setName("test_metric") + .setHelp("test metric") + .setType(Metrics.MetricType.UNTYPED) + .setUnit("seconds") + .addMetric( + Metrics.Metric.newBuilder() + .addLabel(label("path", "/Björn")) + .setGauge(Metrics.Gauge.newBuilder().setValue(2.0))) + .addMetric( + Metrics.Metric.newBuilder() + .setCounter( + Metrics.Counter.newBuilder() + .setValue(3.0) + .setExemplar(exemplar()) + .setCreatedTimestamp(timestamp()))) + .addMetric( + Metrics.Metric.newBuilder() + .setSummary( + Metrics.Summary.newBuilder() + .setSampleCount(7) + .setSampleSum(8.0) + .addQuantile( + Metrics.Quantile.newBuilder().setQuantile(0.99).setValue(42.0)) + .setCreatedTimestamp(timestamp()))) + .addMetric( + Metrics.Metric.newBuilder().setUntyped(Metrics.Untyped.newBuilder().setValue(1.5))) + .addMetric( + Metrics.Metric.newBuilder() + .setHistogram( + Metrics.Histogram.newBuilder() + .setSampleCount(3) + .setSampleSum(4.0) + .addBucket( + Metrics.Bucket.newBuilder() + .setCumulativeCount(1) + .setUpperBound(2.0) + .setExemplar(exemplar()) + .setCumulativeCountFloat(1.0)) + .setSampleCountFloat(3.0) + .setSchema(5) + .setZeroThreshold(2.938735877055719E-39) + .setZeroCount(0) + .setZeroCountFloat(0.0) + .addNegativeSpan( + Metrics.BucketSpan.newBuilder().setOffset(-96).setLength(1)) + .addNegativeDelta(1) + .addNegativeCount(1.0) + .addPositiveSpan( + Metrics.BucketSpan.newBuilder().setOffset(96).setLength(1)) + .addPositiveDelta(1) + .addPositiveCount(1.0) + .setCreatedTimestamp(timestamp()) + .addExemplars(exemplar()))) + .build(); + + assertThat(PrometheusProtobufDebugFormat.toDebugString(metricFamily)) + .isEqualTo(TextFormat.printer().printToString(metricFamily)); + } + + private static Metrics.LabelPair label(String name, String value) { + return Metrics.LabelPair.newBuilder().setName(name).setValue(value).build(); + } + + private static Metrics.Exemplar exemplar() { + return Metrics.Exemplar.newBuilder() + .addLabel(label("trace_id", "abc")) + .setValue(1.0) + .setTimestamp(timestamp()) + .build(); + } + + private static Timestamp timestamp() { + return Timestamp.newBuilder().setSeconds(1000).setNanos(123000000).build(); + } +} From 82a4e90568927aef2be5de924f894aa43b9d283b Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 1 Jul 2026 21:37:45 +0530 Subject: [PATCH 3/6] test: add native protobuf debug reproducer Signed-off-by: Arnab Nandy Signed-off-by: Gregor Zeitlinger --- .../it/springboot/ApplicationTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java index 357c08edf..c56634624 100644 --- a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java +++ b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java @@ -3,7 +3,12 @@ import static org.assertj.core.api.Assertions.assertThat; import io.prometheus.client.it.common.ExporterTest; +import io.prometheus.metrics.config.EscapingScheme; import io.prometheus.metrics.expositionformats.generated.Metrics; +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import io.prometheus.metrics.model.snapshots.HistogramSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets; import java.io.IOException; import java.net.URI; import java.util.List; @@ -31,4 +36,33 @@ void testPrometheusProtobufFormat() throws IOException { .findFirst(); assertThat(metric).isPresent(); } + + @Test + void testPrometheusProtobufDebugFormat() throws IOException { + HistogramSnapshot histogram = + HistogramSnapshot.builder() + .name("native_debug_repro_seconds") + .help("native debug repro") + .dataPoint( + HistogramSnapshot.HistogramDataPointSnapshot.builder() + .sum(0.123) + .nativeSchema(5) + .nativeZeroThreshold(2.938735877055719E-39) + .nativeZeroCount(0) + .nativeBucketsForPositiveValues( + NativeHistogramBuckets.builder().bucket(-96, 1).build()) + .build()) + .build(); + + String debugString = + new PrometheusProtobufWriterImpl() + .toDebugString(MetricSnapshots.of(histogram), EscapingScheme.UNDERSCORE_ESCAPING); + + assertThat(debugString) + .contains( + "name: \"native_debug_repro_seconds\"", + "type: HISTOGRAM", + "schema: 5", + "positive_span"); + } } From f776759adeb704ec7cf846e0e94b7c0e8781acce Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Thu, 2 Jul 2026 00:28:11 +0530 Subject: [PATCH 4/6] chore: rerun api change detector Signed-off-by: Arnab Nandy Signed-off-by: Gregor Zeitlinger From 88ed62ddd55ea4443a7d2a064eafc5ad4c7af4b1 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 2 Jul 2026 11:46:46 +0000 Subject: [PATCH 5/6] chore: refresh api diff report Signed-off-by: Gregor Zeitlinger --- .../prometheus-metrics-exporter-opentelemetry-shaded.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt index 27cf12278..e9179ca06 100644 --- a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt +++ b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt @@ -16,3 +16,4 @@ Comparing source compatibility of prometheus-metrics-exporter-opentelemetry-1.8. +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder serviceNamespace(java.lang.String) +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder serviceVersion(java.lang.String) +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder timeoutSeconds(int) + From 990e0f55550d489dc1d39e275d7b349b7e0a8689 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 2 Jul 2026 12:21:38 +0000 Subject: [PATCH 6/6] chore: drop unrelated api diff changes Signed-off-by: Gregor Zeitlinger --- .../prometheus-metrics-exporter-opentelemetry-shaded.txt | 3 ++- pom.xml | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt index e9179ca06..1b6d81f72 100644 --- a/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt +++ b/docs/apidiffs/current_vs_latest/prometheus-metrics-exporter-opentelemetry-shaded.txt @@ -1,6 +1,7 @@ Comparing source compatibility of prometheus-metrics-exporter-opentelemetry-1.8.1-SNAPSHOT.jar against prometheus-metrics-exporter-opentelemetry-1.8.0.jar -*** MODIFIED CLASS: PUBLIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter (not serializable) +***! MODIFIED CLASS: PUBLIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + ---! REMOVED CONSTRUCTOR: PUBLIC(-) OpenTelemetryExporter(io.prometheus.metrics.shaded.io_opentelemetry_2_28_1_alpha.sdk.metrics.export.MetricReader) *** MODIFIED CLASS: PUBLIC STATIC io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter$Builder (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 +++ NEW METHOD: PUBLIC(+) io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter buildAndStart() diff --git a/pom.xml b/pom.xml index 9c195aea7..c9ce08081 100644 --- a/pom.xml +++ b/pom.xml @@ -440,9 +440,6 @@ io.prometheus.metrics.expositionformats.generated io.prometheus.metrics.shaded - - io.prometheus.metrics.exporter.opentelemetry.OpenTelemetryExporter#OpenTelemetryExporter(io.prometheus.metrics.shaded.io_opentelemetry_*.sdk.metrics.export.MetricReader) - false