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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions docs/content/config/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,11 @@ This works for all Metrics properties.
| io.prometheus.openmetrics2.composite_values | [OpenMetrics2Properties.getCompositeValues()](</client_java/api/io/prometheus/metrics/config/OpenMetrics2Properties.html#getCompositeValues()>) | (1) |
| io.prometheus.openmetrics2.exemplar_compliance | [OpenMetrics2Properties.getExemplarCompliance()](</client_java/api/io/prometheus/metrics/config/OpenMetrics2Properties.html#getExemplarCompliance()>) | (1) |
| io.prometheus.openmetrics2.native_histograms | [OpenMetrics2Properties.getNativeHistograms()](</client_java/api/io/prometheus/metrics/config/OpenMetrics2Properties.html#getNativeHistograms()>) | (1) |
| io.prometheus.openmetrics2.suffixes | [OpenMetrics2Properties.getSuffixes()](</client_java/api/io/prometheus/metrics/config/OpenMetrics2Properties.html#getSuffixes()>) | (1) |

(1) Boolean value, `true` or `false`. `enabled=true` switches OpenMetrics responses to the OM2
writer, preserving metric names as written by the application. The other OM2 properties remain
opt-in. All OpenMetrics 2.0 flags are experimental and default to `false`.
writer. All OM2 properties default to `false`; set `suffixes=true` to preserve OM1 series names
during migration. All OpenMetrics 2.0 flags are experimental.

## Exporter Filter Properties

Expand Down
42 changes: 25 additions & 17 deletions docs/content/exporters/openmetrics2.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ only need to configure the sub-flags you want.
With `enabled=true` alone:

- OpenMetrics requests use the OM2 writer.
- Metric names are preserved as written by the application.
- Registered metric names are preserved; the required `_info` suffix is still emitted for info
metrics.
- Optional OM2 features such as `composite_values`, `exemplar_compliance`, and
`native_histograms` remain off.

Expand All @@ -56,37 +57,43 @@ PrometheusProperties properties = PrometheusProperties.builder()

## Naming Behavior

OpenMetrics 2.0 removes OM1 suffix rewriting.
By default, the OpenMetrics 2.0 writer preserves registered metric names:

- Counters do not get `_total` appended automatically.
- Units do not get appended automatically.
- Info metrics still end in `_info` because that is required by the spec.
- Counter and unit suffixes are not appended.
- Info metrics end in `_info` because that is required by the spec.

Set `suffixes=true` to append missing counter and unit suffixes. Existing suffixes are not
duplicated.

Examples:

| Metric builder input | OM1 output | OM2 output |
| ---------------------------------- | ----------------- | -------------- |
| `Counter("events")` | `events_total` | `events` |
| `Counter("events_total")` | `events_total` | `events_total` |
| `Counter("req").unit(BYTES)` | `req_bytes_total` | `req` |
| `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | `req_bytes` |
| `Info("target")` | `target_info` | `target_info` |
| Metric builder input | OM1 output | Default OM2 output |
| ---------------------------------- | ----------------- | ------------------ |
| `Counter("events")` | `events_total` | `events` |
| `Counter("events_total")` | `events_total` | `events_total` |
| `Counter("req").unit(BYTES)` | `req_bytes_total` | `req` |
| `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | `req_bytes` |
| `Info("target")` | `target_info` | `target_info` |

To retain the previous OM1-compatible suffix behavior during migration, set:

This means OpenMetrics 2.0 does not apply OM1 suffix behavior such as appending `_total` or unit
suffixes, while the legacy OpenMetrics 1.0 and Prometheus text formats keep that existing suffix
behavior.
```properties
io.prometheus.openmetrics2.suffixes=true
```

## Feature Flags

All OpenMetrics 2.0 flags default to `false`.
OpenMetrics 2.0 feature flags default to `false`. Set `suffixes=true` to preserve OM1 series names
while migrating.

| Property | Effect |
| ------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `io.prometheus.openmetrics2.enabled` | Metric names are preserved as written by the application. |
| `io.prometheus.openmetrics2.enabled` | Enable the OpenMetrics 2.0 writer. |
| `io.prometheus.openmetrics2.content_negotiation` | Apply OM2 behavior only when the scraper requests `version=2.0.0`. |
| `io.prometheus.openmetrics2.composite_values` | Emit histograms, summaries, and gauge histograms as single composite lines with `st@`. |
| `io.prometheus.openmetrics2.exemplar_compliance` | Emit only OM2-compliant exemplars with timestamps. |
| `io.prometheus.openmetrics2.native_histograms` | Emit OM2 native histogram text fields. |
| `io.prometheus.openmetrics2.suffixes` | Append counter and unit suffixes to preserve OM1 series names (opt-in). |

Enable all flags at once:

Expand All @@ -104,6 +111,7 @@ io.prometheus.openmetrics2.content_negotiation=true
io.prometheus.openmetrics2.composite_values=true
io.prometheus.openmetrics2.exemplar_compliance=true
io.prometheus.openmetrics2.native_histograms=true
io.prometheus.openmetrics2.suffixes=true
```

## Content Negotiation
Expand Down
6 changes: 3 additions & 3 deletions docs/content/getting-started/metric-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ For the default OpenMetrics 1.0 and Prometheus text formats, counters are expose
`_total` suffix. You can name a counter either `service_time_seconds` or
`service_time_seconds_total`; the exposed name will be `service_time_seconds_total` in both cases.

The experimental OpenMetrics 2.0 writer behaves differently: It preserves metric names instead of
appending `_total` or unit suffixes automatically. In OpenMetrics 2.0, `_total` is recommended for
counters, but not enforced by the Java client.
The experimental OpenMetrics 2.0 writer preserves registered metric names by default. Set
`io.prometheus.openmetrics2.suffixes=true` to append `_total` and unit suffixes for compatibility
when migrating from OpenMetrics 1.0.

## Gauge

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,28 @@ public class OpenMetrics2Properties {
private static final String COMPOSITE_VALUES = "composite_values";
private static final String EXEMPLAR_COMPLIANCE = "exemplar_compliance";
private static final String NATIVE_HISTOGRAMS = "native_histograms";
private static final String SUFFIXES = "suffixes";

@Nullable private final Boolean enabled;
@Nullable private final Boolean contentNegotiation;
@Nullable private final Boolean compositeValues;
@Nullable private final Boolean exemplarCompliance;
@Nullable private final Boolean nativeHistograms;
@Nullable private final Boolean suffixes;

private OpenMetrics2Properties(
@Nullable Boolean enabled,
@Nullable Boolean contentNegotiation,
@Nullable Boolean compositeValues,
@Nullable Boolean exemplarCompliance,
@Nullable Boolean nativeHistograms) {
@Nullable Boolean nativeHistograms,
@Nullable Boolean suffixes) {
this.enabled = enabled;
this.contentNegotiation = contentNegotiation;
this.compositeValues = compositeValues;
this.exemplarCompliance = exemplarCompliance;
this.nativeHistograms = nativeHistograms;
this.suffixes = suffixes;
}

/**
Expand Down Expand Up @@ -64,6 +68,11 @@ public boolean getNativeHistograms() {
return nativeHistograms != null && nativeHistograms;
}

/** Append unit and type suffixes to metric names. Default is {@code false}. */
public boolean getSuffixes() {
return suffixes != null && suffixes;
}

/**
* Note that this will remove entries from {@code propertySource}. This is because we want to know
* if there are unused properties remaining after all properties have been loaded.
Expand All @@ -75,8 +84,14 @@ static OpenMetrics2Properties load(PropertySource propertySource)
Boolean compositeValues = Util.loadBoolean(PREFIX, COMPOSITE_VALUES, propertySource);
Boolean exemplarCompliance = Util.loadBoolean(PREFIX, EXEMPLAR_COMPLIANCE, propertySource);
Boolean nativeHistograms = Util.loadBoolean(PREFIX, NATIVE_HISTOGRAMS, propertySource);
Boolean suffixes = Util.loadBoolean(PREFIX, SUFFIXES, propertySource);
return new OpenMetrics2Properties(
enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
enabled,
contentNegotiation,
compositeValues,
exemplarCompliance,
nativeHistograms,
suffixes);
}

public static Builder builder() {
Expand All @@ -90,6 +105,7 @@ public static class Builder {
@Nullable private Boolean compositeValues;
@Nullable private Boolean exemplarCompliance;
@Nullable private Boolean nativeHistograms;
@Nullable private Boolean suffixes;

private Builder() {}

Expand Down Expand Up @@ -123,19 +139,31 @@ public Builder nativeHistograms(boolean nativeHistograms) {
return this;
}

/** See {@link #getSuffixes()} */
public Builder suffixes(boolean suffixes) {
this.suffixes = suffixes;
return this;
}

/** Enable all OpenMetrics 2.0 features */
public Builder enableAll() {
this.enabled = true;
this.contentNegotiation = true;
this.compositeValues = true;
this.exemplarCompliance = true;
this.nativeHistograms = true;
this.suffixes = true;
return this;
}

public OpenMetrics2Properties build() {
return new OpenMetrics2Properties(
enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
enabled,
contentNegotiation,
compositeValues,
exemplarCompliance,
nativeHistograms,
suffixes);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ void load() {
"io.prometheus.openmetrics2.exemplar_compliance",
"true",
"io.prometheus.openmetrics2.native_histograms",
"true")));
"true",
"io.prometheus.openmetrics2.suffixes",
"false")));
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getContentNegotiation()).isTrue();
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isTrue();
assertThat(properties.getNativeHistograms()).isTrue();
assertThat(properties.getSuffixes()).isFalse();
}

@Test
Expand Down Expand Up @@ -68,6 +71,10 @@ void loadInvalidValue() {
new HashMap<>(
Map.of("io.prometheus.openmetrics2.native_histograms", "invalid"))))
.withMessage("io.prometheus.openmetrics2.native_histograms: Expecting 'true' or 'false'.");
assertThatExceptionOfType(PrometheusPropertiesException.class)
.isThrownBy(
() -> load(new HashMap<>(Map.of("io.prometheus.openmetrics2.suffixes", "invalid"))))
.withMessage("io.prometheus.openmetrics2.suffixes: Expecting 'true' or 'false'.");
}

private static OpenMetrics2Properties load(Map<String, String> map) {
Expand All @@ -85,12 +92,14 @@ void builder() {
.compositeValues(false)
.exemplarCompliance(true)
.nativeHistograms(false)
.suffixes(false)
.build();
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getContentNegotiation()).isTrue();
assertThat(properties.getCompositeValues()).isFalse();
assertThat(properties.getExemplarCompliance()).isTrue();
assertThat(properties.getNativeHistograms()).isFalse();
assertThat(properties.getSuffixes()).isFalse();
}

@Test
Expand All @@ -101,6 +110,7 @@ void builderEnableAll() {
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isTrue();
assertThat(properties.getNativeHistograms()).isTrue();
assertThat(properties.getSuffixes()).isTrue();
}

@Test
Expand All @@ -111,6 +121,7 @@ void defaultValues() {
assertThat(properties.getCompositeValues()).isFalse();
assertThat(properties.getExemplarCompliance()).isFalse();
assertThat(properties.getNativeHistograms()).isFalse();
assertThat(properties.getSuffixes()).isFalse();
}

@Test
Expand All @@ -121,5 +132,6 @@ void partialConfiguration() {
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isFalse();
assertThat(properties.getNativeHistograms()).isFalse();
assertThat(properties.getSuffixes()).isFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,12 @@ void testOpenMetrics2PropertiesLoading() {
properties.put("io.prometheus.openmetrics2.composite_values", "false");
properties.put("io.prometheus.openmetrics2.exemplar_compliance", "true");
properties.put("io.prometheus.openmetrics2.native_histograms", "false");
properties.put("io.prometheus.openmetrics2.suffixes", "false");
PrometheusProperties config = PrometheusPropertiesLoader.load(properties);
assertThat(config.getOpenMetrics2Properties().getContentNegotiation()).isTrue();
assertThat(config.getOpenMetrics2Properties().getCompositeValues()).isFalse();
assertThat(config.getOpenMetrics2Properties().getExemplarCompliance()).isTrue();
assertThat(config.getOpenMetrics2Properties().getNativeHistograms()).isFalse();
assertThat(config.getOpenMetrics2Properties().getSuffixes()).isFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@

class OpenMetrics2TextFormatWriterTest {

@Test
void preservesNamesByDefault() throws IOException {
Counter counter = Counter.builder().name("requests").unit(Unit.BYTES).build();
counter.inc();

String output =
writeWithWriter(
MetricSnapshots.of(counter.collect()), OpenMetrics2TextFormatWriter.create());

assertThat(output)
.contains("# TYPE requests counter\n")
.contains("# UNIT requests bytes\n")
.containsPattern("(?m)^requests 1\\.0 st@\\d+\\.\\d{3}$");
}

@Test
void suffixesCanBeEnabledForOm1Compatibility() throws IOException {
Counter counter = Counter.builder().name("requests").unit(Unit.BYTES).build();
counter.inc();

OpenMetrics2TextFormatWriter writer =
OpenMetrics2TextFormatWriter.builder()
.setOpenMetrics2Properties(OpenMetrics2Properties.builder().suffixes(true).build())
.build();
String output = writeWithWriter(MetricSnapshots.of(counter.collect()), writer);

assertThat(output)
.contains("# TYPE requests_bytes_total counter\n")
.contains("# UNIT requests_bytes_total bytes\n")
.containsPattern("(?m)^requests_bytes_total 1\\.0 st@\\d+\\.\\d{3}$");
}

@Test
void counterPreservesOriginalNameWhenUnitIsConfigured() throws IOException {
Counter counter =
Expand Down Expand Up @@ -93,23 +125,27 @@ void nativeHistogramPreservesOriginalNameWhenUnitIsConfigured() throws IOExcepti
}

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

private String writeWithOM2(MetricSnapshots snapshots) throws IOException {
return write(snapshots, OpenMetrics2TextFormatWriter.create());
OpenMetrics2TextFormatWriter writer =
OpenMetrics2TextFormatWriter.builder()
.setOpenMetrics2Properties(OpenMetrics2Properties.builder().suffixes(false).build())
.build();
return writeWithWriter(snapshots, writer);
}

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

private String write(MetricSnapshots snapshots, ExpositionFormatWriter writer)
private String writeWithWriter(MetricSnapshots snapshots, ExpositionFormatWriter writer)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
writer.write(out, snapshots, EscapingScheme.ALLOW_UTF8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,14 @@ void testRejectsTooLongQueryString() throws IOException {
}

@Test
void testOpenMetrics2DoesNotEscapeUtf8NamesByDefault() throws IOException {
void testOpenMetrics2PreservesNamesAndDoesNotEscapeUtf8ByDefault() throws IOException {
for (boolean contentNegotiation : new boolean[] {true, false}) {
String body =
scrapeUtf8Counter(contentNegotiation, "application/openmetrics-text;version=2.0.0");
assertThat(body).contains("\"my.counter\"").doesNotContain("my_counter");
assertThat(body)
.contains("\"my.counter\"")
.doesNotContain("my.counter_total")
.doesNotContain("my_counter");
}
}

Expand Down
Loading
Loading