From 73e302fc4deba59c246ae3989b6e7e42ea2bd856 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 12 Sep 2024 18:00:42 +0530 Subject: [PATCH 1/4] refactor: pyramid tween instrumentation Signed-off-by: Varsha GS (cherry picked from commit 8ab3210a92502253876f91213009c287167f6790) --- src/instana/instrumentation/pyramid/tweens.py | 155 ++++++++++-------- 1 file changed, 86 insertions(+), 69 deletions(-) diff --git a/src/instana/instrumentation/pyramid/tweens.py b/src/instana/instrumentation/pyramid/tweens.py index 5f6c0d11..aed34e63 100644 --- a/src/instana/instrumentation/pyramid/tweens.py +++ b/src/instana/instrumentation/pyramid/tweens.py @@ -3,90 +3,107 @@ from pyramid.httpexceptions import HTTPException +from typing import TYPE_CHECKING, Dict, Any, Callable -import opentracing as ot -import opentracing.ext.tags as ext +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind -from ...log import logger -from ...singletons import tracer, agent -from ...util.secrets import strip_secrets_from_query +from instana.log import logger +from instana.singletons import tracer, agent +from instana.util.secrets import strip_secrets_from_query +from instana.propagators.format import Format + +if TYPE_CHECKING: + from pyramid.request import Request + from pyramid.response import Response + from pyramid.config import Configurator + from instana.span.span import InstanaSpan + from pyramid.registry import Registry class InstanaTweenFactory(object): """A factory that provides Instana instrumentation tween for Pyramid apps""" - def __init__(self, handler, registry): + def __init__( + self, handler: Callable[["Request"], "Response"], registry: "Registry" + ) -> None: self.handler = handler - def _extract_custom_headers(self, span, headers): - if agent.options.extra_http_headers is None: + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: + if not agent.options.extra_http_headers: return try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[custom_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def __call__(self, request): - ctx = tracer.extract(ot.Format.HTTP_HEADERS, dict(request.headers)) - scope = tracer.start_active_span('http', child_of=ctx) - - scope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - scope.span.set_tag("http.host", request.host) - scope.span.set_tag(ext.HTTP_METHOD, request.method) - scope.span.set_tag(ext.HTTP_URL, request.path) - - if request.matched_route is not None: - scope.span.set_tag("http.path_tpl", request.matched_route.pattern) - - self._extract_custom_headers(scope.span, request.headers) - - if len(request.query_string): - scrubbed_params = strip_secrets_from_query(request.query_string, agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", scrubbed_params) - - response = None - try: - response = self.handler(request) - - self._extract_custom_headers(scope.span, response.headers) - - tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id - except HTTPException as e: - response = e - raise - except BaseException as e: - scope.span.set_tag("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - scope.span.set_tag("message", str(e)) - scope.span.log_exception(e) - - logger.debug("Pyramid Instana tween", exc_info=True) - finally: - if response: - scope.span.set_tag("http.status", response.status_int) - - if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) - scope.span.log_exception(response.exception) - else: - message = response.status - - scope.span.set_tag("message", message) - scope.span.assure_errored() - - scope.close() - - return response - - -def includeme(config): + def __call__(self, request: "Request") -> "Response": + ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) + + with tracer.start_as_current_span("http", span_context=ctx) as span: + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.host", request.host) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_URL, request.path) + + self._extract_custom_headers(span, request.headers) + + if len(request.query_string): + scrubbed_params = strip_secrets_from_query( + request.query_string, + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + + response = None + try: + response = self.handler(request) + if request.matched_route is not None: + span.set_attribute("http.path_tpl", request.matched_route.pattern) + + self._extract_custom_headers(span, response.headers) + + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + "intid;desc=%s" % span.context.trace_id + ) + except HTTPException as e: + response = e + raise + except BaseException as e: + span.set_attribute("http.status", 500) + + # we need to explicitly populate the `message` tag with an error here + # so that it's picked up from an SDK span + span.set_attribute("message", str(e)) + span.record_exception(e) + + logger.debug("Pyramid Instana tween", exc_info=True) + finally: + if response: + span.set_attribute("http.status", response.status_int) + + if 500 <= response.status_int: + if response.exception is not None: + message = str(response.exception) + span.record_exception(response.exception) + else: + message = response.status + + span.set_attribute("message", message) + span.assure_errored() + + return response + + +def includeme(config: "Configurator") -> None: logger.debug("Instrumenting pyramid") - config.add_tween(__name__ + '.InstanaTweenFactory') + config.add_tween(__name__ + ".InstanaTweenFactory") From 1a002498e4a1bfb284ea33d9bc113427ed0722c9 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 17 Sep 2024 19:57:24 +0530 Subject: [PATCH 2/4] pyramid: adapt tests after refactor (cherry picked from commit 126e4ec09440509da867e7b7aa49dfaf0c8a867d) Signed-off-by: Varsha GS --- tests/apps/pyramid_app/__init__.py | 6 +- tests/apps/pyramid_app/app.py | 44 ++- tests/conftest.py | 6 - tests/frameworks/test_pyramid.py | 578 +++++++++++++++++------------ 4 files changed, 370 insertions(+), 264 deletions(-) diff --git a/tests/apps/pyramid_app/__init__.py b/tests/apps/pyramid_app/__init__.py index 31416ae4..c42e24b4 100644 --- a/tests/apps/pyramid_app/__init__.py +++ b/tests/apps/pyramid_app/__init__.py @@ -2,10 +2,10 @@ # (c) Copyright Instana Inc. 2020 import os -from .app import pyramid_server as server -from ..utils import launch_background_thread +from tests.apps.pyramid_app.app import pyramid_server as server +from tests.apps.utils import launch_background_thread app_thread = None -if not os.environ.get('CASSANDRA_TEST'): +if not os.environ.get("CASSANDRA_TEST"): app_thread = launch_background_thread(server.serve_forever, "Pyramid") diff --git a/tests/apps/pyramid_app/app.py b/tests/apps/pyramid_app/app.py index 56dd3f15..89c00b16 100644 --- a/tests/apps/pyramid_app/app.py +++ b/tests/apps/pyramid_app/app.py @@ -8,42 +8,50 @@ from pyramid.response import Response import pyramid.httpexceptions as exc -from ...helpers import testenv +from tests.helpers import testenv logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) testenv["pyramid_port"] = 10815 -testenv["pyramid_server"] = ("http://127.0.0.1:" + str(testenv["pyramid_port"])) +testenv["pyramid_server"] = "http://127.0.0.1:" + str(testenv["pyramid_port"]) + def hello_world(request): - return Response('Ok') + return Response("Ok") + def please_fail(request): raise exc.HTTPInternalServerError("internal error") + def tableflip(request): raise BaseException("fake exception") + def response_headers(request): - headers = { - 'X-Capture-This': 'Ok', - 'X-Capture-That': 'Ok too' - } + headers = {"X-Capture-This": "Ok", "X-Capture-That": "Ok too"} return Response("Stan wuz here with headers!", headers=headers) + +def hello_user(request): + user = request.matchdict["user"] + return Response(f"Hello {user}!") + + app = None with Configurator() as config: - config.add_tween('instana.instrumentation.pyramid.tweens.InstanaTweenFactory') - config.add_route('hello', '/') - config.add_view(hello_world, route_name='hello') - config.add_route('fail', '/500') - config.add_view(please_fail, route_name='fail') - config.add_route('crash', '/exception') - config.add_view(tableflip, route_name='crash') - config.add_route('response_headers', '/response_headers') - config.add_view(response_headers, route_name='response_headers') + config.include("instana.instrumentation.pyramid.tweens") + config.add_route("hello", "/") + config.add_view(hello_world, route_name="hello") + config.add_route("fail", "/500") + config.add_view(please_fail, route_name="fail") + config.add_route("crash", "/exception") + config.add_view(tableflip, route_name="crash") + config.add_route("response_headers", "/response_headers") + config.add_view(response_headers, route_name="response_headers") + config.add_route("hello_user", "/hello_user/{user}") + config.add_view(hello_user, route_name="hello_user") app = config.make_wsgi_app() - -pyramid_server = make_server('127.0.0.1', testenv["pyramid_port"], app) +pyramid_server = make_server("127.0.0.1", testenv["pyramid_port"], app) diff --git a/tests/conftest.py b/tests/conftest.py index 660f1245..c47e0cdd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,6 @@ collect_ignore_glob.append("*frameworks/test_celery*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") -collect_ignore_glob.append("*frameworks/test_pyramid*") collect_ignore_glob.append("*frameworks/test_tornado*") # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will @@ -86,11 +85,6 @@ collect_ignore_glob.append("*test_pep0249*") collect_ignore_glob.append("*test_sqlalchemy*") - # Currently the latest version of pyramid depends on the `cgi` module - # which has been deprecated since Python 3.11 and finally removed in 3.13 - # `ModuleNotFoundError: No module named 'cgi'` - collect_ignore_glob.append("*test_pyramid*") - # Currently not installable dependencies because of 3.13 incompatibilities collect_ignore_glob.append("*test_fastapi*") collect_ignore_glob.append("*test_google-cloud-pubsub*") diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index f3f88fb0..d2378d3a 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -1,319 +1,337 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest - +import pytest import urllib3 +from typing import Generator import tests.apps.pyramid_app -from ..helpers import testenv +from tests.helpers import testenv from instana.singletons import tracer, agent +from instana.span.span import get_current_span -class TestPyramid(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ +class TestPyramid: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() - def tearDown(self): - """ Do nothing for now """ - return None - - def test_vanilla_requests(self): - r = self.http.request('GET', testenv["pyramid_server"] + '/') - self.assertEqual(r.status, 200) + def test_vanilla_requests(self) -> None: + r = self.http.request("GET", testenv["pyramid_server"] + "/") + assert r.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - def test_get_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/') + def test_get_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["pyramid_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], pyramid_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], pyramid_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % pyramid_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_synthetic_request(self): - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } - - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/', headers=headers) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert testenv["pyramid_server"] + "/" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_synthetic_request(self) -> None: + headers = {"X-INSTANA-SYNTHETIC": "1"} + + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/", headers=headers + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertTrue(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy - def test_500(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/500') + def test_500(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["pyramid_server"] + "/500") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert response.status == 500 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], pyramid_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], pyramid_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % pyramid_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, pyramid_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert pyramid_span.ec == 1 # wsgi - self.assertEqual("sdk", pyramid_span.n) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/500', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(500, sdk_custom_tags["http.status"]) - self.assertEqual("internal error", sdk_custom_tags["message"]) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/500" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 500 + assert sdk_custom_attributes["message"] == "internal error" + assert sdk_custom_attributes["http.path_tpl"] == "/500" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/500', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_exception(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/exception') + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert testenv["pyramid_server"] + "/500" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_exception(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/exception" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert response.status == 500 - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, pyramid_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert pyramid_span.ec == 1 # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/exception', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(500, sdk_custom_tags["http.status"]) - self.assertEqual("fake exception", sdk_custom_tags["message"]) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/exception" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 500 + assert sdk_custom_attributes["message"] == "fake exception" + assert "http.path_tpl" not in sdk_custom_attributes # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/exception', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_response_header_capture(self): + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert ( + testenv["pyramid_server"] + "/exception" == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/response_headers') + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/response_headers" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/response_headers', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/response_headers" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/response_headers', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - - self.assertTrue(sdk_custom_tags["http.header.X-Capture-This"]) - self.assertEqual("Ok", sdk_custom_tags["http.header.X-Capture-This"]) - self.assertTrue(sdk_custom_tags["http.header.X-Capture-That"]) - self.assertEqual("Ok too", sdk_custom_tags["http.header.X-Capture-That"]) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert ( + testenv["pyramid_server"] + "/response_headers" + == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + assert sdk_custom_attributes["http.header.X-Capture-This"] + assert sdk_custom_attributes["http.header.X-Capture-This"] == "Ok" + assert sdk_custom_attributes["http.header.X-Capture-That"] + assert sdk_custom_attributes["http.header.X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture(self): + def test_request_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] @@ -322,68 +340,154 @@ def test_request_header_capture(self): "X-Capture-That-Too": "that too", } - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/", headers=request_headers ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert testenv["pyramid_server"] + "/" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # custom headers - self.assertTrue(sdk_custom_tags["http.header.X-Capture-This-Too"]) - self.assertEqual("this too", sdk_custom_tags["http.header.X-Capture-This-Too"]) - self.assertTrue(sdk_custom_tags["http.header.X-Capture-That-Too"]) - self.assertEqual("that too", sdk_custom_tags["http.header.X-Capture-That-Too"]) + assert sdk_custom_attributes["http.header.X-Capture-This-Too"] + assert sdk_custom_attributes["http.header.X-Capture-This-Too"] == "this too" + assert sdk_custom_attributes["http.header.X-Capture-That-Too"] + assert sdk_custom_attributes["http.header.X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers + + def test_scrub_secret_path_template(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/hello_user/oswald?secret=sshhh" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + pyramid_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + assert response + assert response.status == 200 + + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) + + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) + + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + + assert "Server-Timing" in response.headers + server_timing_value = "intid;desc=%s" % pyramid_span.t + assert response.headers["Server-Timing"] == server_timing_value + + assert not get_current_span().is_recording() + + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s + + # Synthetic + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy + + # Error logging + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec + + # HTTP SDK span + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/hello_user/oswald" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert sdk_custom_attributes["http.params"] == "secret=" + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/hello_user/{user}" + + # urllib3 + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert ( + testenv["pyramid_server"] + sdk_custom_attributes["http.url"] + == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 From 112e062e7492b6fe2f468c8a67c74a09310993c6 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 20 Sep 2024 15:06:04 +0530 Subject: [PATCH 3/4] pyramid: enable auto-instrumentation Signed-off-by: Varsha GS --- src/instana/__init__.py | 1 + .../instrumentation/pyramid/__init__.py | 0 src/instana/instrumentation/pyramid/tweens.py | 109 ------------- src/instana/instrumentation/pyramid_inst.py | 147 ++++++++++++++++++ .../{ => pyramid}/pyramid_app/__init__.py | 2 +- tests/apps/{ => pyramid}/pyramid_app/app.py | 6 +- tests/apps/pyramid/pyramid_utils/tweens.py | 16 ++ tests/frameworks/test_pyramid.py | 2 +- 8 files changed, 170 insertions(+), 113 deletions(-) delete mode 100644 src/instana/instrumentation/pyramid/__init__.py delete mode 100644 src/instana/instrumentation/pyramid/tweens.py create mode 100644 src/instana/instrumentation/pyramid_inst.py rename tests/apps/{ => pyramid}/pyramid_app/__init__.py (78%) rename tests/apps/{ => pyramid}/pyramid_app/app.py (91%) create mode 100644 tests/apps/pyramid/pyramid_utils/tweens.py diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 7f870b6a..9f67abff 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -183,6 +183,7 @@ def boot_agent(): starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 urllib3, # noqa: F401 + pyramid_inst, ) from instana.instrumentation.aiohttp import ( client, # noqa: F401 diff --git a/src/instana/instrumentation/pyramid/__init__.py b/src/instana/instrumentation/pyramid/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/instana/instrumentation/pyramid/tweens.py b/src/instana/instrumentation/pyramid/tweens.py deleted file mode 100644 index aed34e63..00000000 --- a/src/instana/instrumentation/pyramid/tweens.py +++ /dev/null @@ -1,109 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - - -from pyramid.httpexceptions import HTTPException -from typing import TYPE_CHECKING, Dict, Any, Callable - -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind - -from instana.log import logger -from instana.singletons import tracer, agent -from instana.util.secrets import strip_secrets_from_query -from instana.propagators.format import Format - -if TYPE_CHECKING: - from pyramid.request import Request - from pyramid.response import Response - from pyramid.config import Configurator - from instana.span.span import InstanaSpan - from pyramid.registry import Registry - - -class InstanaTweenFactory(object): - """A factory that provides Instana instrumentation tween for Pyramid apps""" - - def __init__( - self, handler: Callable[["Request"], "Response"], registry: "Registry" - ) -> None: - self.handler = handler - - def _extract_custom_headers( - self, span: "InstanaSpan", headers: Dict[str, Any] - ) -> None: - if not agent.options.extra_http_headers: - return - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute( - "http.header.%s" % custom_header, headers[custom_header] - ) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - - def __call__(self, request: "Request") -> "Response": - ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) - - with tracer.start_as_current_span("http", span_context=ctx) as span: - span.set_attribute("span.kind", SpanKind.SERVER) - span.set_attribute("http.host", request.host) - span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) - span.set_attribute(SpanAttributes.HTTP_URL, request.path) - - self._extract_custom_headers(span, request.headers) - - if len(request.query_string): - scrubbed_params = strip_secrets_from_query( - request.query_string, - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - - response = None - try: - response = self.handler(request) - if request.matched_route is not None: - span.set_attribute("http.path_tpl", request.matched_route.pattern) - - self._extract_custom_headers(span, response.headers) - - tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - response.headers["Server-Timing"] = ( - "intid;desc=%s" % span.context.trace_id - ) - except HTTPException as e: - response = e - raise - except BaseException as e: - span.set_attribute("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - span.set_attribute("message", str(e)) - span.record_exception(e) - - logger.debug("Pyramid Instana tween", exc_info=True) - finally: - if response: - span.set_attribute("http.status", response.status_int) - - if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) - span.record_exception(response.exception) - else: - message = response.status - - span.set_attribute("message", message) - span.assure_errored() - - return response - - -def includeme(config: "Configurator") -> None: - logger.debug("Instrumenting pyramid") - config.add_tween(__name__ + ".InstanaTweenFactory") diff --git a/src/instana/instrumentation/pyramid_inst.py b/src/instana/instrumentation/pyramid_inst.py new file mode 100644 index 00000000..7527aa6f --- /dev/null +++ b/src/instana/instrumentation/pyramid_inst.py @@ -0,0 +1,147 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.path import caller_package + from pyramid.settings import aslist + from pyramid.tweens import EXCVIEW + from pyramid.config import Configurator + from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple + import wrapt + + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import SpanKind + + from instana.log import logger + from instana.singletons import tracer, agent + from instana.util.secrets import strip_secrets_from_query + from instana.propagators.format import Format + + if TYPE_CHECKING: + from pyramid.request import Request + from pyramid.response import Response + from instana.span.span import InstanaSpan + from pyramid.registry import Registry + + class InstanaTweenFactory(object): + """A factory that provides Instana instrumentation tween for Pyramid apps""" + + def __init__( + self, handler: Callable[["Request"], "Response"], registry: "Registry" + ) -> None: + self.handler = handler + + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: + if not agent.options.extra_http_headers: + return + try: + for custom_header in agent.options.extra_http_headers: + if custom_header in headers: + span.set_attribute( + "http.header.%s" % custom_header, headers[custom_header] + ) + + except Exception: + logger.debug("extract_custom_headers: ", exc_info=True) + + def __call__(self, request: "Request") -> "Response": + ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) + + with tracer.start_as_current_span("http", span_context=ctx) as span: + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.host", request.host) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_URL, request.path) + + self._extract_custom_headers(span, request.headers) + + if len(request.query_string): + scrubbed_params = strip_secrets_from_query( + request.query_string, + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + + response = None + try: + response = self.handler(request) + if request.matched_route is not None: + span.set_attribute( + "http.path_tpl", request.matched_route.pattern + ) + + self._extract_custom_headers(span, response.headers) + + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + "intid;desc=%s" % span.context.trace_id + ) + except HTTPException as e: + response = e + raise + except BaseException as e: + span.set_attribute("http.status", 500) + + # we need to explicitly populate the `message` tag with an error here + # so that it's picked up from an SDK span + span.set_attribute("message", str(e)) + span.record_exception(e) + + logger.debug("Pyramid Instana tween", exc_info=True) + finally: + if response: + span.set_attribute("http.status", response.status_int) + + if 500 <= response.status_int: + if response.exception is not None: + message = str(response.exception) + span.record_exception(response.exception) + else: + message = response.status + + span.set_attribute("message", message) + span.assure_errored() + + return response + + INSTANA_TWEEN = __name__ + ".InstanaTweenFactory" + + # implicit tween ordering + def includeme(config: Configurator) -> None: + logger.debug("Instrumenting pyramid") + config.add_tween(INSTANA_TWEEN) + + # explicit tween ordering + @wrapt.patch_function_wrapper("pyramid.config", "Configurator.__init__") + def init_with_instana( + wrapped: Callable[..., Configurator.__init__], + instance: Configurator, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ): + settings = kwargs.get("settings", {}) + tweens = aslist(settings.get("pyramid.tweens", [])) + + if tweens and INSTANA_TWEEN not in settings: + # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by + # pyramid. We need our tween to be before it, otherwise unhandled + # exceptions will be caught before they reach our tween. + if EXCVIEW in tweens: + tweens = [INSTANA_TWEEN] + tweens + else: + tweens = [INSTANA_TWEEN] + tweens + [EXCVIEW] + settings["pyramid.tweens"] = "\n".join(tweens) + kwargs["settings"] = settings + + if not kwargs.get("package", None): + kwargs["package"] = caller_package() + + wrapped(*args, **kwargs) + instance.include(__name__) + +except ImportError: + pass diff --git a/tests/apps/pyramid_app/__init__.py b/tests/apps/pyramid/pyramid_app/__init__.py similarity index 78% rename from tests/apps/pyramid_app/__init__.py rename to tests/apps/pyramid/pyramid_app/__init__.py index c42e24b4..bae66790 100644 --- a/tests/apps/pyramid_app/__init__.py +++ b/tests/apps/pyramid/pyramid_app/__init__.py @@ -2,7 +2,7 @@ # (c) Copyright Instana Inc. 2020 import os -from tests.apps.pyramid_app.app import pyramid_server as server +from tests.apps.pyramid.pyramid_app.app import pyramid_server as server from tests.apps.utils import launch_background_thread app_thread = None diff --git a/tests/apps/pyramid_app/app.py b/tests/apps/pyramid/pyramid_app/app.py similarity index 91% rename from tests/apps/pyramid_app/app.py rename to tests/apps/pyramid/pyramid_app/app.py index 89c00b16..867b2e7c 100644 --- a/tests/apps/pyramid_app/app.py +++ b/tests/apps/pyramid/pyramid_app/app.py @@ -40,8 +40,10 @@ def hello_user(request): app = None -with Configurator() as config: - config.include("instana.instrumentation.pyramid.tweens") +settings = { + "pyramid.tweens": "tests.apps.pyramid.pyramid_utils.tweens.timing_tween_factory", +} +with Configurator(settings=settings) as config: config.add_route("hello", "/") config.add_view(hello_world, route_name="hello") config.add_route("fail", "/500") diff --git a/tests/apps/pyramid/pyramid_utils/tweens.py b/tests/apps/pyramid/pyramid_utils/tweens.py new file mode 100644 index 00000000..0183df4b --- /dev/null +++ b/tests/apps/pyramid/pyramid_utils/tweens.py @@ -0,0 +1,16 @@ +# (c) Copyright IBM Corp. 2024 + +import time + + +def timing_tween_factory(handler, registry): + def timing_tween(request): + start = time.time() + try: + response = handler(request) + finally: + end = time.time() + print(f"The request took {end - start} seconds") + return response + + return timing_tween diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index d2378d3a..dcd41474 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -5,7 +5,7 @@ import urllib3 from typing import Generator -import tests.apps.pyramid_app +import tests.apps.pyramid.pyramid_app from tests.helpers import testenv from instana.singletons import tracer, agent from instana.span.span import get_current_span From 10f62913021c0be93d517004bec494abcdeff7a0 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Sat, 21 Sep 2024 10:52:00 +0530 Subject: [PATCH 4/4] pyramid: change from sdk span (http) to registered entry span (wsgi) Signed-off-by: Varsha GS --- src/instana/__init__.py | 2 +- .../{pyramid_inst.py => pyramid.py} | 31 ++-- tests/frameworks/test_pyramid.py | 169 ++++++------------ 3 files changed, 73 insertions(+), 129 deletions(-) rename src/instana/instrumentation/{pyramid_inst.py => pyramid.py} (83%) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 9f67abff..75aaf562 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -178,12 +178,12 @@ def boot_agent(): psycopg2, # noqa: F401 pymongo, # noqa: F401 pymysql, # noqa: F401 + pyramid, # noqa: F401 redis, # noqa: F401 # sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 urllib3, # noqa: F401 - pyramid_inst, ) from instana.instrumentation.aiohttp import ( client, # noqa: F401 diff --git a/src/instana/instrumentation/pyramid_inst.py b/src/instana/instrumentation/pyramid.py similarity index 83% rename from src/instana/instrumentation/pyramid_inst.py rename to src/instana/instrumentation/pyramid.py index 7527aa6f..85fd1829 100644 --- a/src/instana/instrumentation/pyramid_inst.py +++ b/src/instana/instrumentation/pyramid.py @@ -41,7 +41,7 @@ def _extract_custom_headers( for custom_header in agent.options.extra_http_headers: if custom_header in headers: span.set_attribute( - "http.header.%s" % custom_header, headers[custom_header] + f"http.header.{custom_header}", headers[custom_header] ) except Exception: @@ -50,7 +50,7 @@ def _extract_custom_headers( def __call__(self, request: "Request") -> "Response": ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) - with tracer.start_as_current_span("http", span_context=ctx) as span: + with tracer.start_as_current_span("wsgi", span_context=ctx) as span: span.set_attribute("span.kind", SpanKind.SERVER) span.set_attribute("http.host", request.host) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) @@ -78,32 +78,29 @@ def __call__(self, request: "Request") -> "Response": tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) response.headers["Server-Timing"] = ( - "intid;desc=%s" % span.context.trace_id + f"intid;desc={span.context.trace_id}" ) except HTTPException as e: response = e - raise + logger.debug( + "Pyramid InstanaTweenFactory HTTPException: ", exc_info=True + ) except BaseException as e: - span.set_attribute("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - span.set_attribute("message", str(e)) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) span.record_exception(e) - logger.debug("Pyramid Instana tween", exc_info=True) + logger.debug( + "Pyramid InstanaTweenFactory BaseException: ", exc_info=True + ) finally: if response: - span.set_attribute("http.status", response.status_int) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_int + ) if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) + if response.exception: span.record_exception(response.exception) - else: - message = response.status - - span.set_attribute("message", message) span.assure_errored() return response diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index dcd41474..6aa39ca4 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -37,7 +37,6 @@ def test_get_request(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 assert "X-INSTANA-T" in response.headers @@ -75,23 +74,14 @@ def test_get_request(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -118,7 +108,6 @@ def test_synthetic_request(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 assert pyramid_span.sy @@ -137,7 +126,6 @@ def test_500(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 500 assert "X-INSTANA-T" in response.headers @@ -171,20 +159,13 @@ def test_500(self) -> None: assert pyramid_span.ec == 1 # wsgi - assert pyramid_span.n == "sdk" - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/500" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 500 - assert sdk_custom_attributes["message"] == "internal error" - assert sdk_custom_attributes["http.path_tpl"] == "/500" + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/500" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 500 + assert pyramid_span.data["http"]["error"] == "internal error" + assert pyramid_span.data["http"]["path_tpl"] == "/500" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -210,7 +191,6 @@ def test_exception(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 500 assert not get_current_span().is_recording() @@ -228,21 +208,14 @@ def test_exception(self) -> None: assert urllib3_span.ec == 1 assert pyramid_span.ec == 1 - # HTTP SDK span - assert pyramid_span.n == "sdk" - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/exception" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 500 - assert sdk_custom_attributes["message"] == "fake exception" - assert "http.path_tpl" not in sdk_custom_attributes + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/exception" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 500 + assert pyramid_span.data["http"]["error"] == "fake exception" + assert not pyramid_span.data["http"]["path_tpl"] # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -294,22 +267,14 @@ def test_response_header_capture(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/response_headers" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/response_headers" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/response_headers" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -324,10 +289,11 @@ def test_response_header_capture(self) -> None: assert type(urllib3_span.stack) is list assert len(urllib3_span.stack) > 1 - assert sdk_custom_attributes["http.header.X-Capture-This"] - assert sdk_custom_attributes["http.header.X-Capture-This"] == "Ok" - assert sdk_custom_attributes["http.header.X-Capture-That"] - assert sdk_custom_attributes["http.header.X-Capture-That"] == "Ok too" + # custom headers + assert "X-Capture-This" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-This"] == "Ok" + assert "X-Capture-That" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers @@ -352,7 +318,6 @@ def test_request_header_capture(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 # Same traceId @@ -373,23 +338,14 @@ def test_request_header_capture(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -402,10 +358,10 @@ def test_request_header_capture(self) -> None: assert len(urllib3_span.stack) > 1 # custom headers - assert sdk_custom_attributes["http.header.X-Capture-This-Too"] - assert sdk_custom_attributes["http.header.X-Capture-This-Too"] == "this too" - assert sdk_custom_attributes["http.header.X-Capture-That-Too"] - assert sdk_custom_attributes["http.header.X-Capture-That-Too"] == "that too" + assert "X-Capture-This-Too" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers @@ -460,31 +416,22 @@ def test_scrub_secret_path_template(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/hello_user/oswald" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert sdk_custom_attributes["http.params"] == "secret=" - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/hello_user/{user}" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/hello_user/oswald" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert pyramid_span.data["http"]["params"] == "secret=" + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/hello_user/{user}" # urllib3 assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 assert ( - testenv["pyramid_server"] + sdk_custom_attributes["http.url"] + testenv["pyramid_server"] + pyramid_span.data["http"]["url"] == urllib3_span.data["http"]["url"] ) assert urllib3_span.data["http"]["method"] == "GET"