Repository navigation
Description
Location (prometheus/client_java @ 7695470):
prometheus-metrics-core/.../StateSet.java:150-185:StateSet.Builderdoes not overrideunit(...), so it inherits the plain setter fromMetricWithFixedMetadata.Builder:166-169.prometheus-metrics-model/.../StateSetSnapshot.java:35-38: the snapshot rejects the unit, andStateSet.collectbuilds this snapshot on every scrape (StateSet.java:87).prometheus-metrics-core/.../Info.java:159-166:Info.Builderalready rejects a unit right away;StateSetSnapshot.Builder.unitdoes too.
public B unit(@Nullable Unit unit) {
this.unit = unit;
return self();
} private void validate() {
if (getMetadata().hasUnit()) {
throw new IllegalArgumentException("An state set metric cannot have a unit.");
}Problem
StateSet metrics cannot have a unit in OpenMetrics, and the model enforces that in StateSetSnapshot. The metric builder does not, so this builds and registers fine:
StateSet stateSet = StateSet.builder()
.name("feature_flags")
.states("a", "b")
.unit(Unit.SECONDS)
.build();and the mistake only shows up later, when the metric is collected: every stateSet.collect() throws IllegalArgumentException("An state set metric cannot have a unit."). Info.Builder.unit(...) and StateSetSnapshot.Builder.unit(...) already fail immediately, so StateSet.Builder is the one builder that accepts it. (I have not checked how each exporter reports a collector that throws during a scrape. The claim here is only that the error appears at collect time and not at build time.)
Reproduction
Added to StateSetTest (same shape as InfoTest#unit):
@Test
void unit() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(
() -> StateSet.builder().name("test").states("a", "b").unit(Unit.SECONDS).build())
.withMessage("StateSet metrics cannot have a unit.");
}./mvnw test -pl prometheus-metrics-core -am -Dtest=StateSetTest -Dsurefire.failIfNoSpecifiedTests=false
[ERROR] io.prometheus.metrics.core.metrics.StateSetTest.unit -- Time elapsed: 0.037 s <<< FAILURE!
java.lang.AssertionError:
Expecting code to raise a throwable.
at io.prometheus.metrics.core.metrics.StateSetTest.unit(StateSetTest.java:79)
Calling collect() on the StateSet from the snippet above:
java.lang.IllegalArgumentException: An state set metric cannot have a unit.
at io.prometheus.metrics.model.snapshots.StateSetSnapshot.validate(StateSetSnapshot.java:37)
at io.prometheus.metrics.model.snapshots.StateSetSnapshot.<init>(StateSetSnapshot.java:27)
at io.prometheus.metrics.core.metrics.StateSet.collect(StateSet.java:87)
at io.prometheus.metrics.core.metrics.StateSet.collect(StateSet.java:55)
at io.prometheus.metrics.core.metrics.StatefulMetric.collect(StatefulMetric.java:67)
Environment:
- Commit:
76954702079ded14718b84cbdde7f3bcf1bc03bd; the files involved are unchanged on currentmain(e574d32) - Toolchain: OpenJDK 25.0.5, Apache Maven 3.9.16
Already implemented in prometheus/client_python
PR: prometheus/client_python#300 — "Initial work on OpenMetrics" (merged 2018-09-07T16:26:29Z, merge commit 10c8eb4a830). Among the OpenMetrics groundwork it rejects a unit for Info and Enum (Python's StateSet) when the metric is created, and the OpenMetrics parser rejects units for info and stateset.
How client_python implemented it (relevant hunks, verbatim):
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -405,15 +462,26 @@ def test_invalid_names_raise(self):
...
+ def test_no_units_for_info_enum(self):
+ self.assertRaises(ValueError, Info, 'foo', 'help', unit="x")
+ self.assertRaises(ValueError, Enum, 'foo', 'help', unit="x")
+--- a/prometheus_client/openmetrics/parser.py
+++ b/prometheus_client/openmetrics/parser.py
@@ -0,0 +1,339 @@
...
+ if unit and not name.endswith("_" + unit):
+ raise ValueError("Unit does not match metric name: " + name)
+ if unit and typ in ['info', 'stateset']:
+ raise ValueError("Units not allowed for this metric type: " + name)Suggested fix
Override unit(...) in StateSet.Builder the same way Info.Builder does:
diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
--- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
+++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/StateSet.java
@@ -8,6 +8,7 @@ import io.prometheus.metrics.core.datapoints.StateSetDataPoint;
import io.prometheus.metrics.model.registry.MetricType;
import io.prometheus.metrics.model.snapshots.Labels;
import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
+import io.prometheus.metrics.model.snapshots.Unit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -170,6 +171,17 @@ public class StateSet extends StatefulMetric<StateSetDataPoint, StateSet.DataPoi
return this;
}
+ /**
+ * Throws an {@link UnsupportedOperationException} because StateSet metrics cannot have a unit.
+ */
+ @Override
+ public Builder unit(@Nullable Unit unit) {
+ if (unit != null) {
+ throw new UnsupportedOperationException("StateSet metrics cannot have a unit.");
+ }
+ return this;
+ }
+
@Override
public StateSet build() {
if (names == null) {Verified on 7695470 (OpenJDK 25.0.5, Maven 3.9.16): the test above fails without this patch and passes with it; no other test results change (prometheus-metrics-core and the modules it depends on).
I can submit a PR for this.
Checked: searches for StateSet unit, cannot have a unit, unit UnsupportedOperationException found only #2114 (metric family descriptors) and #1800 (same-name metrics), which do not cover this.
Activity