diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 7f870b6a..75aaf562 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -178,6 +178,7 @@ 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 diff --git a/src/instana/instrumentation/pyramid.py b/src/instana/instrumentation/pyramid.py new file mode 100644 index 00000000..85fd1829 --- /dev/null +++ b/src/instana/instrumentation/pyramid.py @@ -0,0 +1,144 @@ +# (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( + f"http.header.{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("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) + 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"] = ( + f"intid;desc={span.context.trace_id}" + ) + except HTTPException as e: + response = e + logger.debug( + "Pyramid InstanaTweenFactory HTTPException: ", exc_info=True + ) + except BaseException as e: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + span.record_exception(e) + + logger.debug( + "Pyramid InstanaTweenFactory BaseException: ", exc_info=True + ) + finally: + if response: + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_int + ) + + if 500 <= response.status_int: + if response.exception: + span.record_exception(response.exception) + 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/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 5f6c0d11..00000000 --- a/src/instana/instrumentation/pyramid/tweens.py +++ /dev/null @@ -1,92 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - - -from pyramid.httpexceptions import HTTPException - -import opentracing as ot -import opentracing.ext.tags as ext - -from ...log import logger -from ...singletons import tracer, agent -from ...util.secrets import strip_secrets_from_query - - -class InstanaTweenFactory(object): - """A factory that provides Instana instrumentation tween for Pyramid apps""" - - def __init__(self, handler, registry): - self.handler = handler - - def _extract_custom_headers(self, span, headers): - if agent.options.extra_http_headers is None: - 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]) - - 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): - logger.debug("Instrumenting pyramid") - config.add_tween(__name__ + '.InstanaTweenFactory') diff --git a/tests/apps/pyramid_app/__init__.py b/tests/apps/pyramid/pyramid_app/__init__.py similarity index 50% rename from tests/apps/pyramid_app/__init__.py rename to tests/apps/pyramid/pyramid_app/__init__.py index 31416ae4..bae66790 100644 --- a/tests/apps/pyramid_app/__init__.py +++ b/tests/apps/pyramid/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.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/pyramid_app/app.py b/tests/apps/pyramid/pyramid_app/app.py new file mode 100644 index 00000000..867b2e7c --- /dev/null +++ b/tests/apps/pyramid/pyramid_app/app.py @@ -0,0 +1,59 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +from wsgiref.simple_server import make_server +from pyramid.config import Configurator +import logging + +from pyramid.response import Response +import pyramid.httpexceptions as exc + +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"]) + + +def hello_world(request): + 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"} + return Response("Stan wuz here with headers!", headers=headers) + + +def hello_user(request): + user = request.matchdict["user"] + return Response(f"Hello {user}!") + + +app = None +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") + 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) 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/apps/pyramid_app/app.py b/tests/apps/pyramid_app/app.py deleted file mode 100644 index 56dd3f15..00000000 --- a/tests/apps/pyramid_app/app.py +++ /dev/null @@ -1,49 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -from wsgiref.simple_server import make_server -from pyramid.config import Configurator -import logging - -from pyramid.response import Response -import pyramid.httpexceptions as exc - -from ...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"])) - -def hello_world(request): - 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' - } - return Response("Stan wuz here with headers!", headers=headers) - -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') - app = config.make_wsgi_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..6aa39ca4 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -1,319 +1,303 @@ # (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 +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 -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.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) - - # 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"]) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec - 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) + # 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 - 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.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.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 == "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 - 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.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) - - # 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 not test_span.ec + assert urllib3_span.ec == 1 + assert pyramid_span.ec == 1 + + # 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 - 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) - - # 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"]) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec - 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) + # 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 - 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 + + # 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 - 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 +306,135 @@ 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.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) + # 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 - 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 "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 + + 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 + + # 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"] + pyramid_span.data["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