From 9c4dafa206c70f06edd7bef9b0b61626d64355a6 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 7 Nov 2024 12:26:34 -0800 Subject: [PATCH 01/17] first cut for otel instrumentation --- setup.py | 1 + swagger_zipkin/otel_decorator.py | 147 +++++++++++++++++++++++++++++++ tests/otel_decorator_test.py | 68 ++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 swagger_zipkin/otel_decorator.py create mode 100644 tests/otel_decorator_test.py diff --git a/setup.py b/setup.py index 5d8f571..0352e4a 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ python_requires=">=3.7", install_requires=[ 'py_zipkin>=0.10.1', + 'opentelemetry-sdk>=0.26.1', ], keywords='zipkin', classifiers=[ diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py new file mode 100644 index 0000000..c531e86 --- /dev/null +++ b/swagger_zipkin/otel_decorator.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import os +import importlib + +from typing import Any +from typing import TYPE_CHECKING +from typing import TypeVar + +from opentelemetry import trace +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry.trace.span import TraceFlags +from opentelemetry.trace.span import format_span_id +from opentelemetry.trace.span import format_trace_id + +from typing_extensions import ParamSpec + +from swagger_zipkin.decorate_client import Client +from swagger_zipkin.decorate_client import decorate_client +from swagger_zipkin.decorate_client import Resource + + +T = TypeVar('T', covariant=True) +P = ParamSpec('P') + +if TYPE_CHECKING: + import pyramid.request.Request # type: ignore + +tracer = trace.get_tracer("otel_decorator") + + +class OtelResourceDecorator: + """A wrapper to the swagger resource. + + :param resource: A resource object. eg. `client.pet`, `client.store`. + :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource` + """ + + def __init__(self, resource: Client, client_identifier: str, smartstack_namespace: str) -> None: + self.resource = resource + self.client_identifier = client_identifier + self.smartstack_namespace = smartstack_namespace + + def __getattr__(self, name: str) -> Resource: + return decorate_client(self.resource, self.with_headers, name) + + def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: + kwargs.setdefault('_request_options', {}) + request_options: dict = kwargs['_request_options'] + request_options.setdefault('headers', {}) + + request = get_pyramid_current_request() + http_route = getattr(request, "matched_route", "") + http_request_method = getattr(request, "method", "") + + span_name = f"{http_request_method} {http_route}" + with tracer.start_as_current_span( + span_name, kind=trace.SpanKind.CLIENT + ) as span: + span.set_attribute("url.path", getattr(request, "path", "")) + span.set_attribute("http.route", http_route) + span.set_attribute("http.request.method", http_request_method) + + span.set_attribute("client.namespace", self.client_identifier) + span.set_attribute("peer.service", self.smartstack_namespace) + span.set_attribute("server.namespace", self.smartstack_namespace) + + inject_otel_headers(kwargs, current_span=span) + inject_zipkin_headers(kwargs, current_span=span) + + return getattr(self.resource, call_name)(*args, **kwargs) + + def __dir__(self) -> list[str]: + return dir(self.resource) + + +class OtelClientDecorator: + """A wrapper to swagger client (swagger-py or bravado) to pass on zipkin + headers to the service call. + + Even though client is initialised once, all the calls made will have + independent spans. + + :param client: Swagger Client + :type client: :class:`swaggerpy.client.SwaggerClient` or :class:`bravado.client.SwaggerClient`. + :param client_identifier: the name of the service that is using this + generated clientlib + :type client_identifier: string + :param smartstack_namespace: the smartstack name of the paasta instance + this generated clientlib is hitting + :type smartstack_namespace: string + """ + + def __init__(self, client: Client, client_identifier: str, smartstack_namespace: str): + self._client = client + self.client_identifier = client_identifier + self.smartstack_namespace = smartstack_namespace + + def __getattr__(self, name: str) -> Client: + return OtelResourceDecorator( + getattr(self._client, name), + client_identifier=self.client_identifier, + smartstack_namespace=self.smartstack_namespace, + ) + + def __dir__(self) -> list[str]: + return dir(self._client) # pragma: no cover + + +def inject_otel_headers( + kwargs: dict[str, Any], current_span: trace.Span +) -> None: + propagator = TraceContextTextMapPropagator() + carrier = kwargs['_request_options']["headers"] + propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) + + +def inject_zipkin_headers( + kwargs: dict[str, Any], current_span: trace.Span +) -> None: + current_span_context = current_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( + current_span_context.trace_id + ) + kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( + current_span_context.span_id + ) + parent_span = current_span.parent + if parent_span is not None: + kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( + parent_span.span_id + ) + kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( + "1" + if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) + else "0" + ) + kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" + + +def get_pyramid_current_request() -> pyramid.request.Request | None: + try: + threadlocal = importlib.import_module("pyramid.threadlocal") + except ImportError: + return None + + return threadlocal.get_current_request() diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py new file mode 100644 index 0000000..ba27b90 --- /dev/null +++ b/tests/otel_decorator_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +import os + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace.span import format_span_id +from opentelemetry.trace.span import format_trace_id + +from swagger_zipkin.decorate_client import Client +from swagger_zipkin.otel_decorator import OtelClientDecorator + +memory_exporter = InMemorySpanExporter() +span_processor = SimpleSpanProcessor(memory_exporter) +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor(span_processor) + +client_identifier = "test_client" +smartstack_namespace = "smartstack_namespace" +tracer = trace.get_tracer("otel_decorator") + +def create_request_options(parent_span: trace.Span, exported_span: trace.Span): + trace_id = format_trace_id(parent_span.get_span_context().trace_id) + span_id = format_span_id(exported_span.get_span_context().span_id) + return { + 'headers': { + 'traceparent': f'00-{trace_id}-{span_id}-01', + 'X-B3-TraceId': format_trace_id(parent_span.get_span_context().trace_id), + 'X-B3-SpanId': format_span_id(exported_span.get_span_context().span_id), + 'X-B3-ParentSpanId': format_span_id(parent_span.get_span_context().span_id), + 'X-B3-Flags': '0', + 'X-B3-Sampled': '1', + } + } + +def test_client_request(): + client = mock.Mock() + wrapped_client = OtelClientDecorator( + client, client_identifier=client_identifier, smartstack_namespace=smartstack_namespace + ) + + with tracer.start_as_current_span( + "parent_span", kind=trace.SpanKind.SERVER + ) as parent_span: + resource = wrapped_client.resource + param = mock.Mock() + resource.operation(param) + + assert len(memory_exporter.get_finished_spans()) == 1 + exported_span = memory_exporter.get_finished_spans()[0] + + client.resource.operation.assert_called_with( + param, + _request_options=create_request_options(parent_span, exported_span) + ) + + assert exported_span.attributes["url.path"] == "" + assert exported_span.attributes["http.request.method"] == "" + assert exported_span.attributes["http.route"] == "" + assert exported_span.attributes["client.namespace"] == client_identifier + assert exported_span.attributes["peer.service"] == smartstack_namespace + assert exported_span.attributes["server.namespace"] == smartstack_namespace + #assert exported_span.attributes["http.response.status_code"] == "" + + + From bcb08b1c50de902121237d82b4facdb0f13aa9ed Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 7 Nov 2024 18:31:59 -0800 Subject: [PATCH 02/17] no parent span --- swagger_zipkin/otel_decorator.py | 22 ++++++++-------------- tests/otel_decorator_test.py | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index c531e86..5e7e744 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -1,30 +1,28 @@ from __future__ import annotations -import os import importlib from typing import Any -from typing import TYPE_CHECKING from typing import TypeVar from opentelemetry import trace from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator -from opentelemetry.trace.span import TraceFlags from opentelemetry.trace.span import format_span_id from opentelemetry.trace.span import format_trace_id - +from opentelemetry.trace.span import TraceFlags from typing_extensions import ParamSpec from swagger_zipkin.decorate_client import Client from swagger_zipkin.decorate_client import decorate_client from swagger_zipkin.decorate_client import Resource +# from typing import TYPE_CHECKING T = TypeVar('T', covariant=True) P = ParamSpec('P') -if TYPE_CHECKING: - import pyramid.request.Request # type: ignore +# if TYPE_CHECKING: +import pyramid.request.Request # type: ignore tracer = trace.get_tracer("otel_decorator") @@ -52,15 +50,15 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: request = get_pyramid_current_request() http_route = getattr(request, "matched_route", "") http_request_method = getattr(request, "method", "") - + span_name = f"{http_request_method} {http_route}" with tracer.start_as_current_span( span_name, kind=trace.SpanKind.CLIENT - ) as span: + ) as span: span.set_attribute("url.path", getattr(request, "path", "")) span.set_attribute("http.route", http_route) span.set_attribute("http.request.method", http_request_method) - + span.set_attribute("client.namespace", self.client_identifier) span.set_attribute("peer.service", self.smartstack_namespace) span.set_attribute("server.namespace", self.smartstack_namespace) @@ -125,11 +123,7 @@ def inject_zipkin_headers( kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( current_span_context.span_id ) - parent_span = current_span.parent - if parent_span is not None: - kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( - parent_span.span_id - ) + kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( "1" if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index ba27b90..4e22f38 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -1,7 +1,5 @@ from unittest import mock -import os - from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor @@ -21,6 +19,7 @@ smartstack_namespace = "smartstack_namespace" tracer = trace.get_tracer("otel_decorator") + def create_request_options(parent_span: trace.Span, exported_span: trace.Span): trace_id = format_trace_id(parent_span.get_span_context().trace_id) span_id = format_span_id(exported_span.get_span_context().span_id) @@ -35,27 +34,30 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): } } + def test_client_request(): - client = mock.Mock() + client = mock.Mock(spec=Client) wrapped_client = OtelClientDecorator( - client, client_identifier=client_identifier, smartstack_namespace=smartstack_namespace + client, + client_identifier=client_identifier, + smartstack_namespace=smartstack_namespace ) - + with tracer.start_as_current_span( "parent_span", kind=trace.SpanKind.SERVER ) as parent_span: resource = wrapped_client.resource param = mock.Mock() resource.operation(param) - + assert len(memory_exporter.get_finished_spans()) == 1 exported_span = memory_exporter.get_finished_spans()[0] - + client.resource.operation.assert_called_with( param, _request_options=create_request_options(parent_span, exported_span) ) - + assert exported_span.attributes["url.path"] == "" assert exported_span.attributes["http.request.method"] == "" assert exported_span.attributes["http.route"] == "" @@ -63,6 +65,3 @@ def test_client_request(): assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace #assert exported_span.attributes["http.response.status_code"] == "" - - - From 2fd9b592fce658c3dd1359bc5924ec40ced42a45 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Fri, 8 Nov 2024 15:23:07 -0800 Subject: [PATCH 03/17] status code --- swagger_zipkin/otel_decorator.py | 8 ++++---- tests/otel_decorator_test.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 5e7e744..73e938f 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -1,8 +1,8 @@ from __future__ import annotations import importlib - from typing import Any +from typing import TYPE_CHECKING from typing import TypeVar from opentelemetry import trace @@ -15,14 +15,13 @@ from swagger_zipkin.decorate_client import Client from swagger_zipkin.decorate_client import decorate_client from swagger_zipkin.decorate_client import Resource -# from typing import TYPE_CHECKING T = TypeVar('T', covariant=True) P = ParamSpec('P') -# if TYPE_CHECKING: -import pyramid.request.Request # type: ignore +if TYPE_CHECKING: + import pyramid.request.Request # type: ignore tracer = trace.get_tracer("otel_decorator") @@ -62,6 +61,7 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: span.set_attribute("client.namespace", self.client_identifier) span.set_attribute("peer.service", self.smartstack_namespace) span.set_attribute("server.namespace", self.smartstack_namespace) + span.set_attribute("http.response.status_code", "200") inject_otel_headers(kwargs, current_span=span) inject_zipkin_headers(kwargs, current_span=span) diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 4e22f38..5d40c51 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -7,7 +7,6 @@ from opentelemetry.trace.span import format_span_id from opentelemetry.trace.span import format_trace_id -from swagger_zipkin.decorate_client import Client from swagger_zipkin.otel_decorator import OtelClientDecorator memory_exporter = InMemorySpanExporter() @@ -28,7 +27,6 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): 'traceparent': f'00-{trace_id}-{span_id}-01', 'X-B3-TraceId': format_trace_id(parent_span.get_span_context().trace_id), 'X-B3-SpanId': format_span_id(exported_span.get_span_context().span_id), - 'X-B3-ParentSpanId': format_span_id(parent_span.get_span_context().span_id), 'X-B3-Flags': '0', 'X-B3-Sampled': '1', } @@ -36,10 +34,10 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): def test_client_request(): - client = mock.Mock(spec=Client) + client = mock.Mock() wrapped_client = OtelClientDecorator( client, - client_identifier=client_identifier, + client_identifier=client_identifier, smartstack_namespace=smartstack_namespace ) @@ -64,4 +62,4 @@ def test_client_request(): assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace - #assert exported_span.attributes["http.response.status_code"] == "" + assert exported_span.attributes["http.response.status_code"] == "200" From fe45923d5e12c8a51c1b3cf07d09e1fa971b533c Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Sat, 9 Nov 2024 09:07:16 -0800 Subject: [PATCH 04/17] handle exception --- swagger_zipkin/otel_decorator.py | 47 ++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 73e938f..0118879 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib +from contextlib import contextmanager from typing import Any from typing import TYPE_CHECKING from typing import TypeVar @@ -10,19 +11,19 @@ from opentelemetry.trace.span import format_span_id from opentelemetry.trace.span import format_trace_id from opentelemetry.trace.span import TraceFlags + from typing_extensions import ParamSpec from swagger_zipkin.decorate_client import Client from swagger_zipkin.decorate_client import decorate_client from swagger_zipkin.decorate_client import Resource +if TYPE_CHECKING: + import pyramid.request.Request # type: ignore T = TypeVar('T', covariant=True) P = ParamSpec('P') -if TYPE_CHECKING: - import pyramid.request.Request # type: ignore - tracer = trace.get_tracer("otel_decorator") @@ -32,7 +33,6 @@ class OtelResourceDecorator: :param resource: A resource object. eg. `client.pet`, `client.store`. :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource` """ - def __init__(self, resource: Client, client_identifier: str, smartstack_namespace: str) -> None: self.resource = resource self.client_identifier = client_identifier @@ -54,19 +54,32 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: with tracer.start_as_current_span( span_name, kind=trace.SpanKind.CLIENT ) as span: - span.set_attribute("url.path", getattr(request, "path", "")) - span.set_attribute("http.route", http_route) - span.set_attribute("http.request.method", http_request_method) - - span.set_attribute("client.namespace", self.client_identifier) - span.set_attribute("peer.service", self.smartstack_namespace) - span.set_attribute("server.namespace", self.smartstack_namespace) - span.set_attribute("http.response.status_code", "200") - - inject_otel_headers(kwargs, current_span=span) - inject_zipkin_headers(kwargs, current_span=span) - - return getattr(self.resource, call_name)(*args, **kwargs) + with self.handle_exception(): + span.set_attribute("url.path", getattr(request, "path", "")) + span.set_attribute("http.route", http_route) + span.set_attribute("http.request.method", http_request_method) + + span.set_attribute("client.namespace", self.client_identifier) + span.set_attribute("peer.service", self.smartstack_namespace) + span.set_attribute("server.namespace", self.smartstack_namespace) + span.set_attribute("http.response.status_code", "200") + + inject_otel_headers(kwargs, current_span=span) + inject_zipkin_headers(kwargs, current_span=span) + + return getattr(self.resource, call_name)(*args, **kwargs) + + @contextmanager + def handle_exception( + self, + ) -> Any: + try: + yield + except Exception as e: + span = trace.get_current_span() + span.set_attribute("error.type", e.__class__.__name__) + span.set_attribute("http.response.status_code", "500") + raise e def __dir__(self) -> list[str]: return dir(self.resource) From ff7dbfa60620991ac15f0c4fba546e07d58f75cf Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Sat, 9 Nov 2024 10:42:57 -0800 Subject: [PATCH 05/17] get_request test --- swagger_zipkin/otel_decorator.py | 24 ++++++++++++------------ tests/otel_decorator_test.py | 24 ++++++++++++++++++++---- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 0118879..41d9d35 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -11,7 +11,6 @@ from opentelemetry.trace.span import format_span_id from opentelemetry.trace.span import format_trace_id from opentelemetry.trace.span import TraceFlags - from typing_extensions import ParamSpec from swagger_zipkin.decorate_client import Client @@ -33,6 +32,7 @@ class OtelResourceDecorator: :param resource: A resource object. eg. `client.pet`, `client.store`. :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource` """ + def __init__(self, resource: Client, client_identifier: str, smartstack_namespace: str) -> None: self.resource = resource self.client_identifier = client_identifier @@ -55,19 +55,19 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: span_name, kind=trace.SpanKind.CLIENT ) as span: with self.handle_exception(): - span.set_attribute("url.path", getattr(request, "path", "")) - span.set_attribute("http.route", http_route) - span.set_attribute("http.request.method", http_request_method) + span.set_attribute("url.path", getattr(request, "path", "")) + span.set_attribute("http.route", http_route) + span.set_attribute("http.request.method", http_request_method) - span.set_attribute("client.namespace", self.client_identifier) - span.set_attribute("peer.service", self.smartstack_namespace) - span.set_attribute("server.namespace", self.smartstack_namespace) - span.set_attribute("http.response.status_code", "200") + span.set_attribute("client.namespace", self.client_identifier) + span.set_attribute("peer.service", self.smartstack_namespace) + span.set_attribute("server.namespace", self.smartstack_namespace) + span.set_attribute("http.response.status_code", "200") - inject_otel_headers(kwargs, current_span=span) - inject_zipkin_headers(kwargs, current_span=span) + inject_otel_headers(kwargs, current_span=span) + inject_zipkin_headers(kwargs, current_span=span) - return getattr(self.resource, call_name)(*args, **kwargs) + return getattr(self.resource, call_name)(*args, **kwargs) @contextmanager def handle_exception( @@ -82,7 +82,7 @@ def handle_exception( raise e def __dir__(self) -> list[str]: - return dir(self.resource) + return dir(self.resource) # pragma: no cover class OtelClientDecorator: diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 5d40c51..8b696ef 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -1,5 +1,6 @@ from unittest import mock +import pytest from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor @@ -19,6 +20,15 @@ tracer = trace.get_tracer("otel_decorator") +@pytest.fixture +def get_request(): + mock_request = mock.Mock() + mock_request.path = "/sample-url" + mock_request.method = "GET" + mock_request.matched_route = "sample-view" + return mock_request + + def create_request_options(parent_span: trace.Span, exported_span: trace.Span): trace_id = format_trace_id(parent_span.get_span_context().trace_id) span_id = format_span_id(exported_span.get_span_context().span_id) @@ -33,7 +43,12 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): } -def test_client_request(): +@mock.patch( + "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True +) +def test_client_request(mock_request, get_request): + mock_request.return_value = get_request + client = mock.Mock() wrapped_client = OtelClientDecorator( client, @@ -56,9 +71,10 @@ def test_client_request(): _request_options=create_request_options(parent_span, exported_span) ) - assert exported_span.attributes["url.path"] == "" - assert exported_span.attributes["http.request.method"] == "" - assert exported_span.attributes["http.route"] == "" + assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.attributes["url.path"] == get_request.path + assert exported_span.attributes["http.request.method"] == get_request.method + assert exported_span.attributes["http.route"] == get_request.matched_route assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace From 5570a13e9bf3a0e4e78cb102350a296a01661bc2 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Wed, 13 Nov 2024 11:21:45 -0800 Subject: [PATCH 06/17] check for decoarator exception --- swagger_zipkin/otel_decorator.py | 10 ++++--- tests/otel_decorator_test.py | 48 +++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 41d9d35..cbcb64e 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -18,7 +18,8 @@ from swagger_zipkin.decorate_client import Resource if TYPE_CHECKING: - import pyramid.request.Request # type: ignore + # pragma: no cover + import pyramid.request.Request # type: ignore[import-untyped] T = TypeVar('T', covariant=True) P = ParamSpec('P') @@ -54,6 +55,9 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: with tracer.start_as_current_span( span_name, kind=trace.SpanKind.CLIENT ) as span: + inject_otel_headers(kwargs, current_span=span) + inject_zipkin_headers(kwargs, current_span=span) + with self.handle_exception(): span.set_attribute("url.path", getattr(request, "path", "")) span.set_attribute("http.route", http_route) @@ -64,9 +68,6 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: span.set_attribute("server.namespace", self.smartstack_namespace) span.set_attribute("http.response.status_code", "200") - inject_otel_headers(kwargs, current_span=span) - inject_zipkin_headers(kwargs, current_span=span) - return getattr(self.resource, call_name)(*args, **kwargs) @contextmanager @@ -145,6 +146,7 @@ def inject_zipkin_headers( kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" +# pragma: no cover def get_pyramid_current_request() -> pyramid.request.Request | None: try: threadlocal = importlib.import_module("pyramid.threadlocal") diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 8b696ef..266f78c 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -9,6 +9,7 @@ from opentelemetry.trace.span import format_trace_id from swagger_zipkin.otel_decorator import OtelClientDecorator +from swagger_zipkin.otel_decorator import OtelResourceDecorator memory_exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(memory_exporter) @@ -49,16 +50,15 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): def test_client_request(mock_request, get_request): mock_request.return_value = get_request - client = mock.Mock() - wrapped_client = OtelClientDecorator( - client, - client_identifier=client_identifier, - smartstack_namespace=smartstack_namespace - ) - with tracer.start_as_current_span( "parent_span", kind=trace.SpanKind.SERVER ) as parent_span: + client = mock.Mock() + wrapped_client = OtelClientDecorator( + client, + client_identifier=client_identifier, + smartstack_namespace=smartstack_namespace + ) resource = wrapped_client.resource param = mock.Mock() resource.operation(param) @@ -79,3 +79,37 @@ def test_client_request(mock_request, get_request): assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace assert exported_span.attributes["http.response.status_code"] == "200" + + memory_exporter.clear() + + +@mock.patch( + "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True +) +def test_with_headers_exception(mock_request, get_request): + mock_request.return_value = get_request + + # Create a mock resource and configure it to raise an exception + mock_resource = mock.MagicMock() + mock_method = mock.MagicMock(side_effect=Exception("Simulated exception")) + setattr(mock_resource, 'test_call', mock_method) + + decorator = OtelResourceDecorator(resource=mock_resource, client_identifier="test_client", + smartstack_namespace="smartstack_namespace") + + with pytest.raises(Exception): + decorator.with_headers("test_call") + + assert len(memory_exporter.get_finished_spans()) == 1 + exported_span = memory_exporter.get_finished_spans()[0] + + assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.attributes["url.path"] == get_request.path + assert exported_span.attributes["http.request.method"] == get_request.method + assert exported_span.attributes["http.route"] == get_request.matched_route + assert exported_span.attributes["client.namespace"] == client_identifier + assert exported_span.attributes["peer.service"] == smartstack_namespace + assert exported_span.attributes["server.namespace"] == smartstack_namespace + assert exported_span.attributes["http.response.status_code"] == "500" + + memory_exporter.clear() From a1e18abef140be5f71368fe6953d36c5dd8997b5 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Wed, 13 Nov 2024 12:16:07 -0800 Subject: [PATCH 07/17] parent_span header --- swagger_zipkin/otel_decorator.py | 11 ++++-- tests/otel_decorator_test.py | 58 +++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index cbcb64e..b1d0ddc 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -51,12 +51,13 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: http_route = getattr(request, "matched_route", "") http_request_method = getattr(request, "method", "") + parent_span = trace.get_current_span() span_name = f"{http_request_method} {http_route}" with tracer.start_as_current_span( span_name, kind=trace.SpanKind.CLIENT ) as span: - inject_otel_headers(kwargs, current_span=span) - inject_zipkin_headers(kwargs, current_span=span) + inject_otel_headers(kwargs, span) + inject_zipkin_headers(kwargs, span, parent_span) with self.handle_exception(): span.set_attribute("url.path", getattr(request, "path", "")) @@ -128,7 +129,7 @@ def inject_otel_headers( def inject_zipkin_headers( - kwargs: dict[str, Any], current_span: trace.Span + kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span ) -> None: current_span_context = current_span.get_span_context() kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( @@ -137,6 +138,10 @@ def inject_zipkin_headers( kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( current_span_context.span_id ) + if parent_span is not None and parent_span.is_recording(): + parent_span_context = parent_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( + parent_span_context.span_id) kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( "1" diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 266f78c..1beb806 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -31,17 +31,21 @@ def get_request(): def create_request_options(parent_span: trace.Span, exported_span: trace.Span): - trace_id = format_trace_id(parent_span.get_span_context().trace_id) + trace_id = format_trace_id(exported_span.get_span_context().trace_id) span_id = format_span_id(exported_span.get_span_context().span_id) - return { - 'headers': { - 'traceparent': f'00-{trace_id}-{span_id}-01', - 'X-B3-TraceId': format_trace_id(parent_span.get_span_context().trace_id), - 'X-B3-SpanId': format_span_id(exported_span.get_span_context().span_id), - 'X-B3-Flags': '0', - 'X-B3-Sampled': '1', - } + + headers = {} + headers['headers'] = { + 'traceparent': f'00-{trace_id}-{span_id}-01', + 'X-B3-TraceId': trace_id, + 'X-B3-SpanId': span_id, + 'X-B3-Flags': '0', + 'X-B3-Sampled': '1', } + if parent_span is not None: + headers['headers']['X-B3-ParentSpanId'] = format_span_id(parent_span.get_span_context().span_id) + + return headers @mock.patch( @@ -83,6 +87,42 @@ def test_client_request(mock_request, get_request): memory_exporter.clear() +@mock.patch( + "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True +) +def test_client_request_no_parent_span(mock_request, get_request): + mock_request.return_value = get_request + + client = mock.Mock() + wrapped_client = OtelClientDecorator( + client, + client_identifier=client_identifier, + smartstack_namespace=smartstack_namespace + ) + resource = wrapped_client.resource + param = mock.Mock() + resource.operation(param) + + assert len(memory_exporter.get_finished_spans()) == 1 + exported_span = memory_exporter.get_finished_spans()[0] + + client.resource.operation.assert_called_with( + param, + _request_options=create_request_options(None, exported_span) + ) + + assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.attributes["url.path"] == get_request.path + assert exported_span.attributes["http.request.method"] == get_request.method + assert exported_span.attributes["http.route"] == get_request.matched_route + assert exported_span.attributes["client.namespace"] == client_identifier + assert exported_span.attributes["peer.service"] == smartstack_namespace + assert exported_span.attributes["server.namespace"] == smartstack_namespace + assert exported_span.attributes["http.response.status_code"] == "200" + + memory_exporter.clear() + + @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True ) From e4e067bd9f2238ed927b55bc77d3c5f7ff8d858a Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Wed, 13 Nov 2024 12:21:36 -0800 Subject: [PATCH 08/17] fixture to clear memory exporter --- tests/otel_decorator_test.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 1beb806..b289ef0 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -21,6 +21,11 @@ tracer = trace.get_tracer("otel_decorator") +@pytest.fixture +def setup(): + memory_exporter.clear() + + @pytest.fixture def get_request(): mock_request = mock.Mock() @@ -51,7 +56,7 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True ) -def test_client_request(mock_request, get_request): +def test_client_request(mock_request, get_request, setup): mock_request.return_value = get_request with tracer.start_as_current_span( @@ -84,13 +89,11 @@ def test_client_request(mock_request, get_request): assert exported_span.attributes["server.namespace"] == smartstack_namespace assert exported_span.attributes["http.response.status_code"] == "200" - memory_exporter.clear() - @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True ) -def test_client_request_no_parent_span(mock_request, get_request): +def test_client_request_no_parent_span(mock_request, get_request, setup): mock_request.return_value = get_request client = mock.Mock() @@ -120,13 +123,11 @@ def test_client_request_no_parent_span(mock_request, get_request): assert exported_span.attributes["server.namespace"] == smartstack_namespace assert exported_span.attributes["http.response.status_code"] == "200" - memory_exporter.clear() - @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True ) -def test_with_headers_exception(mock_request, get_request): +def test_with_headers_exception(mock_request, get_request, setup): mock_request.return_value = get_request # Create a mock resource and configure it to raise an exception @@ -151,5 +152,3 @@ def test_with_headers_exception(mock_request, get_request): assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace assert exported_span.attributes["http.response.status_code"] == "500" - - memory_exporter.clear() From 6c846893ab402c8bafe95fddedb3e9c23a7f0c61 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Wed, 13 Nov 2024 13:27:11 -0800 Subject: [PATCH 09/17] self the inject header functions --- swagger_zipkin/otel_decorator.py | 66 +++++++++++++++++--------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index b1d0ddc..8f59190 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -56,9 +56,13 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: with tracer.start_as_current_span( span_name, kind=trace.SpanKind.CLIENT ) as span: - inject_otel_headers(kwargs, span) - inject_zipkin_headers(kwargs, span, parent_span) + self.inject_otel_headers(kwargs, span) + self.inject_zipkin_headers(kwargs, span, parent_span) + # ideally wrap with_headers with exception and catch a specific + # exception - HTTPError? and general exception in handle_exception. + # This would create a dependency on bravado package. Is this ok? + # https://github.com/Yelp/bravado/blob/master/bravado/exception.py with self.handle_exception(): span.set_attribute("url.path", getattr(request, "path", "")) span.set_attribute("http.route", http_route) @@ -120,35 +124,35 @@ def __dir__(self) -> list[str]: return dir(self._client) # pragma: no cover -def inject_otel_headers( - kwargs: dict[str, Any], current_span: trace.Span -) -> None: - propagator = TraceContextTextMapPropagator() - carrier = kwargs['_request_options']["headers"] - propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) - - -def inject_zipkin_headers( - kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span -) -> None: - current_span_context = current_span.get_span_context() - kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( - current_span_context.trace_id - ) - kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( - current_span_context.span_id - ) - if parent_span is not None and parent_span.is_recording(): - parent_span_context = parent_span.get_span_context() - kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( - parent_span_context.span_id) - - kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( - "1" - if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) - else "0" - ) - kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" + def inject_otel_headers( + self, kwargs: dict[str, Any], current_span: trace.Span + ) -> None: + propagator = TraceContextTextMapPropagator() + carrier = kwargs['_request_options']["headers"] + propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) + + + def inject_zipkin_headers( + self, kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span + ) -> None: + current_span_context = current_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( + current_span_context.trace_id + ) + kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( + current_span_context.span_id + ) + if parent_span is not None and parent_span.is_recording(): + parent_span_context = parent_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( + parent_span_context.span_id) + + kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( + "1" + if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) + else "0" + ) + kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" # pragma: no cover From 7709ebc38efdb688f0299a970fffc0ba17d609b2 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Wed, 13 Nov 2024 14:25:51 -0800 Subject: [PATCH 10/17] self the inject header functions --- swagger_zipkin/otel_decorator.py | 65 +++++++++++++++++--------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 8f59190..578eb6b 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -59,9 +59,10 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: self.inject_otel_headers(kwargs, span) self.inject_zipkin_headers(kwargs, span, parent_span) - # ideally wrap with_headers with exception and catch a specific - # exception - HTTPError? and general exception in handle_exception. - # This would create a dependency on bravado package. Is this ok? + # ideally the exception should be scoped for self.with_headers + # handle_exception should handle general excetion and the specific exception HTTPError + # But this would create a dependency on bravado package. Is this ok? + # Assuming resource.operation does throw HTTPError # https://github.com/Yelp/bravado/blob/master/bravado/exception.py with self.handle_exception(): span.set_attribute("url.path", getattr(request, "path", "")) @@ -90,6 +91,36 @@ def handle_exception( def __dir__(self) -> list[str]: return dir(self.resource) # pragma: no cover + def inject_otel_headers( + self, kwargs: dict[str, Any], current_span: trace.Span + ) -> None: + propagator = TraceContextTextMapPropagator() + carrier = kwargs['_request_options']["headers"] + propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) + + + def inject_zipkin_headers( + self, kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span + ) -> None: + current_span_context = current_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( + current_span_context.trace_id + ) + kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( + current_span_context.span_id + ) + if parent_span is not None and parent_span.is_recording(): + parent_span_context = parent_span.get_span_context() + kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( + parent_span_context.span_id) + + kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( + "1" + if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) + else "0" + ) + kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" + class OtelClientDecorator: """A wrapper to swagger client (swagger-py or bravado) to pass on zipkin @@ -124,35 +155,7 @@ def __dir__(self) -> list[str]: return dir(self._client) # pragma: no cover - def inject_otel_headers( - self, kwargs: dict[str, Any], current_span: trace.Span - ) -> None: - propagator = TraceContextTextMapPropagator() - carrier = kwargs['_request_options']["headers"] - propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) - - def inject_zipkin_headers( - self, kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span - ) -> None: - current_span_context = current_span.get_span_context() - kwargs["_request_options"]["headers"]["X-B3-TraceId"] = format_trace_id( - current_span_context.trace_id - ) - kwargs["_request_options"]["headers"]["X-B3-SpanId"] = format_span_id( - current_span_context.span_id - ) - if parent_span is not None and parent_span.is_recording(): - parent_span_context = parent_span.get_span_context() - kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( - parent_span_context.span_id) - - kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( - "1" - if (current_span_context.trace_flags & TraceFlags.SAMPLED == TraceFlags.SAMPLED) - else "0" - ) - kwargs["_request_options"]["headers"]["X-B3-Flags"] = "0" # pragma: no cover From 2b60a7d7bccff9a2e2a5f2fe05832cbc761d6f72 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 14 Nov 2024 05:43:10 -0800 Subject: [PATCH 11/17] multiple operation calls --- tests/otel_decorator_test.py | 39 +++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index b289ef0..a02ed26 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -69,14 +69,14 @@ def test_client_request(mock_request, get_request, setup): smartstack_namespace=smartstack_namespace ) resource = wrapped_client.resource - param = mock.Mock() - resource.operation(param) + param1 = mock.Mock() + resource.operation(param1) assert len(memory_exporter.get_finished_spans()) == 1 exported_span = memory_exporter.get_finished_spans()[0] client.resource.operation.assert_called_with( - param, + param1, _request_options=create_request_options(parent_span, exported_span) ) @@ -89,6 +89,28 @@ def test_client_request(mock_request, get_request, setup): assert exported_span.attributes["server.namespace"] == smartstack_namespace assert exported_span.attributes["http.response.status_code"] == "200" + param2 = mock.Mock() + resource.operation(param2) + + assert len(memory_exporter.get_finished_spans()) == 2 + exported_span = memory_exporter.get_finished_spans()[1] + + client.resource.operation.assert_called_with( + param2, + _request_options=create_request_options(parent_span, exported_span) + ) + + assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.attributes["url.path"] == get_request.path + assert exported_span.attributes["http.request.method"] == get_request.method + assert exported_span.attributes["http.route"] == get_request.matched_route + assert exported_span.attributes["client.namespace"] == client_identifier + assert exported_span.attributes["peer.service"] == smartstack_namespace + assert exported_span.attributes["server.namespace"] == smartstack_namespace + assert exported_span.attributes["http.response.status_code"] == "200" + + + @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True @@ -133,17 +155,23 @@ def test_with_headers_exception(mock_request, get_request, setup): # Create a mock resource and configure it to raise an exception mock_resource = mock.MagicMock() mock_method = mock.MagicMock(side_effect=Exception("Simulated exception")) - setattr(mock_resource, 'test_call', mock_method) + setattr(mock_resource, 'test_operation', mock_method) decorator = OtelResourceDecorator(resource=mock_resource, client_identifier="test_client", smartstack_namespace="smartstack_namespace") with pytest.raises(Exception): - decorator.with_headers("test_call") + decorator.with_headers("test_operation") + assert len(memory_exporter.get_finished_spans()) == 1 exported_span = memory_exporter.get_finished_spans()[0] + mock_resource.operation.assert_called_with( + "test_operation", + _request_options=create_request_options(None, exported_span) + ) + assert exported_span.name == f"{get_request.method} {get_request.matched_route}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method @@ -151,4 +179,5 @@ def test_with_headers_exception(mock_request, get_request, setup): assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace + assert exported_span.attributes["error.type"] == "Exception" assert exported_span.attributes["http.response.status_code"] == "500" From 2787ca2782d05073950e8b1e3ab835d5fb42d057 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 14 Nov 2024 06:37:33 -0800 Subject: [PATCH 12/17] verify headers for exceptions --- tests/otel_decorator_test.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index a02ed26..ecd1c89 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -154,23 +154,25 @@ def test_with_headers_exception(mock_request, get_request, setup): # Create a mock resource and configure it to raise an exception mock_resource = mock.MagicMock() - mock_method = mock.MagicMock(side_effect=Exception("Simulated exception")) + mock_method = mock.MagicMock(side_effect=Exception("simulated exception")) setattr(mock_resource, 'test_operation', mock_method) decorator = OtelResourceDecorator(resource=mock_resource, client_identifier="test_client", smartstack_namespace="smartstack_namespace") - with pytest.raises(Exception): - decorator.with_headers("test_operation") + # Prepare arguments + args = () + kwargs = {'_request_options': {'headers': {}}} + with pytest.raises(Exception): + decorator.with_headers("test_operation", *args, **kwargs) assert len(memory_exporter.get_finished_spans()) == 1 exported_span = memory_exporter.get_finished_spans()[0] - mock_resource.operation.assert_called_with( - "test_operation", - _request_options=create_request_options(None, exported_span) - ) + expected_headers = kwargs['_request_options']['headers'] + actual_headers = create_request_options(None, exported_span)['headers'] + assert expected_headers == actual_headers assert exported_span.name == f"{get_request.method} {get_request.matched_route}" assert exported_span.attributes["url.path"] == get_request.path From 04f363389d83caa2aacccd2cbc35cf7fb26f8946 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 14 Nov 2024 07:00:09 -0800 Subject: [PATCH 13/17] get span kind CLIENT --- swagger_zipkin/otel_decorator.py | 4 ++-- tests/otel_decorator_test.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 578eb6b..34f0be1 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -123,8 +123,8 @@ def inject_zipkin_headers( class OtelClientDecorator: - """A wrapper to swagger client (swagger-py or bravado) to pass on zipkin - headers to the service call. + """A wrapper to swagger client (swagger-py or bravado) to pass on otel and zipkin + headers to the service call. It will also generate a CLIENT span for the outgoing call. Even though client is initialised once, all the calls made will have independent spans. diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index ecd1c89..fec0cd4 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -2,6 +2,7 @@ import pytest from opentelemetry import trace +from opentelemetry.trace import SpanKind from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter @@ -80,6 +81,7 @@ def test_client_request(mock_request, get_request, setup): _request_options=create_request_options(parent_span, exported_span) ) + assert exported_span.kind == SpanKind.CLIENT assert exported_span.name == f"{get_request.method} {get_request.matched_route}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method @@ -136,6 +138,7 @@ def test_client_request_no_parent_span(mock_request, get_request, setup): _request_options=create_request_options(None, exported_span) ) + assert exported_span.kind == SpanKind.CLIENT assert exported_span.name == f"{get_request.method} {get_request.matched_route}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method @@ -174,6 +177,7 @@ def test_with_headers_exception(mock_request, get_request, setup): actual_headers = create_request_options(None, exported_span)['headers'] assert expected_headers == actual_headers + assert exported_span.kind == SpanKind.CLIENT assert exported_span.name == f"{get_request.method} {get_request.matched_route}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method From c328e36f86e9f434b5c4b99b86309f1381cfc40b Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 14 Nov 2024 07:39:46 -0800 Subject: [PATCH 14/17] how to get the right request object --- swagger_zipkin/otel_decorator.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 34f0be1..03c1967 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -18,8 +18,7 @@ from swagger_zipkin.decorate_client import Resource if TYPE_CHECKING: - # pragma: no cover - import pyramid.request.Request # type: ignore[import-untyped] + import pyramid.request.Request # noqa: F401 T = TypeVar('T', covariant=True) P = ParamSpec('P') @@ -47,6 +46,9 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: request_options: dict = kwargs['_request_options'] request_options.setdefault('headers', {}) + # what is the right way to get the Request object. can we use contruct_request + # https://github.com/Yelp/bravado/blob/master/bravado/client.py#L283C5-L283C22 + # this would create a bravado dependency. request = get_pyramid_current_request() http_route = getattr(request, "matched_route", "") http_request_method = getattr(request, "method", "") @@ -155,10 +157,6 @@ def __dir__(self) -> list[str]: return dir(self._client) # pragma: no cover - - - -# pragma: no cover def get_pyramid_current_request() -> pyramid.request.Request | None: try: threadlocal = importlib.import_module("pyramid.threadlocal") From c3dc6e896d39af4cd25589f05d0109d8d1cca911 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Thu, 14 Nov 2024 08:40:53 -0800 Subject: [PATCH 15/17] ignore pyramid request --- swagger_zipkin/otel_decorator.py | 11 +++++------ tests/otel_decorator_test.py | 8 +++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 03c1967..89063ad 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -18,7 +18,7 @@ from swagger_zipkin.decorate_client import Resource if TYPE_CHECKING: - import pyramid.request.Request # noqa: F401 + import pyramid.request.Request # type: ignore # noqa: F401 T = TypeVar('T', covariant=True) P = ParamSpec('P') @@ -48,7 +48,7 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: # what is the right way to get the Request object. can we use contruct_request # https://github.com/Yelp/bravado/blob/master/bravado/client.py#L283C5-L283C22 - # this would create a bravado dependency. + # this would create a bravado dependency. request = get_pyramid_current_request() http_route = getattr(request, "matched_route", "") http_request_method = getattr(request, "method", "") @@ -61,8 +61,8 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: self.inject_otel_headers(kwargs, span) self.inject_zipkin_headers(kwargs, span, parent_span) - # ideally the exception should be scoped for self.with_headers - # handle_exception should handle general excetion and the specific exception HTTPError + # ideally the exception should be scoped for self.with_headers + # handle_exception should handle general excetion and the specific exception HTTPError # But this would create a dependency on bravado package. Is this ok? # Assuming resource.operation does throw HTTPError # https://github.com/Yelp/bravado/blob/master/bravado/exception.py @@ -100,7 +100,6 @@ def inject_otel_headers( carrier = kwargs['_request_options']["headers"] propagator.inject(carrier=carrier, context=trace.set_span_in_context(current_span)) - def inject_zipkin_headers( self, kwargs: dict[str, Any], current_span: trace.Span, parent_span: trace.Span ) -> None: @@ -114,7 +113,7 @@ def inject_zipkin_headers( if parent_span is not None and parent_span.is_recording(): parent_span_context = parent_span.get_span_context() kwargs["_request_options"]["headers"]["X-B3-ParentSpanId"] = format_span_id( - parent_span_context.span_id) + parent_span_context.span_id) kwargs["_request_options"]["headers"]["X-B3-Sampled"] = ( "1" diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index fec0cd4..db55975 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -2,10 +2,10 @@ import pytest from opentelemetry import trace -from opentelemetry.trace import SpanKind from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import SpanKind from opentelemetry.trace.span import format_span_id from opentelemetry.trace.span import format_trace_id @@ -50,7 +50,7 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): } if parent_span is not None: headers['headers']['X-B3-ParentSpanId'] = format_span_id(parent_span.get_span_context().span_id) - + return headers @@ -112,8 +112,6 @@ def test_client_request(mock_request, get_request, setup): assert exported_span.attributes["http.response.status_code"] == "200" - - @mock.patch( "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True ) @@ -185,5 +183,5 @@ def test_with_headers_exception(mock_request, get_request, setup): assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace - assert exported_span.attributes["error.type"] == "Exception" + assert exported_span.attributes["error.type"] == "Exception" assert exported_span.attributes["http.response.status_code"] == "500" From a86f45e0c3439925e34abd7f8c0c479f55b58348 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Fri, 15 Nov 2024 08:45:36 -0800 Subject: [PATCH 16/17] signature changes for decorators, more tests --- setup.py | 1 + swagger_zipkin/decorate_client.py | 6 +-- swagger_zipkin/otel_decorator.py | 83 +++++++++++++----------------- swagger_zipkin/zipkin_decorator.py | 2 +- tests/otel_decorator_test.py | 34 ++++++------ 5 files changed, 58 insertions(+), 68 deletions(-) diff --git a/setup.py b/setup.py index 0352e4a..7b56de2 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ install_requires=[ 'py_zipkin>=0.10.1', 'opentelemetry-sdk>=0.26.1', + 'bravado>=11.0.3' ], keywords='zipkin', classifiers=[ diff --git a/swagger_zipkin/decorate_client.py b/swagger_zipkin/decorate_client.py index 0e18618..0d11295 100644 --- a/swagger_zipkin/decorate_client.py +++ b/swagger_zipkin/decorate_client.py @@ -44,7 +44,7 @@ class OperationDecorator(Generic[P, T]): :type func: callable """ - def __init__(self, operation: Resource, func: Callable[P, T]) -> None: + def __init__(self, operation: Operation, func: Callable[P, T]) -> None: self.operation = operation self.func = func @@ -56,8 +56,8 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: def decorate_client( - api_client: Client, - func: Callable[P, T], + api_client: Resource, + func: Callable[[str, P.args, P.kwargs], T], name: str, ) -> Resource[P, T]: """A helper for decorating :class:`bravado.client.SwaggerClient`. diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 89063ad..3045ff3 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -1,11 +1,11 @@ from __future__ import annotations -import importlib from contextlib import contextmanager from typing import Any -from typing import TYPE_CHECKING from typing import TypeVar +from bravado.client import construct_request +from bravado.exception import HTTPError from opentelemetry import trace from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from opentelemetry.trace.span import format_span_id @@ -17,9 +17,6 @@ from swagger_zipkin.decorate_client import decorate_client from swagger_zipkin.decorate_client import Resource -if TYPE_CHECKING: - import pyramid.request.Request # type: ignore # noqa: F401 - T = TypeVar('T', covariant=True) P = ParamSpec('P') @@ -33,7 +30,7 @@ class OtelResourceDecorator: :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource` """ - def __init__(self, resource: Client, client_identifier: str, smartstack_namespace: str) -> None: + def __init__(self, resource: Resource, client_identifier: str, smartstack_namespace: str) -> None: self.resource = resource self.client_identifier = client_identifier self.smartstack_namespace = smartstack_namespace @@ -42,41 +39,44 @@ def __getattr__(self, name: str) -> Resource: return decorate_client(self.resource, self.with_headers, name) def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: - kwargs.setdefault('_request_options', {}) - request_options: dict = kwargs['_request_options'] - request_options.setdefault('headers', {}) - - # what is the right way to get the Request object. can we use contruct_request - # https://github.com/Yelp/bravado/blob/master/bravado/client.py#L283C5-L283C22 - # this would create a bravado dependency. - request = get_pyramid_current_request() - http_route = getattr(request, "matched_route", "") - http_request_method = getattr(request, "method", "") - - parent_span = trace.get_current_span() - span_name = f"{http_request_method} {http_route}" - with tracer.start_as_current_span( - span_name, kind=trace.SpanKind.CLIENT - ) as span: - self.inject_otel_headers(kwargs, span) - self.inject_zipkin_headers(kwargs, span, parent_span) - - # ideally the exception should be scoped for self.with_headers - # handle_exception should handle general excetion and the specific exception HTTPError - # But this would create a dependency on bravado package. Is this ok? - # Assuming resource.operation does throw HTTPError - # https://github.com/Yelp/bravado/blob/master/bravado/exception.py - with self.handle_exception(): - span.set_attribute("url.path", getattr(request, "path", "")) - span.set_attribute("http.route", http_route) - span.set_attribute("http.request.method", http_request_method) + with self.handle_exception(): + kwargs.setdefault('_request_options', {}) + request_options: dict = kwargs['_request_options'] + request_options.setdefault('headers', {}) + + operation = getattr(self.resource, call_name) + request = construct_request(operation, request_options, **kwargs) # type: ignore + + url = getattr(request, "url", "") + path = getattr(request, "path", "") + method = getattr(request, "method", "") + + parent_span = trace.get_current_span() + span_name = f"{method} {path}" + + with tracer.start_as_current_span( + span_name, kind=trace.SpanKind.CLIENT + ) as span: + span.set_attribute("url.path", url) + span.set_attribute("http.request.method", method) span.set_attribute("client.namespace", self.client_identifier) span.set_attribute("peer.service", self.smartstack_namespace) span.set_attribute("server.namespace", self.smartstack_namespace) span.set_attribute("http.response.status_code", "200") - return getattr(self.resource, call_name)(*args, **kwargs) + self.inject_otel_headers(kwargs, span) + self.inject_zipkin_headers(kwargs, span, parent_span) + + try: + operation(*args, **kwargs) + except HTTPError as e: + span.set_attribute("error.type", e.__class__.__name__) + span.set_status(trace.Status(trace.StatusCode.ERROR, e.message)) + span.set_attribute("http.response.status_code", e.status_code) + raise e + + return operation @contextmanager def handle_exception( @@ -85,9 +85,7 @@ def handle_exception( try: yield except Exception as e: - span = trace.get_current_span() - span.set_attribute("error.type", e.__class__.__name__) - span.set_attribute("http.response.status_code", "500") + # not raising an exception if the instrumentation had a problem raise e def __dir__(self) -> list[str]: @@ -154,12 +152,3 @@ def __getattr__(self, name: str) -> Client: def __dir__(self) -> list[str]: return dir(self._client) # pragma: no cover - - -def get_pyramid_current_request() -> pyramid.request.Request | None: - try: - threadlocal = importlib.import_module("pyramid.threadlocal") - except ImportError: - return None - - return threadlocal.get_current_request() diff --git a/swagger_zipkin/zipkin_decorator.py b/swagger_zipkin/zipkin_decorator.py index 7cc9996..a678434 100644 --- a/swagger_zipkin/zipkin_decorator.py +++ b/swagger_zipkin/zipkin_decorator.py @@ -22,7 +22,7 @@ class ZipkinResourceDecorator: :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource` """ - def __init__(self, resource: Client, context_stack: Stack | None = None) -> None: + def __init__(self, resource: Resource, context_stack: Stack | None = None) -> None: self.resource = resource self._context_stack = context_stack diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index db55975..82bfc6b 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -12,6 +12,9 @@ from swagger_zipkin.otel_decorator import OtelClientDecorator from swagger_zipkin.otel_decorator import OtelResourceDecorator +from bravado.exception import HTTPError +from bravado.exception import HTTPInternalServerError + memory_exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(memory_exporter) trace.set_tracer_provider(TracerProvider()) @@ -30,9 +33,9 @@ def setup(): @pytest.fixture def get_request(): mock_request = mock.Mock() + mock_request.url = "/sample-url" mock_request.path = "/sample-url" mock_request.method = "GET" - mock_request.matched_route = "sample-view" return mock_request @@ -55,7 +58,7 @@ def create_request_options(parent_span: trace.Span, exported_span: trace.Span): @mock.patch( - "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True + "swagger_zipkin.otel_decorator.construct_request", autospec=True ) def test_client_request(mock_request, get_request, setup): mock_request.return_value = get_request @@ -82,10 +85,9 @@ def test_client_request(mock_request, get_request, setup): ) assert exported_span.kind == SpanKind.CLIENT - assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.name == f"{get_request.method} {get_request.path}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method - assert exported_span.attributes["http.route"] == get_request.matched_route assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace @@ -102,10 +104,9 @@ def test_client_request(mock_request, get_request, setup): _request_options=create_request_options(parent_span, exported_span) ) - assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.name == f"{get_request.method} {get_request.path}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method - assert exported_span.attributes["http.route"] == get_request.matched_route assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace @@ -113,7 +114,7 @@ def test_client_request(mock_request, get_request, setup): @mock.patch( - "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True + "swagger_zipkin.otel_decorator.construct_request", autospec=True ) def test_client_request_no_parent_span(mock_request, get_request, setup): mock_request.return_value = get_request @@ -137,10 +138,9 @@ def test_client_request_no_parent_span(mock_request, get_request, setup): ) assert exported_span.kind == SpanKind.CLIENT - assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.name == f"{get_request.method} {get_request.path}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method - assert exported_span.attributes["http.route"] == get_request.matched_route assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace @@ -148,24 +148,25 @@ def test_client_request_no_parent_span(mock_request, get_request, setup): @mock.patch( - "swagger_zipkin.otel_decorator.get_pyramid_current_request", autospec=True + "swagger_zipkin.otel_decorator.construct_request", autospec=True ) def test_with_headers_exception(mock_request, get_request, setup): mock_request.return_value = get_request # Create a mock resource and configure it to raise an exception mock_resource = mock.MagicMock() - mock_method = mock.MagicMock(side_effect=Exception("simulated exception")) + mock_response = mock.MagicMock() + mock_response.status_code = "500" + mock_method = mock.MagicMock(side_effect=HTTPInternalServerError(response=mock_response)) setattr(mock_resource, 'test_operation', mock_method) decorator = OtelResourceDecorator(resource=mock_resource, client_identifier="test_client", smartstack_namespace="smartstack_namespace") - # Prepare arguments args = () kwargs = {'_request_options': {'headers': {}}} - - with pytest.raises(Exception): + + with pytest.raises(HTTPError): decorator.with_headers("test_operation", *args, **kwargs) assert len(memory_exporter.get_finished_spans()) == 1 @@ -176,12 +177,11 @@ def test_with_headers_exception(mock_request, get_request, setup): assert expected_headers == actual_headers assert exported_span.kind == SpanKind.CLIENT - assert exported_span.name == f"{get_request.method} {get_request.matched_route}" + assert exported_span.name == f"{get_request.method} {get_request.path}" assert exported_span.attributes["url.path"] == get_request.path assert exported_span.attributes["http.request.method"] == get_request.method - assert exported_span.attributes["http.route"] == get_request.matched_route assert exported_span.attributes["client.namespace"] == client_identifier assert exported_span.attributes["peer.service"] == smartstack_namespace assert exported_span.attributes["server.namespace"] == smartstack_namespace - assert exported_span.attributes["error.type"] == "Exception" + assert exported_span.attributes["error.type"] == "HTTPInternalServerError" assert exported_span.attributes["http.response.status_code"] == "500" From f78fcd6261f20097085dd325d973e5835b778f23 Mon Sep 17 00:00:00 2001 From: Amit Mokal Date: Fri, 15 Nov 2024 08:47:27 -0800 Subject: [PATCH 17/17] signature changes for decorators, more tests --- swagger_zipkin/otel_decorator.py | 4 ++-- tests/otel_decorator_test.py | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/swagger_zipkin/otel_decorator.py b/swagger_zipkin/otel_decorator.py index 3045ff3..ca40146 100644 --- a/swagger_zipkin/otel_decorator.py +++ b/swagger_zipkin/otel_decorator.py @@ -47,7 +47,7 @@ def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any: operation = getattr(self.resource, call_name) request = construct_request(operation, request_options, **kwargs) # type: ignore - + url = getattr(request, "url", "") path = getattr(request, "path", "") method = getattr(request, "method", "") @@ -85,7 +85,7 @@ def handle_exception( try: yield except Exception as e: - # not raising an exception if the instrumentation had a problem + # not raising an exception if the instrumentation had a problem raise e def __dir__(self) -> list[str]: diff --git a/tests/otel_decorator_test.py b/tests/otel_decorator_test.py index 82bfc6b..904ffa6 100644 --- a/tests/otel_decorator_test.py +++ b/tests/otel_decorator_test.py @@ -1,6 +1,8 @@ from unittest import mock import pytest +from bravado.exception import HTTPError +from bravado.exception import HTTPInternalServerError from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor @@ -12,9 +14,6 @@ from swagger_zipkin.otel_decorator import OtelClientDecorator from swagger_zipkin.otel_decorator import OtelResourceDecorator -from bravado.exception import HTTPError -from bravado.exception import HTTPInternalServerError - memory_exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(memory_exporter) trace.set_tracer_provider(TracerProvider()) @@ -165,7 +164,7 @@ def test_with_headers_exception(mock_request, get_request, setup): args = () kwargs = {'_request_options': {'headers': {}}} - + with pytest.raises(HTTPError): decorator.with_headers("test_operation", *args, **kwargs)