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. The `suffixes` property defaults to `true` to preserve OM1 series names. The other OM2
properties remain opt-in and default to `false`. All OpenMetrics 2.0 flags are experimental.

## Exporter Filter Properties

Expand Down
41 changes: 24 additions & 17 deletions docs/content/exporters/openmetrics2.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ 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.
- Counter and unit suffixes are appended so that series names remain compatible with OM1.
- Optional OM2 features such as `composite_values`, `exemplar_compliance`, and
`native_histograms` remain off.

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

## Naming Behavior

OpenMetrics 2.0 removes OM1 suffix rewriting.
By default, the OpenMetrics 2.0 writer keeps OM1 suffix behavior so that switching formats does not
rename existing series:

- 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.
- Counters get `_total` appended when it is missing.
- Unit suffixes are appended when they are missing.
- Existing suffixes are not duplicated.
- Info metrics end in `_info` because that is required by the spec.

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 and default OM2 output | OM2 with `suffixes=false` |
| ---------------------------------- | -------------------------- | ------------------------- |
| `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` |

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.
To emit metric names exactly as written by the application, set:

```properties
io.prometheus.openmetrics2.suffixes=false
```

## Feature Flags

All OpenMetrics 2.0 flags default to `false`.
OpenMetrics 2.0 feature flags default to `false`, except `suffixes`, which defaults to `true` to
preserve series names when migrating from OM1.

| 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. |

Enable all flags at once:

Expand All @@ -104,6 +110,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 appends `_total` and unit suffixes by default so that
switching from OpenMetrics 1.0 does not rename existing series. Set
`io.prometheus.openmetrics2.suffixes=false` to preserve metric names exactly as written instead.

## 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 true}. */
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()).isTrue();
}

@Test
Expand All @@ -121,5 +132,6 @@ void partialConfiguration() {
assertThat(properties.getCompositeValues()).isTrue();
assertThat(properties.getExemplarCompliance()).isFalse();
assertThat(properties.getNativeHistograms()).isFalse();
assertThat(properties.getSuffixes()).isTrue();
}
}
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,21 @@

class OpenMetrics2TextFormatWriterTest {

@Test
void suffixesAreEnabledByDefault() 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_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 +108,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 @@ -200,7 +200,7 @@ void testOpenMetrics2DoesNotEscapeUtf8NamesByDefault() 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_total\"").doesNotContain("my_counter");
}
}

Expand Down
Loading
Loading