Skip to content

Commit 3b54544

Browse files
committed
Add test and improve code
1 parent da7a2c0 commit 3b54544

File tree

2 files changed

+122
-38
lines changed

2 files changed

+122
-38
lines changed

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

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@
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
9492
from opentelemetry.sdk.metrics import (
93+
Counter,
94+
Exemplar,
95+
Histogram as HistogramInstrument,
9596
ObservableCounter,
9697
ObservableGauge,
9798
ObservableUpDownCounter,
@@ -107,7 +108,8 @@
107108
Sum,
108109
)
109110
from 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+
111113
from prometheus_client.samples import Exemplar as PrometheusExemplar
112114

113115

@@ -118,29 +120,33 @@
118120

119121

120122
def _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+
441455
class _AutoPrometheusMetricReader(PrometheusMetricReader):
442456
"""Thin wrapper around PrometheusMetricReader used for the opentelemetry_metrics_exporter entry point.
443457

exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
GaugeMetricFamily,
2424
InfoMetricFamily,
2525
)
26+
from prometheus_client.openmetrics.exposition import (
27+
generate_latest as openmetrics_generate_latest,
28+
)
2629

2730
from opentelemetry.exporter.prometheus import (
2831
PrometheusMetricReader,
@@ -31,7 +34,7 @@
3134
from opentelemetry.sdk.environment_variables import (
3235
OTEL_PYTHON_EXPERIMENTAL_DISABLE_PROMETHEUS_UNIT_NORMALIZATION,
3336
)
34-
from opentelemetry.sdk.metrics import MeterProvider
37+
from opentelemetry.sdk.metrics import MeterProvider, Exemplar
3538
from opentelemetry.sdk.metrics.export import (
3639
AggregationTemporality,
3740
Histogram,
@@ -42,6 +45,7 @@
4245
ScopeMetrics,
4346
)
4447
from opentelemetry.sdk.resources import Resource
48+
from opentelemetry.trace import format_span_id, format_trace_id
4549
from opentelemetry.test.metrictestutil import (
4650
_generate_gauge,
4751
_generate_histogram,
@@ -59,7 +63,10 @@ def setUp(self):
5963
)
6064

6165
def verify_text_format(
62-
self, metric: Metric, expect_prometheus_text: str
66+
self,
67+
metric: Metric,
68+
expect_prometheus_text: str,
69+
openmetrics_generator: bool = False,
6370
) -> None:
6471
metrics_data = MetricsData(
6572
resource_metrics=[
@@ -79,7 +86,11 @@ def verify_text_format(
7986

8087
collector = _CustomCollector(disable_target_info=True)
8188
collector.add_metrics_data(metrics_data)
82-
result_bytes = generate_latest(collector)
89+
result_bytes = (
90+
openmetrics_generate_latest(collector)
91+
if openmetrics_generator
92+
else generate_latest(collector)
93+
)
8394
result = result_bytes.decode("utf-8")
8495
self.assertEqual(result, expect_prometheus_text)
8596

@@ -135,6 +146,67 @@ def test_histogram_to_prometheus(self):
135146
),
136147
)
137148

149+
def test_histogram_with_exemplar_to_prometheus(self):
150+
span_id = 10217189687419569865
151+
trace_id = 67545097771067222548457157018666467027
152+
metric = Metric(
153+
name="test@name",
154+
description="foo",
155+
unit="s",
156+
data=Histogram(
157+
data_points=[
158+
HistogramDataPoint(
159+
attributes={"histo": 1},
160+
start_time_unix_nano=1641946016139533244,
161+
time_unix_nano=1641946016139533244,
162+
exemplars=[
163+
Exemplar(
164+
{"filtered": "banana"},
165+
305.0,
166+
1641946016139533244,
167+
span_id,
168+
trace_id,
169+
),
170+
# Will be ignored as part of the same buckets
171+
Exemplar(
172+
{"filtered": "banana"},
173+
298.0,
174+
1641946016139533400,
175+
span_id,
176+
trace_id,
177+
),
178+
],
179+
count=6,
180+
sum=579.0,
181+
bucket_counts=[1, 3, 2],
182+
explicit_bounds=[123.0, 456.0],
183+
min=1,
184+
max=457,
185+
)
186+
],
187+
aggregation_temporality=AggregationTemporality.DELTA,
188+
),
189+
)
190+
span_str = format_span_id(span_id)
191+
trace_str = format_trace_id(trace_id)
192+
self.verify_text_format(
193+
metric,
194+
dedent(
195+
f"""\
196+
# HELP test_name_seconds foo
197+
# TYPE test_name_seconds histogram
198+
# UNIT test_name_seconds seconds
199+
test_name_seconds_bucket{{histo="1",le="123.0"}} 1.0
200+
test_name_seconds_bucket{{histo="1",le="456.0"}} 4.0 # {{filtered="banana",span_id="{span_str}",trace_id="{trace_str}"}} 305.0 1641946016.1395333
201+
test_name_seconds_bucket{{histo="1",le="+Inf"}} 6.0
202+
test_name_seconds_count{{histo="1"}} 6.0
203+
test_name_seconds_sum{{histo="1"}} 579.0
204+
# EOF
205+
"""
206+
),
207+
openmetrics_generator=True,
208+
)
209+
138210
def test_monotonic_sum_to_prometheus(self):
139211
labels = {"environment@": "staging", "os": "Windows"}
140212
metric = _generate_sum(
@@ -321,7 +393,6 @@ def test_list_labels(self):
321393
self.assertEqual(prometheus_metric.samples[0].labels["os"], "Unix")
322394

323395
def test_check_value(self):
324-
325396
collector = _CustomCollector()
326397

327398
self.assertEqual(collector._check_value(1), "1")
@@ -335,7 +406,6 @@ def test_check_value(self):
335406
self.assertEqual(collector._check_value(None), "null")
336407

337408
def test_multiple_collection_calls(self):
338-
339409
metric_reader = PrometheusMetricReader()
340410
provider = MeterProvider(metric_readers=[metric_reader])
341411
meter = provider.get_meter("getting-started", "0.1.2")

0 commit comments

Comments
 (0)