Skip to content

Commit 5e27ab6

Browse files
committed
feat: add support for Resource attributes configuration for Prometheus exporter
1 parent 15e9664 commit 5e27ab6

2 files changed

Lines changed: 289 additions & 146 deletions

File tree

exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py

Lines changed: 178 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
from json import dumps
6868
from logging import getLogger
6969
from os import environ
70-
from typing import Deque, Dict, Iterable, Sequence, Tuple, Union
70+
from typing import Callable, Deque, Dict, Iterable, Sequence, Tuple, Union
7171

7272
from prometheus_client import start_http_server
7373
from prometheus_client.core import (
@@ -135,7 +135,10 @@ class PrometheusMetricReader(MetricReader):
135135
"""Prometheus metric exporter for OpenTelemetry."""
136136

137137
def __init__(
138-
self, disable_target_info: bool = False, prefix: str = ""
138+
self,
139+
disable_target_info: bool = False,
140+
prefix: str = "",
141+
resource_attr_filter: Callable[[str], bool] | None = None,
139142
) -> None:
140143
super().__init__(
141144
preferred_temporality={
@@ -149,7 +152,9 @@ def __init__(
149152
otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER,
150153
)
151154
self._collector = _CustomCollector(
152-
disable_target_info=disable_target_info, prefix=prefix
155+
disable_target_info=disable_target_info,
156+
prefix=prefix,
157+
resource_attr_filter=resource_attr_filter,
153158
)
154159
REGISTRY.register(self._collector)
155160
self._collector._callback = self.collect
@@ -176,12 +181,18 @@ class _CustomCollector:
176181
https://github.com/prometheus/client_python#custom-collectors
177182
"""
178183

179-
def __init__(self, disable_target_info: bool = False, prefix: str = ""):
184+
def __init__(
185+
self,
186+
disable_target_info: bool = False,
187+
prefix: str = "",
188+
resource_attr_filter: Callable[[str], bool] | None = None,
189+
):
180190
self._callback = None
181191
self._metrics_datas: Deque[MetricsData] = deque()
182192
self._disable_target_info = disable_target_info
183193
self._target_info = None
184194
self._prefix = prefix
195+
self._resource_attr_filter = resource_attr_filter
185196

186197
def add_metrics_data(self, metrics_data: MetricsData) -> None:
187198
"""Add metrics to Prometheus data"""
@@ -226,161 +237,182 @@ def _translate_to_prometheus(
226237
metrics_data: MetricsData,
227238
metric_family_id_metric_family: Dict[str, PrometheusMetric],
228239
):
229-
metrics = []
230-
231240
for resource_metrics in metrics_data.resource_metrics:
241+
resource_attrs = (
242+
{
243+
key: value
244+
for key, value in resource_metrics.resource.attributes.items()
245+
if self._resource_attr_filter(key)
246+
}
247+
if self._resource_attr_filter is not None
248+
else {}
249+
)
250+
232251
for scope_metrics in resource_metrics.scope_metrics:
233252
for metric in scope_metrics.metrics:
234-
metrics.append(metric)
235-
236-
for metric in metrics:
237-
label_values_data_points = []
238-
values = []
239-
240-
metric_name = metric.name
241-
if self._prefix:
242-
metric_name = self._prefix + "_" + metric_name
243-
metric_name = sanitize_full_name(metric_name)
244-
metric_description = metric.description or ""
245-
metric_unit = map_unit(metric.unit)
246-
247-
# First pass: collect all unique label keys across all data points
248-
all_label_keys_set = set()
249-
data_point_attributes = []
250-
for number_data_point in metric.data.data_points:
251-
attrs = {}
252-
for key, value in number_data_point.attributes.items():
253-
sanitized_key = sanitize_attribute(key)
254-
all_label_keys_set.add(sanitized_key)
255-
attrs[sanitized_key] = self._check_value(value)
256-
data_point_attributes.append(attrs)
257-
258-
if isinstance(number_data_point, HistogramDataPoint):
259-
values.append(
260-
{
261-
"bucket_counts": number_data_point.bucket_counts,
262-
"explicit_bounds": (
263-
number_data_point.explicit_bounds
264-
),
265-
"sum": number_data_point.sum,
266-
}
253+
label_values_data_points = []
254+
values = []
255+
256+
metric_name = metric.name
257+
if self._prefix:
258+
metric_name = self._prefix + "_" + metric_name
259+
metric_name = sanitize_full_name(metric_name)
260+
metric_description = metric.description or ""
261+
metric_unit = map_unit(metric.unit)
262+
263+
# First pass: collect all unique label keys across all data points
264+
all_label_keys_set = set()
265+
data_point_attributes = []
266+
for number_data_point in metric.data.data_points:
267+
attrs = {}
268+
for key, value in chain(
269+
resource_attrs.items(),
270+
number_data_point.attributes.items(),
271+
):
272+
sanitized_key = sanitize_attribute(key)
273+
all_label_keys_set.add(sanitized_key)
274+
attrs[sanitized_key] = self._check_value(value)
275+
data_point_attributes.append(attrs)
276+
277+
if isinstance(number_data_point, HistogramDataPoint):
278+
values.append(
279+
{
280+
"bucket_counts": number_data_point.bucket_counts,
281+
"explicit_bounds": (
282+
number_data_point.explicit_bounds
283+
),
284+
"sum": number_data_point.sum,
285+
}
286+
)
287+
else:
288+
values.append(number_data_point.value)
289+
290+
# Sort label keys for consistent ordering
291+
all_label_keys = sorted(all_label_keys_set)
292+
293+
# Second pass: build label values with empty strings for missing labels
294+
for attrs in data_point_attributes:
295+
label_values = []
296+
for key in all_label_keys:
297+
label_values.append(attrs.get(key, ""))
298+
label_values_data_points.append(label_values)
299+
300+
# Create metric family ID without label keys
301+
per_metric_family_id = "|".join(
302+
[
303+
metric_name,
304+
metric_description,
305+
metric_unit,
306+
]
267307
)
268-
else:
269-
values.append(number_data_point.value)
270-
271-
# Sort label keys for consistent ordering
272-
all_label_keys = sorted(all_label_keys_set)
273-
274-
# Second pass: build label values with empty strings for missing labels
275-
for attrs in data_point_attributes:
276-
label_values = []
277-
for key in all_label_keys:
278-
label_values.append(attrs.get(key, ""))
279-
label_values_data_points.append(label_values)
280-
281-
# Create metric family ID without label keys
282-
per_metric_family_id = "|".join(
283-
[
284-
metric_name,
285-
metric_description,
286-
metric_unit,
287-
]
288-
)
289308

290-
is_non_monotonic_sum = (
291-
isinstance(metric.data, Sum)
292-
and metric.data.is_monotonic is False
293-
)
294-
is_cumulative = (
295-
isinstance(metric.data, Sum)
296-
and metric.data.aggregation_temporality
297-
== AggregationTemporality.CUMULATIVE
298-
)
309+
is_non_monotonic_sum = (
310+
isinstance(metric.data, Sum)
311+
and metric.data.is_monotonic is False
312+
)
313+
is_cumulative = (
314+
isinstance(metric.data, Sum)
315+
and metric.data.aggregation_temporality
316+
== AggregationTemporality.CUMULATIVE
317+
)
299318

300-
# The prometheus compatibility spec for sums says: If the aggregation temporality is cumulative and the sum is non-monotonic, it MUST be converted to a Prometheus Gauge.
301-
should_convert_sum_to_gauge = (
302-
is_non_monotonic_sum and is_cumulative
303-
)
319+
# The prometheus compatibility spec for sums says: If the aggregation temporality is cumulative and the sum is non-monotonic, it MUST be converted to a Prometheus Gauge.
320+
should_convert_sum_to_gauge = (
321+
is_non_monotonic_sum and is_cumulative
322+
)
304323

305-
if (
306-
isinstance(metric.data, Sum)
307-
and not should_convert_sum_to_gauge
308-
):
309-
metric_family_id = "|".join(
310-
[per_metric_family_id, CounterMetricFamily.__name__]
311-
)
324+
if (
325+
isinstance(metric.data, Sum)
326+
and not should_convert_sum_to_gauge
327+
):
328+
metric_family_id = "|".join(
329+
[
330+
per_metric_family_id,
331+
CounterMetricFamily.__name__,
332+
]
333+
)
312334

313-
if metric_family_id not in metric_family_id_metric_family:
314-
metric_family_id_metric_family[metric_family_id] = (
315-
CounterMetricFamily(
316-
name=metric_name,
317-
documentation=metric_description,
318-
labels=all_label_keys,
319-
unit=metric_unit,
335+
if (
336+
metric_family_id
337+
not in metric_family_id_metric_family
338+
):
339+
metric_family_id_metric_family[
340+
metric_family_id
341+
] = CounterMetricFamily(
342+
name=metric_name,
343+
documentation=metric_description,
344+
labels=all_label_keys,
345+
unit=metric_unit,
346+
)
347+
for label_values, value in zip(
348+
label_values_data_points, values
349+
):
350+
metric_family_id_metric_family[
351+
metric_family_id
352+
].add_metric(labels=label_values, value=value)
353+
elif (
354+
isinstance(metric.data, Gauge)
355+
or should_convert_sum_to_gauge
356+
):
357+
metric_family_id = "|".join(
358+
[per_metric_family_id, GaugeMetricFamily.__name__]
320359
)
321-
)
322-
for label_values, value in zip(
323-
label_values_data_points, values
324-
):
325-
metric_family_id_metric_family[
326-
metric_family_id
327-
].add_metric(labels=label_values, value=value)
328-
elif isinstance(metric.data, Gauge) or should_convert_sum_to_gauge:
329-
metric_family_id = "|".join(
330-
[per_metric_family_id, GaugeMetricFamily.__name__]
331-
)
332360

333-
if (
334-
metric_family_id
335-
not in metric_family_id_metric_family.keys()
336-
):
337-
metric_family_id_metric_family[metric_family_id] = (
338-
GaugeMetricFamily(
339-
name=metric_name,
340-
documentation=metric_description,
341-
labels=all_label_keys,
342-
unit=metric_unit,
361+
if (
362+
metric_family_id
363+
not in metric_family_id_metric_family.keys()
364+
):
365+
metric_family_id_metric_family[
366+
metric_family_id
367+
] = GaugeMetricFamily(
368+
name=metric_name,
369+
documentation=metric_description,
370+
labels=all_label_keys,
371+
unit=metric_unit,
372+
)
373+
for label_values, value in zip(
374+
label_values_data_points, values
375+
):
376+
metric_family_id_metric_family[
377+
metric_family_id
378+
].add_metric(labels=label_values, value=value)
379+
elif isinstance(metric.data, Histogram):
380+
metric_family_id = "|".join(
381+
[
382+
per_metric_family_id,
383+
HistogramMetricFamily.__name__,
384+
]
343385
)
344-
)
345-
for label_values, value in zip(
346-
label_values_data_points, values
347-
):
348-
metric_family_id_metric_family[
349-
metric_family_id
350-
].add_metric(labels=label_values, value=value)
351-
elif isinstance(metric.data, Histogram):
352-
metric_family_id = "|".join(
353-
[per_metric_family_id, HistogramMetricFamily.__name__]
354-
)
355386

356-
if (
357-
metric_family_id
358-
not in metric_family_id_metric_family.keys()
359-
):
360-
metric_family_id_metric_family[metric_family_id] = (
361-
HistogramMetricFamily(
362-
name=metric_name,
363-
documentation=metric_description,
364-
labels=all_label_keys,
365-
unit=metric_unit,
387+
if (
388+
metric_family_id
389+
not in metric_family_id_metric_family.keys()
390+
):
391+
metric_family_id_metric_family[
392+
metric_family_id
393+
] = HistogramMetricFamily(
394+
name=metric_name,
395+
documentation=metric_description,
396+
labels=all_label_keys,
397+
unit=metric_unit,
398+
)
399+
for label_values, value in zip(
400+
label_values_data_points, values
401+
):
402+
metric_family_id_metric_family[
403+
metric_family_id
404+
].add_metric(
405+
labels=label_values,
406+
buckets=_convert_buckets(
407+
value["bucket_counts"],
408+
value["explicit_bounds"],
409+
),
410+
sum_value=value["sum"],
411+
)
412+
else:
413+
_logger.warning(
414+
"Unsupported metric data. %s", type(metric.data)
366415
)
367-
)
368-
for label_values, value in zip(
369-
label_values_data_points, values
370-
):
371-
metric_family_id_metric_family[
372-
metric_family_id
373-
].add_metric(
374-
labels=label_values,
375-
buckets=_convert_buckets(
376-
value["bucket_counts"], value["explicit_bounds"]
377-
),
378-
sum_value=value["sum"],
379-
)
380-
else:
381-
_logger.warning(
382-
"Unsupported metric data. %s", type(metric.data)
383-
)
384416

385417
# pylint: disable=no-self-use
386418
def _check_value(self, value: Union[int, float, str, Sequence]) -> str:

0 commit comments

Comments
 (0)