Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
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;
import io.prometheus.metrics.model.snapshots.Labels;
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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
if (data.hasCreatedTimestamp()) {

should we even have createdTimestampsEnabled? It isn't a config in OM2 as far as I can tell and only exists in OM1, and the other metric types don't account for it, meaning the counter currently has a differnet default behavior than the others.

tests that demonstrate this (they both fail right now):

  @Test
  void testCounterStartTimestampWithDefaultWriter() throws IOException {
    // Demonstrates the gap: the default OM2 writer (createdTimestampsEnabled=false) silently
    // drops st@ for counters, even though histograms and summaries emit st@ unconditionally.
    // This test currently FAILS. It will pass once the createdTimestampsEnabled guard is
    // removed from writeCounter so that counter behaviour matches histogram/summary behaviour.
    MetricSnapshots snapshots =
        MetricSnapshots.of(
            CounterSnapshot.builder()
                .name("my_counter")
                .dataPoint(
                    CounterSnapshot.CounterDataPointSnapshot.builder()
                        .value(42.0)
                        .createdTimestampMillis(1672850385800L)
                        .build())
                .build());

    String output = writeWithOM2(snapshots);

    assertThat(output)
        .isEqualTo(
            "# TYPE my_counter counter\n"
                + "my_counter 42.0 st@1672850385.800\n"
                + "# EOF\n");
  }

  @Test
  void testCounterAndHistogramEmitStartTimestampConsistently() throws IOException {
    // Demonstrates the asymmetry: a histogram with a created timestamp emits st@ with the
    // default writer; a counter with an identical created timestamp does not.
    // The histogram assertion already passes; the counter assertion currently FAILS.
    // Both will pass once the createdTimestampsEnabled guard is removed from writeCounter.
    long createdMs = 1672850385800L;

    MetricSnapshots counterSnapshots =
        MetricSnapshots.of(
            CounterSnapshot.builder()
                .name("my_counter")
                .dataPoint(
                    CounterSnapshot.CounterDataPointSnapshot.builder()
                        .value(1.0)
                        .createdTimestampMillis(createdMs)
                        .build())
                .build());

    MetricSnapshots histogramSnapshots =
        MetricSnapshots.of(
            HistogramSnapshot.builder()
                .name("my_histogram")
                .dataPoint(
                    HistogramSnapshot.HistogramDataPointSnapshot.builder()
                        .sum(1.0)
                        .createdTimestampMillis(createdMs)
                        .classicHistogramBuckets(
                            ClassicHistogramBuckets.builder()
                                .bucket(Double.POSITIVE_INFINITY, 1)
                                .build())
                        .build())
                .build());

    OpenMetrics2TextFormatWriter writer =
        OpenMetrics2TextFormatWriter.builder()
            .setOpenMetrics2Properties(
                OpenMetrics2Properties.builder().compositeValues(true).build())
            .build();

    String counterOutput = write(counterSnapshots, writer);
    String histogramOutput = write(histogramSnapshots, writer);

    assertThat(counterOutput).contains("st@1672850385.800");
    assertThat(histogramOutput).contains("st@1672850385.800");
  }

writer.write(" st@");
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
}
writeExemplar(writer, data.getExemplar(), scheme);
writer.write('\n');
}
}

Expand All @@ -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;
}
Expand All @@ -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);
}
}
}
}
Expand All @@ -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('{');
Expand All @@ -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');
}

Expand All @@ -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()
Expand Down Expand Up @@ -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(' ');
Expand All @@ -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');
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Loading