Skip to content

Commit 9d9022d

Browse files
committed
feat(tracer): add OpenTelemetryProvider implementation (#7003)
1 parent 5147309 commit 9d9022d

3 files changed

Lines changed: 165 additions & 1 deletion

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Tracing utility"""
22

33
from .extensions import aiohttp_trace_config
4+
from .opentelemetry import OpenTelemetryProvider, OpenTelemetrySegment
45
from .tracer import Tracer
56

6-
__all__ = ["Tracer", "aiohttp_trace_config"]
7+
__all__ = ["OpenTelemetryProvider", "OpenTelemetrySegment", "Tracer", "aiohttp_trace_config"]
8+
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from __future__ import annotations
2+
3+
from contextlib import contextmanager
4+
from typing import TYPE_CHECKING, Any, Generator, Sequence
5+
6+
from aws_lambda_powertools.tracing.base import BaseProvider, BaseSegment
7+
8+
if TYPE_CHECKING:
9+
import numbers
10+
import traceback
11+
12+
13+
class OpenTelemetrySegment(BaseSegment):
14+
"""Segment implementation wrapping an OpenTelemetry Span."""
15+
16+
def __init__(self, span: Any):
17+
self.span = span
18+
19+
def close(self, end_time: int | None = None):
20+
if self.span and hasattr(self.span, "end"):
21+
if end_time is not None:
22+
self.span.end(end_time=int(end_time * 1e9))
23+
else:
24+
self.span.end()
25+
26+
def add_subsegment(self, subsegment: Any):
27+
pass
28+
29+
def remove_subsegment(self, subsegment: Any):
30+
pass
31+
32+
def put_annotation(self, key: str, value: str | numbers.Number | bool) -> None:
33+
if self.span and hasattr(self.span, "set_attribute"):
34+
self.span.set_attribute(key, value)
35+
36+
def put_metadata(self, key: str, value: Any, namespace: str = "default") -> None:
37+
if self.span and hasattr(self.span, "set_attribute"):
38+
attr_key = f"{namespace}.{key}" if namespace else key
39+
self.span.set_attribute(attr_key, str(value))
40+
41+
def add_exception(
42+
self,
43+
exception: BaseException,
44+
stack: list[traceback.StackSummary] | None = None,
45+
remote: bool = False,
46+
):
47+
if self.span and hasattr(self.span, "record_exception"):
48+
self.span.record_exception(exception)
49+
50+
51+
class OpenTelemetryProvider(BaseProvider):
52+
"""Tracing provider utilizing OpenTelemetry for Powertools Tracer."""
53+
54+
def __init__(self, tracer: Any | None = None):
55+
if tracer is None:
56+
try:
57+
from opentelemetry import trace
58+
59+
tracer = trace.get_tracer("aws_lambda_powertools")
60+
except ImportError:
61+
tracer = None
62+
self._tracer = tracer
63+
64+
@contextmanager
65+
def in_subsegment(self, name: str | None = None, **kwargs) -> Generator[BaseSegment, None, None]:
66+
name = name or "subsegment"
67+
if self._tracer is not None:
68+
with self._tracer.start_as_current_span(name) as span:
69+
yield OpenTelemetrySegment(span)
70+
else:
71+
yield OpenTelemetrySegment(None)
72+
73+
@contextmanager
74+
def in_subsegment_async(self, name: str | None = None, **kwargs) -> Generator[BaseSegment, None, None]:
75+
name = name or "subsegment"
76+
if self._tracer is not None:
77+
with self._tracer.start_as_current_span(name) as span:
78+
yield OpenTelemetrySegment(span)
79+
else:
80+
yield OpenTelemetrySegment(None)
81+
82+
def put_annotation(self, key: str, value: str | numbers.Number | bool) -> None:
83+
try:
84+
from opentelemetry import trace
85+
86+
span = trace.get_current_span()
87+
if span and hasattr(span, "set_attribute"):
88+
span.set_attribute(key, value)
89+
except ImportError:
90+
pass
91+
92+
def put_metadata(self, key: str, value: Any, namespace: str = "default") -> None:
93+
try:
94+
from opentelemetry import trace
95+
96+
span = trace.get_current_span()
97+
if span and hasattr(span, "set_attribute"):
98+
attr_key = f"{namespace}.{key}" if namespace else key
99+
span.set_attribute(attr_key, str(value))
100+
except ImportError:
101+
pass
102+
103+
def patch(self, modules: Sequence[str]) -> None:
104+
pass
105+
106+
def patch_all(self) -> None:
107+
pass

tests/unit/test_opentelemetry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from unittest.mock import MagicMock
2+
3+
from aws_lambda_powertools.tracing import OpenTelemetryProvider, OpenTelemetrySegment, Tracer
4+
5+
6+
def test_opentelemetry_segment_attributes():
7+
mock_span = MagicMock()
8+
segment = OpenTelemetrySegment(mock_span)
9+
10+
segment.put_annotation("key_ann", "val_ann")
11+
mock_span.set_attribute.assert_called_with("key_ann", "val_ann")
12+
13+
segment.put_metadata("key_meta", {"data": 123}, namespace="test_ns")
14+
mock_span.set_attribute.assert_called_with("test_ns.key_meta", "{'data': 123}")
15+
16+
17+
def test_opentelemetry_segment_exception():
18+
mock_span = MagicMock()
19+
segment = OpenTelemetrySegment(mock_span)
20+
err = ValueError("test error")
21+
22+
segment.add_exception(err)
23+
mock_span.record_exception.assert_called_with(err)
24+
25+
26+
def test_opentelemetry_provider_subsegment():
27+
mock_tracer = MagicMock()
28+
mock_span = MagicMock()
29+
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
30+
31+
provider = OpenTelemetryProvider(tracer=mock_tracer)
32+
33+
with provider.in_subsegment("my_subsegment") as sub:
34+
assert isinstance(sub, OpenTelemetrySegment)
35+
assert sub.span == mock_span
36+
37+
mock_tracer.start_as_current_span.assert_called_with("my_subsegment")
38+
39+
40+
def test_tracer_with_opentelemetry_provider():
41+
mock_tracer = MagicMock()
42+
mock_span = MagicMock()
43+
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
44+
45+
provider = OpenTelemetryProvider(tracer=mock_tracer)
46+
tracer = Tracer(service="test-service", provider=provider, disabled=False)
47+
48+
assert tracer.provider == provider
49+
50+
@tracer.capture_method
51+
def sample_func():
52+
return "ok"
53+
54+
res = sample_func()
55+
assert res == "ok"

0 commit comments

Comments
 (0)