Skip to content

Commit 9b0ede8

Browse files
authored
feat: support metric name filtering in OpenTelemetry exporter (#2344)
Closes the last pending high-priority item in #1816 (as all others have already been addressed). Currently, `PrometheusMetricProducer` always exported all metrics in the registry unconditionally. The code already had a TODO sketching how filtering could be added: https://github.com/prometheus/client_java/blob/9432fdc1933611d34d944d4640ebca3bf91f8ee4/prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java#L47-L54 this PR implements that by reusing `ExporterFilterProperties` and `MetricNameFilter`, as already done by `PrometheusScrapeHandler`. As a result, the OpenTelemetry exporter now honors the shared `io.prometheus.exporter.filter.*` configuration, making its behavior consistent with the HTTP/Servlet exporters. --------- Signed-off-by: subhramit <subhramit.bb@live.in>
1 parent 5757c41 commit 9b0ede8

5 files changed

Lines changed: 234 additions & 47 deletions

File tree

docs/content/otel/otlp.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ By default, the `OpenTelemetryExporter` will push metrics every 60 seconds to
4444
the [OpenTelemetryExporter.Builder][builder-javadoc], or at runtime via
4545
[`io.prometheus.exporter.opentelemetry.*`][otel-properties] properties.
4646

47+
The OpenTelemetry exporter also honors the shared [`io.prometheus.exporter.filter.*`][exporter-filter-properties] metric-name
48+
filter properties.
49+
4750
In addition to the Prometheus Java client configuration, the exporter also recognizes standard
4851
OpenTelemetry configuration. For example, you can set
4952
the [OTEL_EXPORTER_OTLP_METRICS_ENDPOINT](https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_metrics_endpoint)
@@ -62,4 +65,5 @@ OTel collector, and a Prometheus server.
6265
[builder-javadoc]: /client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.Builder.html
6366
[opentelemetry-example]: https://github.com/prometheus/client_java/tree/main/examples/example-exporter-opentelemetry
6467
[otel-pipeline]: /client_java/images/otel-pipeline.png
68+
[exporter-filter-properties]: {{< relref "../config/config.md#exporter-filter-properties" >}}
6569
[otel-properties]: {{< relref "../config/config.md#exporter-opentelemetry-properties" >}}

prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/OtelAutoConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ static MetricReader createReader(
4040
MetricReader reader = requireNonNull(readerRef.get());
4141
boolean preserveNames = resolvePreserveNames(builder, config);
4242
reader.register(
43-
new PrometheusMetricProducer(
44-
registry, instrumentationScopeInfo, getResourceField(sdk), preserveNames));
43+
PrometheusMetricProducer.builder(
44+
registry, instrumentationScopeInfo, getResourceField(sdk), preserveNames)
45+
.exporterFilterProperties(config.getExporterFilterProperties())
46+
.build());
4547
return reader;
4648
}
4749

prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import io.opentelemetry.sdk.metrics.export.CollectionRegistration;
88
import io.opentelemetry.sdk.resources.Resource;
99
import io.opentelemetry.sdk.resources.ResourceBuilder;
10+
import io.prometheus.metrics.config.ExporterFilterProperties;
1011
import io.prometheus.metrics.exporter.opentelemetry.otelmodel.MetricDataFactory;
12+
import io.prometheus.metrics.model.registry.MetricNameFilter;
1113
import io.prometheus.metrics.model.registry.PrometheusRegistry;
1214
import io.prometheus.metrics.model.snapshots.CounterSnapshot;
1315
import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
@@ -22,6 +24,7 @@
2224
import java.util.ArrayList;
2325
import java.util.Collection;
2426
import java.util.List;
27+
import java.util.function.Predicate;
2528
import javax.annotation.Nullable;
2629

2730
class PrometheusMetricProducer implements CollectionRegistration {
@@ -30,29 +33,65 @@ class PrometheusMetricProducer implements CollectionRegistration {
3033
private final Resource resource;
3134
private final InstrumentationScopeInfo instrumentationScopeInfo;
3235
private final boolean preserveNames;
36+
@Nullable private final Predicate<String> nameFilter;
3337

34-
public PrometheusMetricProducer(
38+
private PrometheusMetricProducer(
3539
PrometheusRegistry registry,
3640
InstrumentationScopeInfo instrumentationScopeInfo,
3741
Resource resource,
38-
boolean preserveNames) {
42+
boolean preserveNames,
43+
@Nullable Predicate<String> nameFilter) {
3944
this.registry = registry;
4045
this.instrumentationScopeInfo = instrumentationScopeInfo;
4146
this.resource = resource;
4247
this.preserveNames = preserveNames;
48+
this.nameFilter = nameFilter;
49+
}
50+
51+
/**
52+
* Creates a builder for a producer with no metric name filtering by default, i.e. all metrics in
53+
* {@code registry} are exported unless filter properties are configured on the builder.
54+
*/
55+
static Builder builder(
56+
PrometheusRegistry registry,
57+
InstrumentationScopeInfo instrumentationScopeInfo,
58+
Resource resource,
59+
boolean preserveNames) {
60+
return new Builder(registry, instrumentationScopeInfo, resource, preserveNames);
61+
}
62+
63+
/**
64+
* Builds a name filter from {@code io.prometheus.exporter.filter.*} properties, mirroring how
65+
* {@code PrometheusScrapeHandler} builds its filter for the Servlet/HTTPServer exporters so that
66+
* filtering config behaves consistently across exporters.
67+
*
68+
* <p>OpenTelemetry's own Views API also supports filtering and aggregation, and may be preferable
69+
* for OpenTelemetry-specific deployments; this filter is intended for users who want the same
70+
* {@code io.prometheus.exporter.filter.*} config to apply regardless of which exporter they use.
71+
*
72+
* @return {@code null} if no filter properties are set, to avoid the overhead of testing every
73+
* metric name against a filter that matches everything.
74+
*/
75+
@Nullable
76+
private static Predicate<String> makeNameFilter(ExporterFilterProperties props) {
77+
if (props.getAllowedMetricNames() == null
78+
&& props.getExcludedMetricNames() == null
79+
&& props.getAllowedMetricNamePrefixes() == null
80+
&& props.getExcludedMetricNamePrefixes() == null) {
81+
return null;
82+
}
83+
return MetricNameFilter.builder()
84+
.nameMustBeEqualTo(props.getAllowedMetricNames())
85+
.nameMustNotBeEqualTo(props.getExcludedMetricNames())
86+
.nameMustStartWith(props.getAllowedMetricNamePrefixes())
87+
.nameMustNotStartWith(props.getExcludedMetricNamePrefixes())
88+
.build();
4389
}
4490

4591
@Override
4692
public Collection<MetricData> collectAllMetrics() {
47-
// Note: Currently all metrics from the registry are exported. To add metric filtering
48-
// similar to the Servlet exporter, one could:
49-
// 1. Add filter properties to ExporterOpenTelemetryProperties (allowedNames, excludedNames,
50-
// etc.)
51-
// 2. Convert these properties to a Predicate<String> using MetricNameFilter.builder()
52-
// 3. Call registry.scrape(filter) instead of registry.scrape()
53-
// OpenTelemetry also provides its own Views API for filtering and aggregation, which may be
54-
// preferred for OpenTelemetry-specific deployments.
55-
MetricSnapshots snapshots = registry.scrape();
93+
MetricSnapshots snapshots =
94+
nameFilter != null ? registry.scrape(nameFilter) : registry.scrape();
5695
Resource resourceWithTargetInfo = resource.merge(resourceFromTargetInfo(snapshots));
5796
InstrumentationScopeInfo scopeFromInfo = instrumentationScopeFromOtelScopeInfo(snapshots);
5897
List<MetricData> result = new ArrayList<>(snapshots.size());
@@ -142,4 +181,37 @@ private void addUnlessNull(List<MetricData> result, @Nullable MetricData data) {
142181
result.add(data);
143182
}
144183
}
184+
185+
static class Builder {
186+
private final PrometheusRegistry registry;
187+
private final Resource resource;
188+
private final InstrumentationScopeInfo instrumentationScopeInfo;
189+
private final boolean preserveNames;
190+
private ExporterFilterProperties filterProperties = ExporterFilterProperties.builder().build();
191+
192+
private Builder(
193+
PrometheusRegistry registry,
194+
InstrumentationScopeInfo instrumentationScopeInfo,
195+
Resource resource,
196+
boolean preserveNames) {
197+
this.registry = registry;
198+
this.instrumentationScopeInfo = instrumentationScopeInfo;
199+
this.resource = resource;
200+
this.preserveNames = preserveNames;
201+
}
202+
203+
Builder exporterFilterProperties(ExporterFilterProperties filterProperties) {
204+
this.filterProperties = filterProperties;
205+
return this;
206+
}
207+
208+
PrometheusMetricProducer build() {
209+
return new PrometheusMetricProducer(
210+
registry,
211+
instrumentationScopeInfo,
212+
resource,
213+
preserveNames,
214+
makeNameFilter(filterProperties));
215+
}
216+
}
145217
}

prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/ExportTest.java

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions;
1313
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
1414
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
15+
import io.prometheus.metrics.config.ExporterFilterProperties;
1516
import io.prometheus.metrics.core.metrics.Counter;
1617
import io.prometheus.metrics.core.metrics.Gauge;
1718
import io.prometheus.metrics.core.metrics.Histogram;
@@ -46,11 +47,12 @@ void setUp() throws IllegalAccessException, NoSuchFieldException {
4647
MetricReader reader = (MetricReader) field.get(testing);
4748

4849
PrometheusMetricProducer prometheusMetricProducer =
49-
new PrometheusMetricProducer(
50-
registry,
51-
InstrumentationScopeInfo.create("test"),
52-
Resource.create(Attributes.builder().put("staticRes", "value").build()),
53-
false);
50+
PrometheusMetricProducer.builder(
51+
registry,
52+
InstrumentationScopeInfo.create("test"),
53+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
54+
false)
55+
.build();
5456

5557
reader.register(prometheusMetricProducer);
5658
}
@@ -332,11 +334,12 @@ void preserveNamesWithUnit() {
332334
InMemoryMetricReader reader = InMemoryMetricReader.create();
333335
PrometheusRegistry preserveRegistry = new PrometheusRegistry();
334336
reader.register(
335-
new PrometheusMetricProducer(
336-
preserveRegistry,
337-
InstrumentationScopeInfo.create("test"),
338-
Resource.create(Attributes.builder().put("staticRes", "value").build()),
339-
true));
337+
PrometheusMetricProducer.builder(
338+
preserveRegistry,
339+
InstrumentationScopeInfo.create("test"),
340+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
341+
true)
342+
.build());
340343

341344
Counter.builder().name("req").unit(Unit.BYTES).register(preserveRegistry).inc();
342345

@@ -350,11 +353,12 @@ void preserveNamesWithUnitAlreadyInName() {
350353
InMemoryMetricReader reader = InMemoryMetricReader.create();
351354
PrometheusRegistry preserveRegistry = new PrometheusRegistry();
352355
reader.register(
353-
new PrometheusMetricProducer(
354-
preserveRegistry,
355-
InstrumentationScopeInfo.create("test"),
356-
Resource.create(Attributes.builder().put("staticRes", "value").build()),
357-
true));
356+
PrometheusMetricProducer.builder(
357+
preserveRegistry,
358+
InstrumentationScopeInfo.create("test"),
359+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
360+
true)
361+
.build());
358362

359363
Counter.builder().name("req_bytes").unit(Unit.BYTES).register(preserveRegistry).inc();
360364

@@ -368,11 +372,12 @@ void preserveNamesWithoutUnit() {
368372
InMemoryMetricReader reader = InMemoryMetricReader.create();
369373
PrometheusRegistry preserveRegistry = new PrometheusRegistry();
370374
reader.register(
371-
new PrometheusMetricProducer(
372-
preserveRegistry,
373-
InstrumentationScopeInfo.create("test"),
374-
Resource.create(Attributes.builder().put("staticRes", "value").build()),
375-
true));
375+
PrometheusMetricProducer.builder(
376+
preserveRegistry,
377+
InstrumentationScopeInfo.create("test"),
378+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
379+
true)
380+
.build());
376381

377382
Counter.builder().name("events_total").register(preserveRegistry).inc();
378383

@@ -381,6 +386,50 @@ void preserveNamesWithoutUnit() {
381386
OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("events_total");
382387
}
383388

389+
@Test
390+
void metricNameFilterExcludedNames() {
391+
InMemoryMetricReader reader = InMemoryMetricReader.create();
392+
PrometheusRegistry filteredRegistry = new PrometheusRegistry();
393+
reader.register(
394+
PrometheusMetricProducer.builder(
395+
filteredRegistry,
396+
InstrumentationScopeInfo.create("test"),
397+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
398+
false)
399+
.exporterFilterProperties(
400+
ExporterFilterProperties.builder().excludedNames("secret_total").build())
401+
.build());
402+
403+
Counter.builder().name("secret").register(filteredRegistry).inc();
404+
Counter.builder().name("public").register(filteredRegistry).inc();
405+
406+
List<MetricData> metrics = new ArrayList<>(reader.collectAllMetrics());
407+
assertThat(metrics).hasSize(1);
408+
OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("public");
409+
}
410+
411+
@Test
412+
void metricNameFilterAllowedPrefixes() {
413+
InMemoryMetricReader reader = InMemoryMetricReader.create();
414+
PrometheusRegistry filteredRegistry = new PrometheusRegistry();
415+
reader.register(
416+
PrometheusMetricProducer.builder(
417+
filteredRegistry,
418+
InstrumentationScopeInfo.create("test"),
419+
Resource.create(Attributes.builder().put("staticRes", "value").build()),
420+
false)
421+
.exporterFilterProperties(
422+
ExporterFilterProperties.builder().allowedPrefixes("http_").build())
423+
.build());
424+
425+
Counter.builder().name("http_requests").register(filteredRegistry).inc();
426+
Counter.builder().name("jvm_threads").register(filteredRegistry).inc();
427+
428+
List<MetricData> metrics = new ArrayList<>(reader.collectAllMetrics());
429+
assertThat(metrics).hasSize(1);
430+
OpenTelemetryAssertions.assertThat(metrics.get(0)).hasName("http_requests");
431+
}
432+
384433
private MetricAssert metricAssert() {
385434
List<MetricData> metrics = testing.getMetrics();
386435
assertThat(metrics).hasSize(1);

0 commit comments

Comments
 (0)