Skip to content

Commit 62b9cbc

Browse files
committed
remove scrape time validation
Signed-off-by: Jay DeLuca <[email protected]>
1 parent 11efcfb commit 62b9cbc

File tree

8 files changed

+37
-190
lines changed

8 files changed

+37
-190
lines changed

docs/content/getting-started/metric-types.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,6 @@ in the `prometheus-metrics-core` API.
276276
However, `prometheus-metrics-model` implements the underlying data model for these types.
277277
To use these types, you need to implement your own `Collector` where the `collect()` method returns
278278
an `UnknownSnapshot` or a `HistogramSnapshot` with `.gaugeHistogram(true)`.
279+
If your custom collector does not implement `getMetricType()` and `getLabelNames()`, ensure it does
280+
not produce the same metric name and label set as another collector, or the exposition may contain
281+
duplicate time series.

docs/content/getting-started/registry.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ Counter eventsTotal2 = Counter.builder()
7878
.register(); // IllegalArgumentException, because a metric with that name is already registered
7979
```
8080

81+
## Validation at registration only
82+
83+
Validation of duplicate metric names and label schemas happens at registration time only.
84+
Built-in metrics (Counter, Gauge, Histogram, etc.) participate in this validation.
85+
86+
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.
91+
8192
## Unregistering a Metric
8293

8394
There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain

docs/content/internals/model.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ All metric types implement
1919
the [Collector](/client_java/api/io/prometheus/metrics/model/registry/Collector.html) interface,
2020
i.e. they provide
2121
a [collect()](</client_java/api/io/prometheus/metrics/model/registry/Collector.html#collect()>)
22-
method to produce snapshots.
22+
method to produce snapshots. Implementors that do not provide metric type or label names (returning
23+
null from `getMetricType()` and `getLabelNames()`) are not validated at registration; they must
24+
avoid producing the same metric name and label schema as another collector, or exposition may be
25+
invalid.
2326

2427
## prometheus-metrics-model
2528

prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/DuplicateNamesExpositionTest.java

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
44
import static org.assertj.core.api.Assertions.assertThat;
5-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
65

76
import io.prometheus.metrics.model.registry.Collector;
87
import io.prometheus.metrics.model.registry.PrometheusRegistry;
@@ -181,57 +180,4 @@ void testOpenMetricsFormat_withDuplicateNames() throws IOException {
181180
""";
182181
assertThat(output).isEqualTo(expected);
183182
}
184-
185-
@Test
186-
void testDuplicateNames_sameLabels_throwsException() {
187-
PrometheusRegistry registry = new PrometheusRegistry();
188-
189-
registry.register(
190-
new Collector() {
191-
@Override
192-
public MetricSnapshot collect() {
193-
return CounterSnapshot.builder()
194-
.name("api_responses")
195-
.help("API responses")
196-
.dataPoint(
197-
CounterSnapshot.CounterDataPointSnapshot.builder()
198-
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
199-
.value(100)
200-
.build())
201-
.build();
202-
}
203-
204-
@Override
205-
public String getPrometheusName() {
206-
return "api_responses_total";
207-
}
208-
});
209-
210-
registry.register(
211-
new Collector() {
212-
@Override
213-
public MetricSnapshot collect() {
214-
return CounterSnapshot.builder()
215-
.name("api_responses")
216-
.help("API responses")
217-
.dataPoint(
218-
CounterSnapshot.CounterDataPointSnapshot.builder()
219-
.labels(Labels.of("uri", "/hello", "outcome", "SUCCESS"))
220-
.value(50)
221-
.build())
222-
.build();
223-
}
224-
225-
@Override
226-
public String getPrometheusName() {
227-
return "api_responses_total";
228-
}
229-
});
230-
231-
// Scrape should throw exception due to duplicate time series (same name + same labels)
232-
assertThatThrownBy(registry::scrape)
233-
.isInstanceOf(IllegalStateException.class)
234-
.hasMessageContaining("duplicate metric name with identical label schema [uri, outcome]")
235-
.hasMessageContaining("api_responses");
236-
}
237183
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ default String getPrometheusName() {
8686
* <p>This is used to prevent different metric types (e.g., Counter and Gauge) from sharing the
8787
* same name. Returning {@code null} means type validation is skipped for this collector.
8888
*
89+
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
90+
* type validation is performed for this collector, and duplicate or conflicting metrics may
91+
* result in invalid exposition output.
92+
*
8993
* @return the metric type, or {@code null} to skip validation
9094
*/
9195
@Nullable
@@ -106,6 +110,11 @@ default MetricType getMetricType() {
106110
*
107111
* <p>Returning {@code null} means label schema validation is skipped for this collector.
108112
*
113+
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
114+
* label-schema validation is performed for this collector. If such a collector produces the same
115+
* metric name and label schema as another at scrape time, the exposition may contain duplicate
116+
* time series, which is invalid in Prometheus.
117+
*
109118
* @return the set of all label names, or {@code null} to skip validation
110119
*/
111120
@Nullable

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ default List<String> getPrometheusNames() {
7878
* <p>This is used for per-name type validation during registration. Returning {@code null} means
7979
* type validation is skipped for that specific metric name.
8080
*
81+
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
82+
* type validation is performed for that name, and duplicate or conflicting metrics may result in
83+
* invalid exposition output.
84+
*
8185
* @param prometheusName the Prometheus metric name
8286
* @return the metric type for the given name, or {@code null} to skip validation
8387
*/
@@ -98,6 +102,11 @@ default MetricType getMetricType(String prometheusName) {
98102
* <p>Returning {@code null} means label schema validation is skipped for that specific metric
99103
* name.
100104
*
105+
* <p>Validation is performed only at registration time. If this method returns {@code null}, no
106+
* label-schema validation is performed for that name. If such a collector produces the same
107+
* metric name and label schema as another at scrape time, the exposition may contain duplicate
108+
* time series, which is invalid in Prometheus.
109+
*
101110
* @param prometheusName the Prometheus metric name
102111
* @return the set of all label names for the given name, or {@code null} to skip validation
103112
*/

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

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
package io.prometheus.metrics.model.registry;
22

3-
import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
43
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
54
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
65
import java.util.ArrayList;
76
import java.util.Collections;
8-
import java.util.HashMap;
97
import java.util.HashSet;
108
import java.util.List;
11-
import java.util.Map;
129
import java.util.Set;
1310
import java.util.concurrent.ConcurrentHashMap;
1411
import java.util.function.Predicate;
@@ -254,8 +251,6 @@ public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) {
254251
}
255252
}
256253

257-
validateNoDuplicateLabelSchemas(allSnapshots);
258-
259254
MetricSnapshots.Builder result = MetricSnapshots.builder();
260255
for (MetricSnapshot snapshot : allSnapshots) {
261256
result.metricSnapshot(snapshot);
@@ -316,78 +311,10 @@ public MetricSnapshots scrape(
316311
}
317312
}
318313

319-
validateNoDuplicateLabelSchemas(allSnapshots);
320-
321314
MetricSnapshots.Builder result = MetricSnapshots.builder();
322315
for (MetricSnapshot snapshot : allSnapshots) {
323316
result.metricSnapshot(snapshot);
324317
}
325318
return result.build();
326319
}
327-
328-
/**
329-
* Validates that snapshots with the same metric name don't have identical label schemas. This
330-
* prevents duplicate time series which would occur if two snapshots produce data points with
331-
* identical label sets.
332-
*/
333-
private void validateNoDuplicateLabelSchemas(List<MetricSnapshot> snapshots) {
334-
Map<String, List<MetricSnapshot>> snapshotsByName = new HashMap<>();
335-
for (MetricSnapshot snapshot : snapshots) {
336-
String name = snapshot.getMetadata().getPrometheusName();
337-
snapshotsByName.computeIfAbsent(name, k -> new ArrayList<>()).add(snapshot);
338-
}
339-
340-
// For each group with the same name, check for duplicate label schemas
341-
for (Map.Entry<String, List<MetricSnapshot>> entry : snapshotsByName.entrySet()) {
342-
List<MetricSnapshot> group = entry.getValue();
343-
if (group.size() <= 1) {
344-
continue;
345-
}
346-
347-
List<Set<String>> labelSchemas = new ArrayList<>();
348-
for (MetricSnapshot snapshot : group) {
349-
Set<String> labelSchema = extractLabelSchema(snapshot);
350-
if (labelSchema != null) {
351-
if (labelSchemas.contains(labelSchema)) {
352-
throw new IllegalStateException(
353-
snapshot.getMetadata().getPrometheusName()
354-
+ ": duplicate metric name with identical label schema "
355-
+ labelSchema);
356-
}
357-
labelSchemas.add(labelSchema);
358-
}
359-
}
360-
}
361-
}
362-
363-
/**
364-
* Extracts the label schema (set of label names) from a snapshot's data points. Returns null if
365-
* the snapshot has no data points or if data points have inconsistent label schemas.
366-
*/
367-
@Nullable
368-
private Set<String> extractLabelSchema(MetricSnapshot snapshot) {
369-
if (snapshot.getDataPoints().isEmpty()) {
370-
return null;
371-
}
372-
373-
DataPointSnapshot firstDataPoint = snapshot.getDataPoints().get(0);
374-
Set<String> labelNames = new HashSet<>();
375-
for (int i = 0; i < firstDataPoint.getLabels().size(); i++) {
376-
labelNames.add(firstDataPoint.getLabels().getName(i));
377-
}
378-
379-
for (DataPointSnapshot dataPoint : snapshot.getDataPoints()) {
380-
Set<String> currentLabelNames = new HashSet<>();
381-
for (int i = 0; i < dataPoint.getLabels().size(); i++) {
382-
currentLabelNames.add(dataPoint.getLabels().getName(i));
383-
}
384-
if (!currentLabelNames.equals(labelNames)) {
385-
// Data points have inconsistent label schemas - this is unusual but valid
386-
// We can't determine a single label schema, so return null
387-
return null;
388-
}
389-
}
390-
391-
return labelNames;
392-
}
393320
}

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

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
1010
import io.prometheus.metrics.model.snapshots.MetricSnapshot;
1111
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
12-
1312
import java.util.Arrays;
1413
import java.util.HashSet;
1514
import java.util.List;
@@ -439,7 +438,7 @@ public MetricType getMetricType() {
439438

440439
@Override
441440
public Set<String> getLabelNames() {
442-
return new HashSet<>(List.of("region"));
441+
return new HashSet<>(asList("region"));
443442
}
444443
};
445444

@@ -474,66 +473,6 @@ public Set<String> getLabelNames() {
474473
assertThatCode(() -> registry.register(counterWithPathLabelAgain)).doesNotThrowAnyException();
475474
}
476475

477-
@Test
478-
public void scrape_withFilter_shouldValidateDuplicateLabelSchemas() {
479-
PrometheusRegistry registry = new PrometheusRegistry();
480-
481-
Collector collector1 =
482-
new Collector() {
483-
@Override
484-
public MetricSnapshot collect() {
485-
return GaugeSnapshot.builder()
486-
.name("requests")
487-
.dataPoint(
488-
GaugeSnapshot.GaugeDataPointSnapshot.builder()
489-
.value(100)
490-
.labels(io.prometheus.metrics.model.snapshots.Labels.of("path", "/api"))
491-
.build())
492-
.build();
493-
}
494-
495-
@Override
496-
public String getPrometheusName() {
497-
return "requests";
498-
}
499-
// No getMetricType() or getLabelNames() - returns null by default
500-
};
501-
502-
Collector collector2 =
503-
new Collector() {
504-
@Override
505-
public MetricSnapshot collect() {
506-
return GaugeSnapshot.builder()
507-
.name("requests")
508-
.dataPoint(
509-
GaugeSnapshot.GaugeDataPointSnapshot.builder()
510-
.value(200)
511-
.labels(io.prometheus.metrics.model.snapshots.Labels.of("path", "/home"))
512-
.build())
513-
.build();
514-
}
515-
516-
@Override
517-
public String getPrometheusName() {
518-
return "requests";
519-
}
520-
// No getMetricType() or getLabelNames() - returns null by default
521-
};
522-
523-
// Both collectors can register because they don't provide type/label info
524-
registry.register(collector1);
525-
registry.register(collector2);
526-
527-
assertThatThrownBy(registry::scrape)
528-
.isInstanceOf(IllegalStateException.class)
529-
.hasMessageContaining("duplicate metric name with identical label schema");
530-
531-
// Filtered scrape should also detect duplicate label schemas
532-
assertThatThrownBy(() -> registry.scrape(name -> name.equals("requests")))
533-
.isInstanceOf(IllegalStateException.class)
534-
.hasMessageContaining("duplicate metric name with identical label schema");
535-
}
536-
537476
@Test
538477
public void register_withEmptyLabelSets_shouldDetectDuplicates() {
539478
PrometheusRegistry registry = new PrometheusRegistry();

0 commit comments

Comments
 (0)