Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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.getMetadataName;
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;

import io.prometheus.metrics.config.EscapingScheme;
Expand Down Expand Up @@ -40,9 +40,9 @@
import javax.annotation.Nullable;

/**
* Write the OpenMetrics 2.0 text format. This is currently a skeleton implementation that produces
* identical output to OpenMetrics 1.0, with infrastructure for future OM2 features. This is
* experimental and subject to change as the <a
* 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
* 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 @@ -171,22 +171,24 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch
private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
writeMetadata(writer, "counter", metadata, scheme);
// OM2: use the name as provided by the user, no _total appending
String counterName = getExpositionBaseMetadataName(metadata, scheme);
writeMetadataWithName(writer, counterName, "counter", metadata);
for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(
writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
writeDouble(writer, data.getValue());
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
writeCreated(writer, metadata, data, scheme);
writeCreated(writer, counterName, data, scheme);
}
}

private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
writeMetadata(writer, "gauge", metadata, scheme);
String name = getExpositionBaseMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "gauge", metadata);
for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
writeDouble(writer, data.getValue());
if (exemplarsOnAllMetricTypesEnabled) {
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
Expand All @@ -199,20 +201,21 @@ private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme sc
private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
if (snapshot.isGaugeHistogram()) {
writeMetadata(writer, "gaugehistogram", metadata, scheme);
writeMetadataWithName(writer, name, "gaugehistogram", metadata);
writeClassicHistogramBuckets(
writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
} else {
writeMetadata(writer, "histogram", metadata, scheme);
writeMetadataWithName(writer, name, "histogram", metadata);
writeClassicHistogramBuckets(
writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme);
writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme);
}
}

private void writeClassicHistogramBuckets(
Writer writer,
MetricMetadata metadata,
String name,
String countSuffix,
String sumSuffix,
List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
Expand All @@ -225,13 +228,7 @@ private void writeClassicHistogramBuckets(
for (int i = 0; i < buckets.size(); i++) {
cumulativeCount += buckets.getCount(i);
writeNameAndLabels(
writer,
getMetadataName(metadata, scheme),
"_bucket",
data.getLabels(),
scheme,
"le",
buckets.getUpperBound(i));
writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i));
writeLong(writer, cumulativeCount);
Exemplar exemplar;
if (i == 0) {
Expand All @@ -243,9 +240,9 @@ private void writeClassicHistogramBuckets(
}
// In OpenMetrics format, histogram _count and _sum are either both present or both absent.
if (data.hasCount() && data.hasSum()) {
writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme);
writeCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme);
}
writeCreated(writer, metadata, data, scheme);
writeCreated(writer, name, data, scheme);
}
}

Expand All @@ -263,12 +260,13 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
throws IOException {
boolean metadataWritten = false;
MetricMetadata metadata = snapshot.getMetadata();
String name = getExpositionBaseMetadataName(metadata, scheme);
for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
continue;
}
if (!metadataWritten) {
writeMetadata(writer, "summary", metadata, scheme);
writeMetadataWithName(writer, name, "summary", metadata);
metadataWritten = true;
}
Exemplars exemplars = data.getExemplars();
Expand All @@ -280,13 +278,7 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
int exemplarIndex = 1;
for (Quantile quantile : data.getQuantiles()) {
writeNameAndLabels(
writer,
getMetadataName(metadata, scheme),
null,
data.getLabels(),
scheme,
"quantile",
quantile.getQuantile());
writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile());
writeDouble(writer, quantile.getValue());
if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
exemplarIndex = (exemplarIndex + 1) % exemplars.size();
Expand All @@ -296,18 +288,20 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
}
}
// Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme);
writeCreated(writer, metadata, data, scheme);
writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme);
writeCreated(writer, name, data, scheme);
}
}

private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
writeMetadata(writer, "info", metadata, scheme);
// OM2 spec: Info MetricFamily name MUST end in _info
String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info");
String baseName = removeSuffix(infoName, "_info");
writeMetadataWithName(writer, baseName, "info", metadata);
for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(
writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
writer.write("1");
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
}
Expand All @@ -316,10 +310,11 @@ private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme sche
private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
writeMetadata(writer, "stateset", metadata, scheme);
String name = getExpositionBaseMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "stateset", metadata);
for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
for (int i = 0; i < data.size(); i++) {
writer.write(getMetadataName(metadata, scheme));
writer.write(name);
writer.write('{');
Labels labels = data.getLabels();
for (int j = 0; j < labels.size(); j++) {
Expand All @@ -334,7 +329,7 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch
if (!labels.isEmpty()) {
writer.write(",");
}
writer.write(getMetadataName(metadata, scheme));
writer.write(name);
writer.write("=\"");
writeEscapedString(writer, data.getName(i));
writer.write("\"} ");
Expand All @@ -351,9 +346,10 @@ private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingSch
private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
writeMetadata(writer, "unknown", metadata, scheme);
String name = getExpositionBaseMetadataName(metadata, scheme);
writeMetadataWithName(writer, name, "unknown", metadata);
for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
writeDouble(writer, data.getValue());
if (exemplarsOnAllMetricTypesEnabled) {
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
Expand All @@ -365,16 +361,15 @@ private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingSchem

private void writeCountAndSum(
Writer writer,
MetricMetadata metadata,
String name,
DistributionDataPointSnapshot data,
String countSuffix,
String sumSuffix,
Exemplars exemplars,
EscapingScheme scheme)
throws IOException {
if (data.hasCount()) {
writeNameAndLabels(
writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme);
writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme);
writeLong(writer, data.getCount());
if (exemplarsOnAllMetricTypesEnabled) {
writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
Expand All @@ -383,19 +378,17 @@ private void writeCountAndSum(
}
}
if (data.hasSum()) {
writeNameAndLabels(
writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme);
writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme);
writeDouble(writer, data.getSum());
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
}
}

private void writeCreated(
Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme)
Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme)
throws IOException {
if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
writeNameAndLabels(
writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme);
writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme);
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
if (data.hasScrapeTimestamp()) {
writer.write(' ');
Expand Down Expand Up @@ -466,27 +459,40 @@ private void writeScrapeTimestampAndExemplar(
writer.write('\n');
}

private void writeMetadata(
Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme)
throws IOException {
private void writeMetadataWithName(
Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException {
writer.write("# TYPE ");
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
writeName(writer, name, NameType.Metric);
writer.write(' ');
writer.write(typeName);
writer.write('\n');
if (metadata.getUnit() != null) {
writer.write("# UNIT ");
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
writeName(writer, name, NameType.Metric);
writer.write(' ');
writeEscapedString(writer, metadata.getUnit().toString());
writer.write('\n');
}
if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
writer.write("# HELP ");
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
writeName(writer, name, NameType.Metric);
writer.write(' ');
writeEscapedString(writer, metadata.getHelp());
writer.write('\n');
}
}

private static String ensureSuffix(String name, String suffix) {
if (name.endsWith(suffix)) {
return name;
}
return name + suffix;
}

private static String removeSuffix(String name, String suffix) {
if (name.endsWith(suffix)) {
return name.substring(0, name.length() - suffix.length());
}
return name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void testGetOpenMetrics2Properties() {
}

@Test
void testOutputIdenticalToOM1ForCounter() throws IOException {
void testCounterNoTotalSuffix() throws IOException {
MetricSnapshots snapshots =
MetricSnapshots.of(
CounterSnapshot.builder()
Expand All @@ -101,10 +101,37 @@ void testOutputIdenticalToOM1ForCounter() throws IOException {
.build())
.build());

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

assertThat(om2Output).isEqualTo(om1Output);
// OM2: name as provided, no _total appending
assertThat(om2Output)
.isEqualTo(
"# TYPE my_counter_seconds counter\n"
+ "# UNIT my_counter_seconds seconds\n"
+ "# HELP my_counter_seconds Test counter\n"
+ "my_counter_seconds{method=\"GET\"} 42.0\n"
+ "# EOF\n");
}

@Test
void testCounterWithTotalSuffix() throws IOException {
MetricSnapshots snapshots =
MetricSnapshots.of(
CounterSnapshot.builder()
.name("requests_total")
.help("Total requests")
.dataPoint(CounterSnapshot.CounterDataPointSnapshot.builder().value(100.0).build())
.build());

String om2Output = writeWithOM2(snapshots);

// OM2: preserves _total if user provided it
assertThat(om2Output)
.isEqualTo(
"# TYPE requests_total counter\n"
+ "# HELP requests_total Total requests\n"
+ "requests_total 100.0\n"
+ "# EOF\n");
}

@Test
Expand Down Expand Up @@ -220,7 +247,7 @@ void testOutputIdenticalToOM1ForStateSet() throws IOException {
}

@Test
void testOutputIdenticalToOM1WithExemplars() throws IOException {
void testCounterWithExemplars() throws IOException {
Exemplar exemplar =
Exemplar.builder()
.value(100.0)
Expand All @@ -241,14 +268,20 @@ void testOutputIdenticalToOM1WithExemplars() throws IOException {
.build())
.build());

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

assertThat(om2Output).isEqualTo(om1Output);
// OM2: no _total, but exemplar is preserved
assertThat(om2Output)
.isEqualTo(
"# TYPE requests counter\n"
+ "# HELP requests Total requests\n"
+ "requests 1000.0 # {span_id=\"12345\",trace_id=\"abcde\"}"
+ " 100.0 1672850685.829\n"
+ "# EOF\n");
}

@Test
void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException {
void testCounterWithCreatedTimestamps() throws IOException {
MetricSnapshots snapshots =
MetricSnapshots.of(
CounterSnapshot.builder()
Expand All @@ -261,16 +294,19 @@ void testOutputIdenticalToOM1WithCreatedTimestamps() throws IOException {
.build())
.build());

OpenMetricsTextFormatWriter om1Writer =
OpenMetricsTextFormatWriter.builder().setCreatedTimestampsEnabled(true).build();

OpenMetrics2TextFormatWriter om2Writer =
OpenMetrics2TextFormatWriter.builder().setCreatedTimestampsEnabled(true).build();

String om1Output = write(snapshots, om1Writer);
String om2Output = write(snapshots, om2Writer);

assertThat(om2Output).isEqualTo(om1Output);
// OM2: no _total, _created uses the counter name directly
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"
+ "# EOF\n");
}
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.

how should info metrics work?

if we add a test:

  @Test
  void testInfoMetricEnforcesInfoSuffix() throws IOException {
    // User provides "jvm" → OM2 outputs "jvm_info"
    MetricSnapshots snapshots = MetricSnapshots.of(
      InfoSnapshot.builder().name("jvm").help("JVM info")
        .dataPoint(InfoSnapshot.InfoDataPointSnapshot.builder().build()).build());

    String om2Output = writeWithOM2(snapshots);

    // is this right?
    assertThat(om2Output).contains("# TYPE jvm info");
    assertThat(om2Output).contains("jvm_info 1");
  }

the result is:

# TYPE jvm info
# HELP jvm JVM info
jvm_info 1
# EOF

The writeInfo method enforces _info suffix, then removes it for the TYPE line, and then writes the time series with the full _info suffix

Do we require the TYPE line to use the base name or the full name?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good point - om2 should never strip - because metric names are never mutated.

Fixed and added a new test case.


@Test
Expand Down
Loading