8989 OTEL_EXPORTER_PROMETHEUS_PORT ,
9090 OTEL_PYTHON_EXPERIMENTAL_DISABLE_PROMETHEUS_UNIT_NORMALIZATION ,
9191)
92- from opentelemetry .sdk .metrics import Counter
93- from opentelemetry .sdk .metrics import Histogram as HistogramInstrument
9492from opentelemetry .sdk .metrics import (
93+ Counter ,
94+ Exemplar ,
95+ Histogram as HistogramInstrument ,
9596 ObservableCounter ,
9697 ObservableGauge ,
9798 ObservableUpDownCounter ,
107108 Sum ,
108109)
109110from opentelemetry .util .types import Attributes
110- from opentelemetry .sdk .metrics ._internal import Exemplar
111+ from opentelemetry .trace import format_span_id , format_trace_id
112+
111113from prometheus_client .samples import Exemplar as PrometheusExemplar
112114
113115
118120
119121
120122def _convert_buckets (
121- bucket_counts : Sequence [int ], explicit_bounds : Sequence [float ], exemplars : Sequence [Optional [PrometheusExemplar ]] = None
123+ bucket_counts : Sequence [int ],
124+ explicit_bounds : Sequence [float ],
125+ exemplars : Optional [Sequence [PrometheusExemplar ]] = None ,
122126) -> Sequence [Tuple [str , int , Optional [Exemplar ]]]:
123127 buckets = []
124128 total_count = 0
125- previous_bound = float ('-inf' )
129+ previous_bound = float ("-inf" )
130+
131+ exemplars = list (reversed (exemplars or []))
132+ exemplar = exemplars .pop () if exemplars else None
126133
127134 for upper_bound , count in zip (
128135 chain (explicit_bounds , ["+Inf" ]),
129136 bucket_counts ,
130137 ):
131138 total_count += count
132- buckets .append ((f"{ upper_bound } " , total_count , None ))
133-
134- # assigning exemplars to their corresponding values
135- if exemplars :
136- for i , (upper_bound , _ , _ ) in enumerate (buckets ):
137- for exemplar in exemplars :
138- if previous_bound <= exemplar .value < float (upper_bound ):
139- # Assign the exemplar to the current bucket if it's the first valid one found
140- _ , current_count , current_exemplar = buckets [i ]
141- if current_exemplar is None : # Only assign if no exemplar has been assigned yet
142- buckets [i ] = (upper_bound , current_count , exemplar )
143- previous_bound = float (upper_bound )
139+ current_exemplar = None
140+ upper_bound_f = float (upper_bound )
141+ while exemplar and previous_bound <= exemplar .value < upper_bound_f :
142+ if current_exemplar is None :
143+ # Assign the exemplar to the current bucket if it's the first valid one found
144+ current_exemplar = exemplar
145+ exemplar = exemplars .pop () if exemplars else None
146+ previous_bound = upper_bound_f
147+
148+ buckets .append ((f"{ upper_bound } " , total_count , current_exemplar ))
149+
144150 return buckets
145151
146152
@@ -266,10 +272,10 @@ def _translate_to_prometheus(
266272 label_keys = []
267273 label_values = []
268274 exemplars = [
269- self ._convert_exemplar (ex ) for ex in number_data_point .exemplars
275+ self ._convert_exemplar (ex )
276+ for ex in number_data_point .exemplars
270277 ]
271278
272-
273279 for key , value in sorted (number_data_point .attributes .items ()):
274280 label_keys .append (sanitize_attribute (key ))
275281 label_values .append (self ._check_value (value ))
@@ -322,7 +328,6 @@ def _translate_to_prometheus(
322328 isinstance (metric .data , Sum )
323329 and not should_convert_sum_to_gauge
324330 ):
325-
326331 metric_family_id = "|" .join (
327332 [pre_metric_family_id , CounterMetricFamily .__name__ ]
328333 )
@@ -343,7 +348,6 @@ def _translate_to_prometheus(
343348 isinstance (metric .data , Gauge )
344349 or should_convert_sum_to_gauge
345350 ):
346-
347351 metric_family_id = "|" .join (
348352 [pre_metric_family_id , GaugeMetricFamily .__name__ ]
349353 )
@@ -364,12 +368,11 @@ def _translate_to_prometheus(
364368 metric_family_id
365369 ].add_metric (labels = label_values , value = value )
366370 elif isinstance (metric .data , Histogram ):
367-
368371 metric_family_id = "|" .join (
369372 [pre_metric_family_id , HistogramMetricFamily .__name__ ]
370373 )
371374
372- if (
375+ if (
373376 metric_family_id
374377 not in metric_family_id_metric_family .keys ()
375378 ):
@@ -378,15 +381,17 @@ def _translate_to_prometheus(
378381 name = metric_name ,
379382 documentation = metric_description ,
380383 labels = label_keys ,
381- unit = metric_unit ,
384+ unit = metric_unit ,
382385 )
383386 )
384387 metric_family_id_metric_family [
385388 metric_family_id
386389 ].add_metric (
387390 labels = label_values ,
388391 buckets = _convert_buckets (
389- value ["bucket_counts" ], value ["explicit_bounds" ], value ["exemplars" ]
392+ value ["bucket_counts" ],
393+ value ["explicit_bounds" ],
394+ value ["exemplars" ],
390395 ),
391396 sum_value = value ["sum" ],
392397 )
@@ -414,7 +419,7 @@ def _create_info_metric(
414419 info = InfoMetricFamily (name , description , labels = attributes )
415420 info .add_metric (labels = list (attributes .keys ()), value = attributes )
416421 return info
417-
422+
418423 def _convert_exemplar (self , exemplar_data : Exemplar ) -> PrometheusExemplar :
419424 """
420425 Converts the SDK exemplar into a Prometheus Exemplar, including proper time conversion.
@@ -426,18 +431,27 @@ def _convert_exemplar(self, exemplar_data: Exemplar) -> PrometheusExemplar:
426431 Returns:
427432 - Exemplar: A Prometheus Exemplar object with correct labeling and timing.
428433 """
429- labels = {self ._sanitize_label (key ): str (value ) for key , value in exemplar_data .filtered_attributes .items ()}
430-
434+ labels = {
435+ sanitize_attribute (key ): str (value )
436+ for key , value in exemplar_data .filtered_attributes .items ()
437+ }
438+
431439 # Add trace_id and span_id to labels only if they are valid and not None
432- if exemplar_data .trace_id and exemplar_data .span_id :
433- labels ['trace_id' ] = exemplar_data .trace_id
434- labels ['span_id' ] = exemplar_data .span_id
435-
440+ if (
441+ exemplar_data .trace_id is not None
442+ and exemplar_data .span_id is not None
443+ ):
444+ labels ["trace_id" ] = format_trace_id (exemplar_data .trace_id )
445+ labels ["span_id" ] = format_span_id (exemplar_data .span_id )
446+
436447 # Convert time from nanoseconds to seconds
437448 timestamp_seconds = exemplar_data .time_unix_nano / 1e9
438- prom_exemplar = PrometheusExemplar (labels , exemplar_data .value , timestamp_seconds )
449+ prom_exemplar = PrometheusExemplar (
450+ labels , exemplar_data .value , timestamp_seconds
451+ )
439452 return prom_exemplar
440453
454+
441455class _AutoPrometheusMetricReader (PrometheusMetricReader ):
442456 """Thin wrapper around PrometheusMetricReader used for the opentelemetry_metrics_exporter entry point.
443457
0 commit comments