Skip to content

Commit c4fa8d7

Browse files
fcollonvalxrmxemdnetoaabmass
authored
Fix: filter exemplar for observable instrument and export of exemplar without trace and span ids (#4251)
* Deal with missing span and trace ids * Fix applying exemplar filter to observable instruments * Lint the code * add tests * Add entry in changelog * Fix span and trace id typing * Fix CI * Test consume_measurement is called for async instrument * Add integration tests * fix integration tests Signed-off-by: emdneto <[email protected]> * Update opentelemetry-sdk/tests/metrics/integration_test/test_exemplars.py * add test that default exemplar filter with no span does not create exemplar --------- Signed-off-by: emdneto <[email protected]> Co-authored-by: Frédéric Collonval <[email protected]> Co-authored-by: Riccardo Magliocchetti <[email protected]> Co-authored-by: Emídio Neto <[email protected]> Co-authored-by: Aaron Abbott <[email protected]>
1 parent a2d77a0 commit c4fa8d7

File tree

8 files changed

+532
-29
lines changed

8 files changed

+532
-29
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Fix metrics export with exemplar and no context and filtering observable instruments
11+
([#4251](https://github.com/open-telemetry/opentelemetry-python/pull/4251))
12+
1013
## Version 1.28.0/0.49b0 (2024-11-05)
1114

1215
- Removed superfluous py.typed markers and added them where they were missing

exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/metrics_encoder/__init__.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
import logging
1515
from os import environ
16-
from typing import Dict
16+
from typing import Dict, List
1717

1818
from opentelemetry.exporter.otlp.proto.common._internal import (
1919
_encode_attributes,
@@ -34,6 +34,7 @@
3434
)
3535
from opentelemetry.sdk.metrics import (
3636
Counter,
37+
Exemplar,
3738
Histogram,
3839
ObservableCounter,
3940
ObservableGauge,
@@ -341,7 +342,7 @@ def _encode_metric(metric, pb2_metric):
341342
)
342343

343344

344-
def _encode_exemplars(sdk_exemplars: list) -> list:
345+
def _encode_exemplars(sdk_exemplars: List[Exemplar]) -> List[pb2.Exemplar]:
345346
"""
346347
Converts a list of SDK Exemplars into a list of protobuf Exemplars.
347348
@@ -353,14 +354,26 @@ def _encode_exemplars(sdk_exemplars: list) -> list:
353354
"""
354355
pb_exemplars = []
355356
for sdk_exemplar in sdk_exemplars:
356-
pb_exemplar = pb2.Exemplar(
357-
time_unix_nano=sdk_exemplar.time_unix_nano,
358-
span_id=_encode_span_id(sdk_exemplar.span_id),
359-
trace_id=_encode_trace_id(sdk_exemplar.trace_id),
360-
filtered_attributes=_encode_attributes(
361-
sdk_exemplar.filtered_attributes
362-
),
363-
)
357+
if (
358+
sdk_exemplar.span_id is not None
359+
and sdk_exemplar.trace_id is not None
360+
):
361+
pb_exemplar = pb2.Exemplar(
362+
time_unix_nano=sdk_exemplar.time_unix_nano,
363+
span_id=_encode_span_id(sdk_exemplar.span_id),
364+
trace_id=_encode_trace_id(sdk_exemplar.trace_id),
365+
filtered_attributes=_encode_attributes(
366+
sdk_exemplar.filtered_attributes
367+
),
368+
)
369+
else:
370+
pb_exemplar = pb2.Exemplar(
371+
time_unix_nano=sdk_exemplar.time_unix_nano,
372+
filtered_attributes=_encode_attributes(
373+
sdk_exemplar.filtered_attributes
374+
),
375+
)
376+
364377
# Assign the value based on its type in the SDK exemplar
365378
if isinstance(sdk_exemplar.value, float):
366379
pb_exemplar.as_double = sdk_exemplar.value

exporter/opentelemetry-exporter-otlp-proto-common/tests/test_metrics_encoder.py

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# pylint: disable=protected-access
15+
# pylint: disable=protected-access,too-many-lines
1616
import unittest
1717

1818
from opentelemetry.exporter.otlp.proto.common._internal.metrics_encoder import (
@@ -33,6 +33,7 @@
3333
from opentelemetry.proto.resource.v1.resource_pb2 import (
3434
Resource as OTLPResource,
3535
)
36+
from opentelemetry.sdk.metrics import Exemplar
3637
from opentelemetry.sdk.metrics.export import (
3738
AggregationTemporality,
3839
Buckets,
@@ -55,6 +56,9 @@
5556

5657

5758
class TestOTLPMetricsEncoder(unittest.TestCase):
59+
span_id = int("6e0c63257de34c92", 16)
60+
trace_id = int("d4cda95b652f4a1592b449d5929fda1b", 16)
61+
5862
histogram = Metric(
5963
name="histogram",
6064
description="foo",
@@ -65,6 +69,22 @@ class TestOTLPMetricsEncoder(unittest.TestCase):
6569
attributes={"a": 1, "b": True},
6670
start_time_unix_nano=1641946016139533244,
6771
time_unix_nano=1641946016139533244,
72+
exemplars=[
73+
Exemplar(
74+
{"filtered": "banana"},
75+
298.0,
76+
1641946016139533400,
77+
span_id,
78+
trace_id,
79+
),
80+
Exemplar(
81+
{"filtered": "banana"},
82+
298.0,
83+
1641946016139533400,
84+
None,
85+
None,
86+
),
87+
],
6888
count=5,
6989
sum=67,
7090
bucket_counts=[1, 4],
@@ -460,7 +480,34 @@ def test_encode_histogram(self):
460480
sum=67,
461481
bucket_counts=[1, 4],
462482
explicit_bounds=[10.0, 20.0],
463-
exemplars=[],
483+
exemplars=[
484+
pb2.Exemplar(
485+
time_unix_nano=1641946016139533400,
486+
as_double=298,
487+
span_id=b"n\x0cc%}\xe3L\x92",
488+
trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b",
489+
filtered_attributes=[
490+
KeyValue(
491+
key="filtered",
492+
value=AnyValue(
493+
string_value="banana"
494+
),
495+
)
496+
],
497+
),
498+
pb2.Exemplar(
499+
time_unix_nano=1641946016139533400,
500+
as_double=298,
501+
filtered_attributes=[
502+
KeyValue(
503+
key="filtered",
504+
value=AnyValue(
505+
string_value="banana"
506+
),
507+
)
508+
],
509+
),
510+
],
464511
max=18.0,
465512
min=8.0,
466513
)
@@ -563,7 +610,34 @@ def test_encode_multiple_scope_histogram(self):
563610
sum=67,
564611
bucket_counts=[1, 4],
565612
explicit_bounds=[10.0, 20.0],
566-
exemplars=[],
613+
exemplars=[
614+
pb2.Exemplar(
615+
time_unix_nano=1641946016139533400,
616+
as_double=298,
617+
span_id=b"n\x0cc%}\xe3L\x92",
618+
trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b",
619+
filtered_attributes=[
620+
KeyValue(
621+
key="filtered",
622+
value=AnyValue(
623+
string_value="banana"
624+
),
625+
)
626+
],
627+
),
628+
pb2.Exemplar(
629+
time_unix_nano=1641946016139533400,
630+
as_double=298,
631+
filtered_attributes=[
632+
KeyValue(
633+
key="filtered",
634+
value=AnyValue(
635+
string_value="banana"
636+
),
637+
)
638+
],
639+
),
640+
],
567641
max=18.0,
568642
min=8.0,
569643
)
@@ -598,7 +672,34 @@ def test_encode_multiple_scope_histogram(self):
598672
sum=67,
599673
bucket_counts=[1, 4],
600674
explicit_bounds=[10.0, 20.0],
601-
exemplars=[],
675+
exemplars=[
676+
pb2.Exemplar(
677+
time_unix_nano=1641946016139533400,
678+
as_double=298,
679+
span_id=b"n\x0cc%}\xe3L\x92",
680+
trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b",
681+
filtered_attributes=[
682+
KeyValue(
683+
key="filtered",
684+
value=AnyValue(
685+
string_value="banana"
686+
),
687+
)
688+
],
689+
),
690+
pb2.Exemplar(
691+
time_unix_nano=1641946016139533400,
692+
as_double=298,
693+
filtered_attributes=[
694+
KeyValue(
695+
key="filtered",
696+
value=AnyValue(
697+
string_value="banana"
698+
),
699+
)
700+
],
701+
),
702+
],
602703
max=18.0,
603704
min=8.0,
604705
)
@@ -640,7 +741,34 @@ def test_encode_multiple_scope_histogram(self):
640741
sum=67,
641742
bucket_counts=[1, 4],
642743
explicit_bounds=[10.0, 20.0],
643-
exemplars=[],
744+
exemplars=[
745+
pb2.Exemplar(
746+
time_unix_nano=1641946016139533400,
747+
as_double=298,
748+
span_id=b"n\x0cc%}\xe3L\x92",
749+
trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b",
750+
filtered_attributes=[
751+
KeyValue(
752+
key="filtered",
753+
value=AnyValue(
754+
string_value="banana"
755+
),
756+
)
757+
],
758+
),
759+
pb2.Exemplar(
760+
time_unix_nano=1641946016139533400,
761+
as_double=298,
762+
filtered_attributes=[
763+
KeyValue(
764+
key="filtered",
765+
value=AnyValue(
766+
string_value="banana"
767+
),
768+
)
769+
],
770+
),
771+
],
644772
max=18.0,
645773
min=8.0,
646774
)
@@ -682,7 +810,34 @@ def test_encode_multiple_scope_histogram(self):
682810
sum=67,
683811
bucket_counts=[1, 4],
684812
explicit_bounds=[10.0, 20.0],
685-
exemplars=[],
813+
exemplars=[
814+
pb2.Exemplar(
815+
time_unix_nano=1641946016139533400,
816+
as_double=298,
817+
span_id=b"n\x0cc%}\xe3L\x92",
818+
trace_id=b"\xd4\xcd\xa9[e/J\x15\x92\xb4I\xd5\x92\x9f\xda\x1b",
819+
filtered_attributes=[
820+
KeyValue(
821+
key="filtered",
822+
value=AnyValue(
823+
string_value="banana"
824+
),
825+
)
826+
],
827+
),
828+
pb2.Exemplar(
829+
time_unix_nano=1641946016139533400,
830+
as_double=298,
831+
filtered_attributes=[
832+
KeyValue(
833+
key="filtered",
834+
value=AnyValue(
835+
string_value="banana"
836+
),
837+
)
838+
],
839+
),
840+
],
686841
max=18.0,
687842
min=8.0,
688843
)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar/exemplar.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,5 @@ class Exemplar:
4646
filtered_attributes: Attributes
4747
value: Union[int, float]
4848
time_unix_nano: int
49-
span_id: Optional[str] = None
50-
trace_id: Optional[str] = None
49+
span_id: Optional[int] = None
50+
trace_id: Optional[int] = None

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/exemplar/exemplar_reservoir.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def __init__(self) -> None:
7777
self.__value: Union[int, float] = 0
7878
self.__attributes: Attributes = None
7979
self.__time_unix_nano: int = 0
80-
self.__span_id: Optional[str] = None
81-
self.__trace_id: Optional[str] = None
80+
self.__span_id: Optional[int] = None
81+
self.__trace_id: Optional[int] = None
8282
self.__offered: bool = False
8383

8484
def offer(

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,17 @@ def __init__(
7878
] = []
7979

8080
def consume_measurement(self, measurement: Measurement) -> None:
81+
should_sample_exemplar = (
82+
self._sdk_config.exemplar_filter.should_sample(
83+
measurement.value,
84+
measurement.time_unix_nano,
85+
measurement.attributes,
86+
measurement.context,
87+
)
88+
)
8189
for reader_storage in self._reader_storages.values():
8290
reader_storage.consume_measurement(
83-
measurement,
84-
self._sdk_config.exemplar_filter.should_sample(
85-
measurement.value,
86-
measurement.time_unix_nano,
87-
measurement.attributes,
88-
measurement.context,
89-
),
91+
measurement, should_sample_exemplar
9092
)
9193

9294
def register_asynchronous_instrument(
@@ -126,7 +128,17 @@ def collect(
126128
)
127129

128130
for measurement in measurements:
129-
metric_reader_storage.consume_measurement(measurement)
131+
should_sample_exemplar = (
132+
self._sdk_config.exemplar_filter.should_sample(
133+
measurement.value,
134+
measurement.time_unix_nano,
135+
measurement.attributes,
136+
measurement.context,
137+
)
138+
)
139+
metric_reader_storage.consume_measurement(
140+
measurement, should_sample_exemplar
141+
)
130142

131143
result = self._reader_storages[metric_reader].collect()
132144

0 commit comments

Comments
 (0)