From 2e79688fa11f9d97cd9ad6bf0a9ac87a09ecfaf3 Mon Sep 17 00:00:00 2001 From: Dimitra Paraskevopoulou Date: Fri, 28 May 2021 09:06:14 +0200 Subject: [PATCH] Instrument sanic (#313) Instrumentation of Sanic Framework * initial instrumentation for sanic v21 * supporting sanic v20 * supporting v19 * uninstall uvloop, sanic optional dependency as it interferes with asynqp * Update .circleci/config.yml Co-authored-by: Manoj Pandey * Update tests/conftest.py Co-authored-by: Manoj Pandey * requested review changes and refactoring tests to class based Co-authored-by: Manoj Pandey --- .circleci/config.yml | 2 + instana/__init__.py | 9 +- instana/instrumentation/sanic_inst.py | 138 ++++++++++ instana/util/traceutils.py | 16 ++ tests/apps/sanic_app/__init__.py | 20 ++ tests/apps/sanic_app/name.py | 14 + tests/apps/sanic_app/server.py | 36 +++ tests/apps/sanic_app/simpleview.py | 24 ++ tests/conftest.py | 3 +- tests/frameworks/test_sanic.py | 358 ++++++++++++++++++++++++++ tests/requirements.txt | 1 + 11 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 instana/instrumentation/sanic_inst.py create mode 100644 instana/util/traceutils.py create mode 100644 tests/apps/sanic_app/__init__.py create mode 100644 tests/apps/sanic_app/name.py create mode 100644 tests/apps/sanic_app/server.py create mode 100644 tests/apps/sanic_app/simpleview.py create mode 100644 tests/frameworks/test_sanic.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b8b80eba..8a55459b5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,6 +98,8 @@ jobs: INSTANA_TEST: "true" command: | . venv/bin/activate + # We uninstall uvloop as it interferes with asyncio changing the event loop policy + pip uninstall -y uvloop pytest -v python38: diff --git a/instana/__init__.py b/instana/__init__.py index 6b8bf3d6d..29a3387b1 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -47,6 +47,7 @@ do_not_load_list = ["pip", "pip2", "pip3", "pipenv", "docker-compose", "easy_install", "easy_install-2.7", "smtpd.py", "twine", "ufw", "unattended-upgrade"] + def load(_): """ Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP @@ -57,6 +58,7 @@ def load(_): sys.argv = [''] return None + def get_lambda_handler_or_default(): """ For instrumenting AWS Lambda, users specify their original lambda handler in the LAMBDA_HANDLER environment @@ -108,7 +110,7 @@ def lambda_handler(event, context): def boot_agent_later(): """ Executes in the future! """ if 'gevent' in sys.modules: - import gevent # pylint: disable=import-outside-toplevel + import gevent # pylint: disable=import-outside-toplevel gevent.spawn_later(2.0, boot_agent) else: Timer(2.0, boot_agent).start() @@ -127,6 +129,9 @@ def boot_agent(): # Import & initialize instrumentation from .instrumentation.aws import lambda_inst + if sys.version_info >= (3, 7, 0): + from .instrumentation import sanic_inst + if sys.version_info >= (3, 6, 0): from .instrumentation import fastapi_inst from .instrumentation import starlette_inst @@ -173,6 +178,7 @@ def boot_agent(): # Hooks from .hooks import hook_uwsgi + if 'INSTANA_DISABLE' not in os.environ: # There are cases when sys.argv may not be defined at load time. Seems to happen in embedded Python, # and some Pipenv installs. If this is the case, it's best effort. @@ -184,6 +190,7 @@ def boot_agent(): # AutoProfile if "INSTANA_AUTOPROFILE" in os.environ: from .singletons import get_profiler + profiler = get_profiler() if profiler: profiler.start() diff --git a/instana/instrumentation/sanic_inst.py b/instana/instrumentation/sanic_inst.py new file mode 100644 index 000000000..a5159058b --- /dev/null +++ b/instana/instrumentation/sanic_inst.py @@ -0,0 +1,138 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +""" +Instrumentation for Sanic +https://sanicframework.org/en/ +""" +try: + import sanic + import wrapt + import opentracing + from ..log import logger + from ..singletons import async_tracer, agent + from ..util.secrets import strip_secrets_from_query + from ..util.traceutils import extract_custom_headers + + + @wrapt.patch_function_wrapper('sanic.exceptions', 'SanicException.__init__') + def exception_with_instana(wrapped, instance, args, kwargs): + message = kwargs.get("message", args[0]) + status_code = kwargs.get("status_code") + span = async_tracer.active_span + + if all([span, status_code, message]) and (500 <= status_code <= 599): + span.set_tag("http.error", message) + try: + wrapped(*args, **kwargs) + except Exception as exc: + span.log_exception(exc) + else: + wrapped(*args, **kwargs) + + + def response_details(span, response): + try: + status_code = response.status + if status_code is not None: + if 500 <= int(status_code) <= 511: + span.mark_as_errored() + span.set_tag('http.status_code', status_code) + + if response.headers is not None: + async_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, response.headers) + response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id + except Exception: + logger.debug("send_wrapper: ", exc_info=True) + + + if hasattr(sanic.response.BaseHTTPResponse, "send"): + @wrapt.patch_function_wrapper('sanic.response', 'BaseHTTPResponse.send') + async def send_with_instana(wrapped, instance, args, kwargs): + span = async_tracer.active_span + if span is None: + await wrapped(*args, **kwargs) + else: + response_details(span=span, response=instance) + try: + await wrapped(*args, **kwargs) + except Exception as exc: + span.log_exception(exc) + raise + else: + @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.write_response') + def write_with_instana(wrapped, instance, args, kwargs): + response = args[0] + span = async_tracer.active_span + if span is None: + wrapped(*args, **kwargs) + else: + response_details(span=span, response=response) + try: + wrapped(*args, **kwargs) + except Exception as exc: + span.log_exception(exc) + raise + + + @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.stream_response') + async def stream_with_instana(wrapped, instance, args, kwargs): + response = args[0] + span = async_tracer.active_span + if span is None: + await wrapped(*args, **kwargs) + else: + response_details(span=span, response=response) + try: + await wrapped(*args, **kwargs) + except Exception as exc: + span.log_exception(exc) + raise + + + @wrapt.patch_function_wrapper('sanic.app', 'Sanic.handle_request') + async def handle_request_with_instana(wrapped, instance, args, kwargs): + + try: + request = args[0] + try: # scheme attribute is calculated in the sanic handle_request method for v19, not yet present + if "http" not in request.scheme: + return await wrapped(*args, **kwargs) + except AttributeError: + pass + headers = request.headers.copy() + ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, headers) + with async_tracer.start_active_span("asgi", child_of=ctx) as scope: + scope.span.set_tag('span.kind', 'entry') + scope.span.set_tag('http.path', request.path) + scope.span.set_tag('http.method', request.method) + scope.span.set_tag('http.host', request.host) + scope.span.set_tag("http.url", request.url) + + query = request.query_string + + if isinstance(query, (str, bytes)) and len(query): + if isinstance(query, bytes): + query = query.decode('utf-8') + scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, + agent.options.secrets_list) + scope.span.set_tag("http.params", scrubbed_params) + + if agent.options.extra_http_headers is not None: + extract_custom_headers(scope, headers) + await wrapped(*args, **kwargs) + if hasattr(request, "uri_template"): + scope.span.set_tag("http.path_tpl", request.uri_template) + if hasattr(request, "ctx"): # ctx attribute added in the latest v19 versions + request.ctx.iscope = scope + except Exception as e: + logger.debug("Sanic framework @ handle_request", exc_info=True) + return await wrapped(*args, **kwargs) + + + logger.debug("Instrumenting Sanic") + +except ImportError: + pass +except AttributeError: + logger.debug("Not supported Sanic version") diff --git a/instana/util/traceutils.py b/instana/util/traceutils.py new file mode 100644 index 000000000..e93619feb --- /dev/null +++ b/instana/util/traceutils.py @@ -0,0 +1,16 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +from ..singletons import agent +from ..log import logger + + +def extract_custom_headers(tracing_scope, headers): + try: + for custom_header in agent.options.extra_http_headers: + # Headers are in the following format: b'x-header-1' + for header_key, value in headers.items(): + if header_key.lower() == custom_header.lower(): + tracing_scope.span.set_tag("http.header.%s" % custom_header, value) + except Exception as e: + logger.debug("extract_custom_headers: ", exc_info=True) diff --git a/tests/apps/sanic_app/__init__.py b/tests/apps/sanic_app/__init__.py new file mode 100644 index 000000000..cc7c5e129 --- /dev/null +++ b/tests/apps/sanic_app/__init__.py @@ -0,0 +1,20 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + + +import uvicorn +from ...helpers import testenv +from instana.log import logger + +testenv["sanic_port"] = 1337 +testenv["sanic_server"] = ("http://127.0.0.1:" + str(testenv["sanic_port"])) + + +def launch_sanic(): + from .server import app + from instana.singletons import agent + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + + uvicorn.run(app, host='127.0.0.1', port=testenv['sanic_port'], log_level="critical") diff --git a/tests/apps/sanic_app/name.py b/tests/apps/sanic_app/name.py new file mode 100644 index 000000000..055f51896 --- /dev/null +++ b/tests/apps/sanic_app/name.py @@ -0,0 +1,14 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + + +from sanic.views import HTTPMethodView +from sanic.response import text + + +class NameView(HTTPMethodView): + + def get(self, request, name): + return text("Hello {}".format(name)) + + diff --git a/tests/apps/sanic_app/server.py b/tests/apps/sanic_app/server.py new file mode 100644 index 000000000..1780e2bbf --- /dev/null +++ b/tests/apps/sanic_app/server.py @@ -0,0 +1,36 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +from sanic import Sanic +from sanic.exceptions import SanicException +from .simpleview import SimpleView +from .name import NameView +from sanic.response import text +import instana + +app = Sanic('test') + +@app.get("/foo/") +async def uuid_handler(request, foo_id: int): + return text("INT - {}".format(foo_id)) + + +@app.route("/test_request_args") +async def test_request_args(request): + raise SanicException("Something went wrong.", status_code=500) + + +@app.get("/tag/") +async def tag_handler(request, tag): + return text("Tag - {}".format(tag)) + + +app.add_route(SimpleView.as_view(), "/") +app.add_route(NameView.as_view(), "/") + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000, debug=True, access_log=True) + + + diff --git a/tests/apps/sanic_app/simpleview.py b/tests/apps/sanic_app/simpleview.py new file mode 100644 index 000000000..646a310d6 --- /dev/null +++ b/tests/apps/sanic_app/simpleview.py @@ -0,0 +1,24 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + + +from sanic.views import HTTPMethodView +from sanic.response import text + +class SimpleView(HTTPMethodView): + + def get(self, request): + return text("I am get method") + + # You can also use async syntax + async def post(self, request): + return text("I am post method") + + def put(self, request): + return text("I am put method") + + def patch(self, request): + return text("I am patch method") + + def delete(self, request): + return text("I am delete method") diff --git a/tests/conftest.py b/tests/conftest.py index 3d5fb925c..028d0e605 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ import pytest from distutils.version import LooseVersion - collect_ignore_glob = [] # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will @@ -44,6 +43,7 @@ # Make sure the instana package is fully loaded import instana + @pytest.fixture(scope='session') def celery_config(): return { @@ -62,4 +62,3 @@ def celery_includes(): return { 'tests.frameworks.test_celery' } - diff --git a/tests/frameworks/test_sanic.py b/tests/frameworks/test_sanic.py new file mode 100644 index 000000000..7ce7bdd0d --- /dev/null +++ b/tests/frameworks/test_sanic.py @@ -0,0 +1,358 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +from __future__ import absolute_import + +import time +import pytest +import requests +import multiprocessing +from instana.singletons import tracer +from ..helpers import testenv +from ..helpers import get_first_span_by_filter +from ..test_utils import _TraceContextMixin +import sys +import unittest + + +@pytest.mark.skipif(sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 7), + reason="testing sanic for python 3.7 and up") +class TestSanic(unittest.TestCase, _TraceContextMixin): + + def setUp(self): + from tests.apps.sanic_app import launch_sanic + self.proc = multiprocessing.Process(target=launch_sanic, args=(), daemon=True) + self.proc.start() + time.sleep(2) + + def tearDown(self): + self.proc.kill() + + def test_vanilla_get(self): + result = requests.get(testenv["sanic_server"] + '/') + + self.assertEqual(result.status_code, 200) + self.assertIn("X-INSTANA-T", result.headers) + self.assertIn("X-INSTANA-S", result.headers) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], "1") + self.assertIn("Server-Timing", result.headers) + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 1) + self.assertEqual(spans[0].n, 'sdk') + + def test_basic_get(self): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/') + + self.assertEqual(result.status_code, 200) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 200) + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + + def test_404(self): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/foo/not_an_int') + + self.assertEqual(result.status_code, 404) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/foo/not_an_int') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 404) + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.path_tpl', asgi_span.data['sdk']['custom']['tags']) + + def test_500(self): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/test_request_args') + + self.assertEqual(result.status_code, 500) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertEqual(asgi_span.ec, 1) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/test_request_args') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/test_request_args') + + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 500) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.error'], 'Something went wrong.') + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + + def test_path_templates(self): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/foo/1') + + self.assertEqual(result.status_code, 200) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/foo/1') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/foo/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 200) + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + + def test_secret_scrubbing(self): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/?secret=shhh') + + self.assertEqual(result.status_code, 200) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 200) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.params'], 'secret=') + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + + def test_synthetic_request(self): + request_headers = { + 'X-INSTANA-SYNTHETIC': '1' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/', headers=request_headers) + + self.assertEqual(result.status_code, 200) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 200) + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + + self.assertIsNotNone(asgi_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + + def test_custom_header_capture(self): + request_headers = { + 'X-Capture-This': 'this', + 'X-Capture-That': 'that' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["sanic_server"] + '/', headers=request_headers) + + self.assertEqual(result.status_code, 200) + + spans = tracer.recorder.queued_spans() + self.assertEqual(len(spans), 3) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + test_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(urllib3_span) + + span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'asgi' + asgi_span = get_first_span_by_filter(spans, span_filter) + self.assertIsNotNone(asgi_span) + + self.assertTraceContextPropagated(test_span, urllib3_span) + self.assertTraceContextPropagated(urllib3_span, asgi_span) + + self.assertIn("X-INSTANA-T", result.headers) + self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) + self.assertIn("X-INSTANA-S", result.headers) + self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) + self.assertIn("X-INSTANA-L", result.headers) + self.assertEqual(result.headers["X-INSTANA-L"], '1') + self.assertIn("Server-Timing", result.headers) + self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) + + self.assertIsNone(asgi_span.ec) + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.host'], '127.0.0.1:1337') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.path_tpl'], '/') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.method'], 'GET') + self.assertEqual(asgi_span.data['sdk']['custom']['tags']['http.status_code'], 200) + self.assertNotIn('http.error', asgi_span.data['sdk']['custom']['tags']) + self.assertNotIn('http.params', asgi_span.data['sdk']['custom']['tags']) + + self.assertIn("http.header.X-Capture-This", asgi_span.data["sdk"]["custom"]['tags']) + self.assertEqual("this", asgi_span.data["sdk"]["custom"]['tags']["http.header.X-Capture-This"]) + self.assertIn("http.header.X-Capture-That", asgi_span.data["sdk"]["custom"]['tags']) + self.assertEqual("that", asgi_span.data["sdk"]["custom"]['tags']["http.header.X-Capture-That"]) diff --git a/tests/requirements.txt b/tests/requirements.txt index dd7cfcc9c..5d4ec137b 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -23,6 +23,7 @@ pyramid>=2.0 pytest>=6.2.4 pytest-celery redis>=3.5.3 +sanic>=19.0.0 sqlalchemy>=1.4.15 spyne>=2.13.16 suds-jurko>=0.6