Skip to content

StateSet.Builder.unit(...) is accepted at build time, then every collect() throws #2632

Open

Description

@hidingmyname

Location (prometheus/client_java @ 7695470):

    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:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions