6767from json import dumps
6868from logging import getLogger
6969from 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
7272from prometheus_client import start_http_server
7373from 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