Skip to content

Commit 64d64d4

Browse files
committed
pr review
Signed-off-by: Jay DeLuca <jaydeluca4@gmail.com>
1 parent e3e0fe0 commit 64d64d4

7 files changed

Lines changed: 144 additions & 71 deletions

File tree

docs/content/getting-started/registry.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ weight: 2
66
In order to expose metrics, you need to register them with a `PrometheusRegistry`. We are using a
77
counter as an example here, but the `register()` method is the same for all metric types.
88

9-
## Registering a Metrics with the Default Registry
9+
## Registering a Metric with the Default Registry
1010

1111
```java
1212
Counter eventsTotal = Counter.builder()
@@ -18,7 +18,7 @@ Counter eventsTotal = Counter.builder()
1818
The `register()` call above builds the counter and registers it with the global static
1919
`PrometheusRegistry.defaultRegistry`. Using the default registry is recommended.
2020

21-
## Registering a Metrics with a Custom Registry
21+
## Registering a Metric with a Custom Registry
2222

2323
You can also register your metric with a custom registry:
2424

@@ -84,17 +84,24 @@ Validation of duplicate metric names and label schemas happens at registration t
8484
Built-in metrics (Counter, Gauge, Histogram, etc.) participate in this validation.
8585

8686
Custom collectors that implement the `Collector` or `MultiCollector` interface can optionally
87-
implement `getMetricType()` and `getLabelNames()` (or the MultiCollector per-name variants) so the
88-
registry can enforce consistency. If those methods return `null`, the registry does not validate
89-
that collector. If two such collectors produce the same metric name and same label set at scrape
90-
time, the exposition output may contain duplicate time series and be invalid for Prometheus.
87+
implement `getPrometheusName()` and `getMetricType()` (and the MultiCollector per-name variants) so
88+
the registry can enforce consistency. **Validation is skipped when metric name or type is
89+
unavailable:** if `getPrometheusName()` or `getMetricType()` returns `null`, the registry does not
90+
validate that collector. If two such collectors produce the same metric name and same label set at
91+
scrape time, the exposition output may contain duplicate time series and be invalid for Prometheus.
92+
93+
When validation *is* performed (name and type are non-null), **null label names are treated as an
94+
empty label schema:** `getLabelNames()` returning `null` is normalized to `Collections.emptySet()`
95+
and full label-schema validation and duplicate detection still apply. A collector that returns a
96+
non-null type but leaves `getLabelNames()` as `null` is still validated, with its labels treated as
97+
empty.
9198

9299
## Unregistering a Metric
93100

94101
There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain
95102
registered forever.
96103

97-
However, you can programmatically unregistered an obsolete metric like this:
104+
However, you can programmatically unregister an obsolete metric like this:
98105

99106
```java
100107
PrometheusRegistry.defaultRegistry.unregister(eventsTotal);

mise.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tools]
22
"go:github.com/gohugoio/hugo" = "v0.155.0"
33
"go:github.com/grafana/oats" = "0.6.0"
4-
java = "temurin-25.0.1+8.0.LTS"
4+
java = "temurin-25.0.2+10.0.LTS"
55
lychee = "0.22.0"
66
protoc = "33.4"
77

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,13 @@ default MetricType getMetricType() {
109109
* same metric name. Two collectors with the same name and type can coexist if they have different
110110
* label name sets.
111111
*
112-
* <p>Returning {@code null} means label schema validation is skipped for this collector.
112+
* <p>Returning {@code null} is treated as an empty label set: the registry normalizes it to
113+
* {@code Collections.emptySet()} and performs full label-schema validation and duplicate
114+
* detection. Two collectors with the same name, type, and {@code null} (or empty) label names are
115+
* considered duplicate and registration of the second will fail.
113116
*
114-
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
115-
* label-schema validation is performed for this collector. If such a collector produces the same
116-
* metric name and label schema as another at scrape time, the exposition may contain duplicate
117-
* time series, which is invalid in Prometheus.
118-
*
119-
* @return the set of all label names, or {@code null} to skip validation
117+
* @return the set of all label names, or {@code null} (treated as empty) for a metric with no
118+
* labels
120119
*/
121120
@Nullable
122121
default Set<String> getLabelNames() {

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,14 @@ default MetricType getMetricType(String prometheusName) {
100100
* <p>This is used for per-name label schema validation during registration. Two collectors with
101101
* the same name and type can coexist if they have different label name sets.
102102
*
103-
* <p>Returning {@code null} means label schema validation is skipped for that specific metric
104-
* name.
105-
*
106-
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
107-
* label-schema validation is performed for that name. If such a collector produces the same
108-
* metric name and label schema as another at scrape time, the exposition may contain duplicate
109-
* time series, which is invalid in Prometheus.
103+
* <p>Returning {@code null} is treated as an empty label set: the registry normalizes it to
104+
* {@code Collections.emptySet()} and performs full label-schema validation and duplicate
105+
* detection. Two collectors with the same name, type, and {@code null} (or empty) label names are
106+
* considered duplicate and registration of the second will fail.
110107
*
111108
* @param prometheusName the Prometheus metric name
112-
* @return the set of all label names for the given name, or {@code null} to skip validation
109+
* @return the set of all label names for the given name, or {@code null} (treated as empty) for a
110+
* metric with no labels
113111
*/
114112
@Nullable
115113
default Set<String> getLabelNames(String prometheusName) {

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -154,63 +154,69 @@ private static Set<String> immutableLabelNames(@Nullable Set<String> labelNames)
154154
}
155155

156156
public void register(Collector collector) {
157-
if (collectors.contains(collector)) {
157+
if (!collectors.add(collector)) {
158158
throw new IllegalArgumentException("Collector instance is already registered");
159159
}
160-
161-
String prometheusName = collector.getPrometheusName();
162-
MetricType metricType = collector.getMetricType();
163-
Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames());
164-
MetricMetadata metadata = collector.getMetadata();
165-
String help = metadata != null ? metadata.getHelp() : null;
166-
Unit unit = metadata != null ? metadata.getUnit() : null;
167-
168-
// Only perform validation if collector provides sufficient metadata.
169-
// Collectors that don't implement getPrometheusName()/getMetricType() will skip validation.
170-
if (prometheusName != null && metricType != null) {
171-
final String name = prometheusName;
172-
final MetricType type = metricType;
173-
final Set<String> names = normalizedLabels;
174-
final String helpForValidation = help;
175-
final Unit unitForValidation = unit;
176-
registered.compute(
177-
prometheusName,
178-
(n, existingInfo) -> {
179-
if (existingInfo == null) {
180-
return RegistrationInfo.of(type, names, helpForValidation, unitForValidation);
181-
} else {
182-
if (existingInfo.getType() != type) {
183-
throw new IllegalArgumentException(
184-
name
185-
+ ": Conflicting metric types. Existing: "
186-
+ existingInfo.getType()
187-
+ ", new: "
188-
+ type);
189-
}
190-
existingInfo.validateMetadata(helpForValidation, unitForValidation);
191-
if (!existingInfo.addLabelSet(names)) {
192-
throw new IllegalArgumentException(
193-
name + ": duplicate metric name with identical label schema " + names);
160+
try {
161+
String prometheusName = collector.getPrometheusName();
162+
MetricType metricType = collector.getMetricType();
163+
Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames());
164+
MetricMetadata metadata = collector.getMetadata();
165+
String help = metadata != null ? metadata.getHelp() : null;
166+
Unit unit = metadata != null ? metadata.getUnit() : null;
167+
168+
// Only perform validation if collector provides sufficient metadata.
169+
// Collectors that don't implement getPrometheusName()/getMetricType() will skip validation.
170+
if (prometheusName != null && metricType != null) {
171+
final String name = prometheusName;
172+
final MetricType type = metricType;
173+
final Set<String> names = normalizedLabels;
174+
final String helpForValidation = help;
175+
final Unit unitForValidation = unit;
176+
registered.compute(
177+
prometheusName,
178+
(n, existingInfo) -> {
179+
if (existingInfo == null) {
180+
return RegistrationInfo.of(type, names, helpForValidation, unitForValidation);
181+
} else {
182+
if (existingInfo.getType() != type) {
183+
throw new IllegalArgumentException(
184+
name
185+
+ ": Conflicting metric types. Existing: "
186+
+ existingInfo.getType()
187+
+ ", new: "
188+
+ type);
189+
}
190+
existingInfo.validateMetadata(helpForValidation, unitForValidation);
191+
if (!existingInfo.addLabelSet(names)) {
192+
throw new IllegalArgumentException(
193+
name + ": duplicate metric name with identical label schema " + names);
194+
}
195+
return existingInfo;
194196
}
195-
return existingInfo;
196-
}
197-
});
197+
});
198198

199-
collectorMetadata.put(collector, new CollectorRegistration(prometheusName, normalizedLabels));
200-
}
199+
collectorMetadata.put(
200+
collector, new CollectorRegistration(prometheusName, normalizedLabels));
201+
}
201202

202-
if (prometheusName != null) {
203-
prometheusNames.add(prometheusName);
203+
if (prometheusName != null) {
204+
prometheusNames.add(prometheusName);
205+
}
206+
} catch (Exception e) {
207+
collectors.remove(collector);
208+
CollectorRegistration reg = collectorMetadata.remove(collector);
209+
if (reg != null && reg.prometheusName != null) {
210+
unregisterLabelSchema(reg.prometheusName, reg.labelNames);
211+
}
212+
throw e;
204213
}
205-
206-
collectors.add(collector);
207214
}
208215

209216
public void register(MultiCollector collector) {
210-
if (multiCollectors.contains(collector)) {
217+
if (!multiCollectors.add(collector)) {
211218
throw new IllegalArgumentException("MultiCollector instance is already registered");
212219
}
213-
214220
List<String> prometheusNamesList = collector.getPrometheusNames();
215221
List<MultiCollectorRegistration> registrations = new ArrayList<>();
216222
Set<String> namesOnlyInPrometheusNames = new HashSet<>();
@@ -264,8 +270,8 @@ public void register(MultiCollector collector) {
264270
}
265271

266272
multiCollectorMetadata.put(collector, registrations);
267-
multiCollectors.add(collector);
268273
} catch (Exception e) {
274+
multiCollectors.remove(collector);
269275
for (MultiCollectorRegistration registration : registrations) {
270276
unregisterLabelSchema(registration.prometheusName, registration.labelNames);
271277
}

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricSnapshots.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public MetricSnapshots(MetricSnapshot... snapshots) {
2929
*
3030
* @param snapshots the constructor creates a sorted copy of snapshots.
3131
* @throws IllegalArgumentException if snapshots contain conflicting metric types (same name but
32-
* different metric types like Counter vs Gauge).
32+
* different metric types like Counter vs Gauge), or if two HistogramSnapshots share a name
33+
* but differ in gauge histogram vs classic histogram.
3334
*/
3435
public MetricSnapshots(Collection<MetricSnapshot> snapshots) {
3536
List<MetricSnapshot> list = new ArrayList<>(snapshots);

prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,68 @@ public Set<String> getLabelNames() {
256256
.hasMessageContaining("duplicate metric name with identical label schema");
257257
}
258258

259+
@Test
260+
void register_duplicateLabelSchema_rollsBackCollectorOnFailure() {
261+
PrometheusRegistry registry = new PrometheusRegistry();
262+
263+
Collector counter1 =
264+
new Collector() {
265+
@Override
266+
public MetricSnapshot collect() {
267+
return CounterSnapshot.builder().name("http_requests").build();
268+
}
269+
270+
@Override
271+
public String getPrometheusName() {
272+
return "http_requests";
273+
}
274+
275+
@Override
276+
public MetricType getMetricType() {
277+
return MetricType.COUNTER;
278+
}
279+
280+
@Override
281+
public Set<String> getLabelNames() {
282+
return new HashSet<>(asList("path", "status"));
283+
}
284+
};
285+
286+
Collector counter2 =
287+
new Collector() {
288+
@Override
289+
public MetricSnapshot collect() {
290+
return CounterSnapshot.builder().name("http_requests").build();
291+
}
292+
293+
@Override
294+
public String getPrometheusName() {
295+
return "http_requests";
296+
}
297+
298+
@Override
299+
public MetricType getMetricType() {
300+
return MetricType.COUNTER;
301+
}
302+
303+
@Override
304+
public Set<String> getLabelNames() {
305+
return new HashSet<>(asList("path", "status"));
306+
}
307+
};
308+
309+
registry.register(counter1);
310+
311+
// Second collector has same name and label schema - registration fails during metadata
312+
// validation. The failed collector must be rolled back (not present in the registry).
313+
assertThatThrownBy(() -> registry.register(counter2))
314+
.isInstanceOf(IllegalArgumentException.class)
315+
.hasMessageContaining("duplicate metric name with identical label schema");
316+
317+
// Only the first collector should be in the registry; counter2 was removed on rollback.
318+
assertThat(registry.scrape().size()).isEqualTo(1);
319+
}
320+
259321
@Test
260322
void register_nullType_skipsValidation() {
261323
PrometheusRegistry registry = new PrometheusRegistry();

0 commit comments

Comments
 (0)