Skip to content
Merged
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
@@ -0,0 +1,118 @@
package io.prometheus.metrics.core.metrics;

import static org.assertj.core.api.Assertions.assertThat;

import io.prometheus.metrics.config.EscapingScheme;
import io.prometheus.metrics.config.OpenMetrics2Properties;
import io.prometheus.metrics.expositionformats.ExpositionFormatWriter;
import io.prometheus.metrics.expositionformats.OpenMetrics2TextFormatWriter;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import io.prometheus.metrics.model.snapshots.Unit;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;

class OpenMetrics2TextFormatWriterTest {

@Test
void counterPreservesOriginalNameWhenUnitIsConfigured() throws IOException {
Counter counter =
Counter.builder()
.name("my_counter")
.unit(Unit.SECONDS)
.help("Test counter")
.labelNames("method")
.build();
counter.labelValues("GET").inc(42.0);
MetricSnapshots snapshots = MetricSnapshots.of(counter.collect());

String om1Output = writeWithOM1(snapshots);
String om2Output = writeWithOM2(snapshots);

assertThat(om1Output).contains("my_counter_seconds_total{method=\"GET\"} 42.0");
assertThat(om2Output)
.contains("# TYPE my_counter counter\n")
.contains("# UNIT my_counter seconds\n")
.contains("# HELP my_counter Test counter\n")
.containsPattern("(?m)^my_counter\\{method=\"GET\"} 42\\.0 st@\\d+\\.\\d{3}$")
.doesNotContain("my_counter_seconds");
}

@Test
void classicHistogramPreservesOriginalNameWhenUnitIsConfigured() throws IOException {
Histogram histogram =
Histogram.builder()
.name("request_duration")
.unit(Unit.SECONDS)
.help("Request duration in seconds")
.labelNames("path")
.classicOnly()
.classicUpperBounds(10.0)
.build();
histogram.labelValues("/hello").observe(3.2);
MetricSnapshots snapshots = MetricSnapshots.of(histogram.collect());

String om1Output = writeWithOM1(snapshots);
String om2Output = writeWithOM2(snapshots);

assertThat(om1Output).contains("request_duration_seconds_bucket");
assertThat(om2Output)
.contains("# TYPE request_duration histogram\n")
.contains("# UNIT request_duration seconds\n")
.contains("# HELP request_duration Request duration in seconds\n")
.contains("request_duration_bucket{path=\"/hello\",le=\"10.0\"} 1\n")
.contains("request_duration_bucket{path=\"/hello\",le=\"+Inf\"} 1\n")
.contains("request_duration_count{path=\"/hello\"} 1\n")
.contains("request_duration_sum{path=\"/hello\"} 3.2\n")
.doesNotContain("request_duration_seconds");
}

@Test
void nativeHistogramPreservesOriginalNameWhenUnitIsConfigured() throws IOException {
Histogram histogram =
Histogram.builder()
.name("my.request.duration")
.unit(Unit.SECONDS)
.help("Request duration in seconds")
.labelNames("http.path")
.nativeOnly()
.build();
histogram.labelValues("/hello").observe(3.2);
MetricSnapshots snapshots = MetricSnapshots.of(histogram.collect());

String om2Output = writeWithNativeHistograms(snapshots);

assertThat(om2Output)
.contains("# TYPE \"my.request.duration\" histogram\n")
.contains("# UNIT \"my.request.duration\" seconds\n")
.contains("# HELP \"my.request.duration\" Request duration in seconds\n")
.contains("{\"my.request.duration\",\"http.path\"=\"/hello\"} {count:1,sum:3.2,")
.doesNotContain("my.request.duration_seconds");
}

private String writeWithOM1(MetricSnapshots snapshots) throws IOException {
return write(snapshots, OpenMetricsTextFormatWriter.create());
}

private String writeWithOM2(MetricSnapshots snapshots) throws IOException {
return write(snapshots, OpenMetrics2TextFormatWriter.create());
}

private String writeWithNativeHistograms(MetricSnapshots snapshots) throws IOException {
OpenMetrics2TextFormatWriter writer =
OpenMetrics2TextFormatWriter.builder()
.setOpenMetrics2Properties(
OpenMetrics2Properties.builder().nativeHistograms(true).build())
.build();
return write(snapshots, writer);
}

private String write(MetricSnapshots snapshots, ExpositionFormatWriter writer)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
writer.write(out, snapshots, EscapingScheme.ALLOW_UTF8);
return out.toString(StandardCharsets.UTF_8.name());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp;
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getOriginalMetadataName;
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;

import io.prometheus.metrics.config.EscapingScheme;
Expand Down Expand Up @@ -40,8 +40,8 @@

/**
* Write the OpenMetrics 2.0 text format. Unlike the OM1 writer, this writer outputs metric names as
* provided by the user — no {@code _total} or unit suffix appending. The {@code _info} suffix is
* enforced per the OM2 spec (MUST). This is experimental and subject to change as the <a
* provided by the user, without appending {@code _total} or unit suffixes. The {@code _info} suffix
* is enforced per the OM2 spec (MUST). This is experimental and subject to change as the <a
* href="https://github.com/prometheus/docs/blob/main/docs/specs/om/open_metrics_spec_2_0.md">OpenMetrics
* 2.0 specification</a> evolves.
*/
Expand Down Expand Up @@ -89,6 +89,7 @@ public OpenMetrics2TextFormatWriter build() {
public static final String CONTENT_TYPE =
"application/openmetrics-text; version=2.0.0; charset=utf-8";
private final OpenMetrics2Properties openMetrics2Properties;
private final boolean createdTimestampsEnabled;
private final boolean exemplarsOnAllMetricTypesEnabled;
private final OpenMetricsTextFormatWriter om1Writer;

Expand All @@ -102,6 +103,7 @@ public OpenMetrics2TextFormatWriter(
boolean createdTimestampsEnabled,
boolean exemplarsOnAllMetricTypesEnabled) {
this.openMetrics2Properties = openMetrics2Properties;
this.createdTimestampsEnabled = createdTimestampsEnabled;
this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
this.om1Writer =
new OpenMetricsTextFormatWriter(createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled);
Expand Down Expand Up @@ -170,8 +172,8 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch
private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
// OM2: use the name as provided by the user, no _total appending
String counterName = getExpositionBaseMetadataName(metadata, scheme);
// OM2: use the original name, no _total or unit suffix appending.
String counterName = getOriginalMetadataName(metadata, scheme);
writeMetadataWithName(writer, counterName, "counter", metadata);
for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
Expand All @@ -192,7 +194,7 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem
private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
String name = getOriginalMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "gauge", metadata);
for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
Expand All @@ -209,12 +211,12 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS
throws IOException {
boolean compositeHistogram =
openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms();
MetricMetadata metadata = snapshot.getMetadata();
String name = getOriginalMetadataName(metadata, scheme);
if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) {
om1Writer.writeHistogram(writer, snapshot, scheme);
writeClassicHistogram(writer, name, snapshot, scheme);
return;
}
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
if (snapshot.isGaugeHistogram()) {
writeMetadataWithName(writer, name, "gaugehistogram", metadata);
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
Expand All @@ -236,6 +238,88 @@ private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingS
}
}

private void writeClassicHistogram(
Writer writer, String name, HistogramSnapshot snapshot, EscapingScheme scheme)
throws IOException {
if (snapshot.isGaugeHistogram()) {
writeMetadataWithName(writer, name, "gaugehistogram", snapshot.getMetadata());
writeClassicHistogramDataPoints(writer, name, "_gcount", "_gsum", snapshot, scheme);
} else {
writeMetadataWithName(writer, name, "histogram", snapshot.getMetadata());
writeClassicHistogramDataPoints(writer, name, "_count", "_sum", snapshot, scheme);
}
}

private void writeClassicHistogramDataPoints(
Writer writer,
String name,
String countSuffix,
String sumSuffix,
HistogramSnapshot snapshot,
EscapingScheme scheme)
throws IOException {
for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
ClassicHistogramBuckets buckets = getClassicBuckets(data);
Exemplars exemplars = data.getExemplars();
long cumulativeCount = 0;
for (int i = 0; i < buckets.size(); i++) {
cumulativeCount += buckets.getCount(i);
writeNameAndLabels(
writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i));
writeLong(writer, cumulativeCount);
Exemplar exemplar;
if (i == 0) {
exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
} else {
exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
}
writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
}
if (data.hasCount() && data.hasSum()) {
writeClassicCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme);
}
writeClassicCreated(writer, name, data, scheme);
}
}

private void writeClassicCountAndSum(
Writer writer,
String name,
HistogramSnapshot.HistogramDataPointSnapshot data,
String countSuffix,
String sumSuffix,
Exemplars exemplars,
EscapingScheme scheme)
throws IOException {
writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme);
writeLong(writer, data.getCount());
if (exemplarsOnAllMetricTypesEnabled) {
writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
} else {
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
}
writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme);
writeDouble(writer, data.getSum());
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
}

private void writeClassicCreated(
Writer writer,
String name,
HistogramSnapshot.HistogramDataPointSnapshot 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 writeCompositeHistogramDataPoint(
Writer writer,
String name,
Expand Down Expand Up @@ -398,7 +482,7 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
}
boolean metadataWritten = false;
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
String name = getOriginalMetadataName(metadata, scheme);
for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
continue;
Expand Down Expand Up @@ -465,7 +549,7 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche
MetricMetadata metadata = snapshot.getMetadata();
// OM2 spec: Info MetricFamily name MUST end in _info.
// In OM2, TYPE/HELP use the same name as the data lines.
String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info");
String infoName = ensureSuffix(getOriginalMetadataName(metadata, scheme), "_info");
writeMetadataWithName(writer, infoName, "info", metadata);
for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
Expand All @@ -477,7 +561,7 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche
private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
String name = getOriginalMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "stateset", metadata);
for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
for (int i = 0; i < data.size(); i++) {
Expand Down Expand Up @@ -513,7 +597,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch
private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
String name = getOriginalMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "unknown", metadata);
for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
Expand Down
Loading