Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion aws_lambda_powertools/tracing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tracing utility"""

from .extensions import aiohttp_trace_config
from .opentelemetry import OpenTelemetryProvider, OpenTelemetrySegment
from .tracer import Tracer

__all__ = ["Tracer", "aiohttp_trace_config"]
__all__ = ["OpenTelemetryProvider", "OpenTelemetrySegment", "Tracer", "aiohttp_trace_config"]

107 changes: 107 additions & 0 deletions aws_lambda_powertools/tracing/opentelemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from __future__ import annotations

from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Generator, Sequence

from aws_lambda_powertools.tracing.base import BaseProvider, BaseSegment

if TYPE_CHECKING:
import numbers
import traceback


class OpenTelemetrySegment(BaseSegment):
"""Segment implementation wrapping an OpenTelemetry Span."""

def __init__(self, span: Any):
self.span = span

def close(self, end_time: int | None = None):
if self.span and hasattr(self.span, "end"):
if end_time is not None:
self.span.end(end_time=int(end_time * 1e9))
else:
self.span.end()

def add_subsegment(self, subsegment: Any):

Check failure on line 26 in aws_lambda_powertools/tracing/opentelemetry.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AZ_RezZMUkolppjiMDcy&open=AZ_RezZMUkolppjiMDcy&pullRequest=8366
pass

def remove_subsegment(self, subsegment: Any):

Check failure on line 29 in aws_lambda_powertools/tracing/opentelemetry.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AZ_RezZMUkolppjiMDcz&open=AZ_RezZMUkolppjiMDcz&pullRequest=8366
pass

def put_annotation(self, key: str, value: str | numbers.Number | bool) -> None:
if self.span and hasattr(self.span, "set_attribute"):
self.span.set_attribute(key, value)

def put_metadata(self, key: str, value: Any, namespace: str = "default") -> None:
if self.span and hasattr(self.span, "set_attribute"):
attr_key = f"{namespace}.{key}" if namespace else key
self.span.set_attribute(attr_key, str(value))

def add_exception(
self,
exception: BaseException,
stack: list[traceback.StackSummary] | None = None,
remote: bool = False,
):
if self.span and hasattr(self.span, "record_exception"):
self.span.record_exception(exception)


class OpenTelemetryProvider(BaseProvider):
"""Tracing provider utilizing OpenTelemetry for Powertools Tracer."""

def __init__(self, tracer: Any | None = None):
if tracer is None:
try:
from opentelemetry import trace

tracer = trace.get_tracer("aws_lambda_powertools")
except ImportError:
tracer = None
self._tracer = tracer

@contextmanager
def in_subsegment(self, name: str | None = None, **kwargs) -> Generator[BaseSegment, None, None]:
name = name or "subsegment"
if self._tracer is not None:
with self._tracer.start_as_current_span(name) as span:
yield OpenTelemetrySegment(span)
else:
yield OpenTelemetrySegment(None)

@contextmanager
def in_subsegment_async(self, name: str | None = None, **kwargs) -> Generator[BaseSegment, None, None]:

Check warning on line 74 in aws_lambda_powertools/tracing/opentelemetry.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Update this function so that its implementation is not identical to in_subsegment on line 65.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AZ_RezZMUkolppjiMDc0&open=AZ_RezZMUkolppjiMDc0&pullRequest=8366
name = name or "subsegment"
if self._tracer is not None:
with self._tracer.start_as_current_span(name) as span:
yield OpenTelemetrySegment(span)
else:
yield OpenTelemetrySegment(None)

def put_annotation(self, key: str, value: str | numbers.Number | bool) -> None:
try:
from opentelemetry import trace

span = trace.get_current_span()
if span and hasattr(span, "set_attribute"):
span.set_attribute(key, value)
except ImportError:
pass

def put_metadata(self, key: str, value: Any, namespace: str = "default") -> None:
try:
from opentelemetry import trace

span = trace.get_current_span()
if span and hasattr(span, "set_attribute"):
attr_key = f"{namespace}.{key}" if namespace else key
span.set_attribute(attr_key, str(value))
except ImportError:
pass

def patch(self, modules: Sequence[str]) -> None:

Check failure on line 103 in aws_lambda_powertools/tracing/opentelemetry.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AZ_RezZMUkolppjiMDc1&open=AZ_RezZMUkolppjiMDc1&pullRequest=8366
pass

def patch_all(self) -> None:

Check failure on line 106 in aws_lambda_powertools/tracing/opentelemetry.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-python&issues=AZ_RezZMUkolppjiMDc2&open=AZ_RezZMUkolppjiMDc2&pullRequest=8366
pass
55 changes: 55 additions & 0 deletions tests/unit/test_opentelemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from unittest.mock import MagicMock

from aws_lambda_powertools.tracing import OpenTelemetryProvider, OpenTelemetrySegment, Tracer


def test_opentelemetry_segment_attributes():
mock_span = MagicMock()
segment = OpenTelemetrySegment(mock_span)

segment.put_annotation("key_ann", "val_ann")
mock_span.set_attribute.assert_called_with("key_ann", "val_ann")

segment.put_metadata("key_meta", {"data": 123}, namespace="test_ns")
mock_span.set_attribute.assert_called_with("test_ns.key_meta", "{'data': 123}")


def test_opentelemetry_segment_exception():
mock_span = MagicMock()
segment = OpenTelemetrySegment(mock_span)
err = ValueError("test error")

segment.add_exception(err)
mock_span.record_exception.assert_called_with(err)


def test_opentelemetry_provider_subsegment():
mock_tracer = MagicMock()
mock_span = MagicMock()
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span

provider = OpenTelemetryProvider(tracer=mock_tracer)

with provider.in_subsegment("my_subsegment") as sub:
assert isinstance(sub, OpenTelemetrySegment)
assert sub.span == mock_span

mock_tracer.start_as_current_span.assert_called_with("my_subsegment")


def test_tracer_with_opentelemetry_provider():
mock_tracer = MagicMock()
mock_span = MagicMock()
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span

provider = OpenTelemetryProvider(tracer=mock_tracer)
tracer = Tracer(service="test-service", provider=provider, disabled=False)

assert tracer.provider == provider

@tracer.capture_method
def sample_func():
return "ok"

res = sample_func()
assert res == "ok"