diff --git a/.circleci/config.yml b/.circleci/config.yml index a3027569..f62b7661 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,7 +56,6 @@ commands: - run: name: Run Tests With Coverage Report environment: - INSTANA_TEST: "true" CASSANDRA_TEST: "<>" COUCHBASE_TEST: "<>" GEVENT_STARLETTE_TEST: "<>" @@ -78,6 +77,7 @@ commands: steps: - store_test_results: path: test-results + run_sonarqube: steps: - attach_workspace: @@ -118,8 +118,8 @@ commands: jobs: python38: docker: - - image: cimg/python:3.8.20 - - image: cimg/postgres:9.6.24 + - image: cimg/python:3.8 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -142,8 +142,8 @@ jobs: python39: docker: - - image: cimg/python:3.9.20 - - image: cimg/postgres:9.6.24 + - image: cimg/python:3.9 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -166,8 +166,8 @@ jobs: python310: docker: - - image: cimg/python:3.10.15 - - image: cimg/postgres:9.6.24 + - image: cimg/python:3.10 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -191,8 +191,8 @@ jobs: python311: docker: - - image: cimg/python:3.11.10 - - image: cimg/postgres:9.6.24 + - image: cimg/python:3.11 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -231,8 +231,8 @@ jobs: python312: docker: - - image: cimg/python:3.12.6 - - image: cimg/postgres:9.6.24 + - image: cimg/python:3.12 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -272,7 +272,7 @@ jobs: python313: docker: - image: python:3.13.0rc2-bookworm - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -296,7 +296,7 @@ jobs: py39couchbase: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: couchbase/server-sandbox:5.5.0 working_directory: ~/repo steps: @@ -312,7 +312,7 @@ jobs: py39cassandra: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: cassandra:3.11 environment: MAX_HEAP_SIZE: 2048m @@ -363,11 +363,11 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette - - py311googlecloud - - py312googlecloud + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette + # - py311googlecloud + # - py312googlecloud - final_job: requires: - python38 @@ -376,8 +376,8 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette - - py311googlecloud - - py312googlecloud + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette + # - py311googlecloud + # - py312googlecloud diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..084fd0bc --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: + except ImportError: + except Exception: + except Exception as exc: diff --git a/.tekton/run_unittests.sh b/.tekton/run_unittests.sh index dfcc79eb..fe91bb53 100755 --- a/.tekton/run_unittests.sh +++ b/.tekton/run_unittests.sh @@ -52,7 +52,6 @@ esac echo -n "Configuration is '${TEST_CONFIGURATION}' on ${PYTHON_VERSION} " echo "with dependencies in '${REQUIREMENTS}'" -export INSTANA_TEST='true' ls -lah . if [[ -n "${COUCHBASE_TEST}" ]]; then echo "Install Couchbase Dependencies" diff --git a/pyproject.toml b/pyproject.toml index 2703ace0..247b90e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires-python = ">=3.8" license = "MIT" keywords = [ "performance", - "opentracing", + "opentelemetry", "metrics", "monitoring", "tracing", @@ -44,13 +44,13 @@ classifiers = [ ] dependencies = [ "autowrapt>=1.0", - "basictracer>=3.1.0", "fysom>=2.1.2", - "opentracing>=2.3.0", "protobuf<5.0.0", "requests>=2.6.0", "six>=1.12.0", "urllib3>=1.26.5", + "opentelemetry-api>=1.26.0", + "opentelemetry-semantic-conventions>=0.47b0", ] [project.entry-points."instana"] @@ -59,6 +59,8 @@ string = "instana:load" [project.optional-dependencies] dev = [ "pytest", + "pytest-cov", + "pytest-mock", ] [project.urls] @@ -77,3 +79,10 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/instana"] + +[tool.coverage.report] +exclude_also = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "except ImportError:", + ] diff --git a/pytest.ini b/pytest.ini index 52835b1d..be615810 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,4 @@ log_cli = 1 log_cli_level = WARN log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %H:%M:%S +pythonpath = src diff --git a/src/instana/__init__.py b/src/instana/__init__.py index e6ae417b..bb4d53b9 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -1,38 +1,36 @@ # coding=utf-8 +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2016 """ -▀████▀███▄ ▀███▀▄█▀▀▀█▄███▀▀██▀▀███ ██ ▀███▄ ▀███▀ ██ - ██ ███▄ █ ▄██ ▀█▀ ██ ▀█ ▄██▄ ███▄ █ ▄██▄ - ██ █ ███ █ ▀███▄ ██ ▄█▀██▄ █ ███ █ ▄█▀██▄ - ██ █ ▀██▄ █ ▀█████▄ ██ ▄█ ▀██ █ ▀██▄ █ ▄█ ▀██ - ██ █ ▀██▄█ ▄ ▀██ ██ ████████ █ ▀██▄█ ████████ - ██ █ ███ ██ ██ ██ █▀ ██ █ ███ █▀ ██ -▄████▄███▄ ██ █▀█████▀ ▄████▄ ▄███▄ ▄████▄███▄ ██ ▄███▄ ▄████▄ +Instana -https://www.instana.com/ +https://www.ibm.com/products/instana -Documentation: https://www.instana.com/docs/ +Documentation: https://www.ibm.com/docs/en/instana-observability/current Source Code: https://github.com/instana/python-sensor """ - +import importlib import os import sys -import importlib -from .version import VERSION -from instana.collector.helpers.runtime import is_autowrapt_instrumented, is_webhook_instrumented - -__author__ = 'Instana Inc.' -__copyright__ = 'Copyright 2020 Instana Inc.' -__credits__ = ['Pavlo Baron', 'Peter Giacomo Lombardo', 'Andrey Slotin'] -__license__ = 'MIT' -__maintainer__ = 'Peter Giacomo Lombardo' -__email__ = 'peter.lombardo@instana.com' +from instana.collector.helpers.runtime import ( + is_autowrapt_instrumented, + is_webhook_instrumented, +) +from instana.version import VERSION + +__author__ = "Instana Inc." +__copyright__ = "Copyright 2020 Instana Inc." +__credits__ = ["Pavlo Baron", "Peter Giacomo Lombardo", "Andrey Slotin"] +__license__ = "MIT" +__maintainer__ = "Peter Giacomo Lombardo" +__email__ = "peter.lombardo@instana.com" __version__ = VERSION # User configurable EUM API key for instana.helpers.eum_snippet() # pylint: disable=invalid-name -eum_api_key = '' +eum_api_key = "" # This Python package can be loaded into Python processes one of three ways: # 1. manual import statement @@ -42,8 +40,19 @@ # With such magic, we may get pulled into Python processes that we have no interest being in. # As a safety measure, we maintain a "do not load list" and if this process matches something # in that list, then we go sit in a corner quietly and don't load anything at all. -do_not_load_list = ["pip", "pip2", "pip3", "pipenv", "docker-compose", "easy_install", "easy_install-2.7", - "smtpd.py", "twine", "ufw", "unattended-upgrade"] +do_not_load_list = [ + "pip", + "pip2", + "pip3", + "pipenv", + "docker-compose", + "easy_install", + "easy_install-2.7", + "smtpd.py", + "twine", + "ufw", + "unattended-upgrade", +] def load(_): @@ -53,25 +62,38 @@ def load(_): """ # Work around https://bugs.python.org/issue32573 if not hasattr(sys, "argv"): - sys.argv = [''] + sys.argv = [""] return None + def apply_gevent_monkey_patch(): from gevent import monkey if os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS"): + def short_key(k): - return k[3:] if k.startswith('no-') else k - + return k[3:] if k.startswith("no-") else k + def key_to_bool(k): - return not k.startswith('no-') + return not k.startswith("no-") import inspect - all_accepted_patch_all_args = inspect.getfullargspec(monkey.patch_all)[0] - provided_options = os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS").replace(" ","").replace("--","").split(',') - provided_options = [k for k in provided_options if short_key(k) in all_accepted_patch_all_args] - fargs = {short_key(k): key_to_bool(k) for (k,v) in zip(provided_options, [True]*len(provided_options))} + all_accepted_patch_all_args = inspect.getfullargspec(monkey.patch_all)[0] + provided_options = ( + os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS") + .replace(" ", "") + .replace("--", "") + .split(",") + ) + provided_options = [ + k for k in provided_options if short_key(k) in all_accepted_patch_all_args + ] + + fargs = { + short_key(k): key_to_bool(k) + for (k, v) in zip(provided_options, [True] * len(provided_options)) + } monkey.patch_all(**fargs) else: monkey.patch_all() @@ -115,81 +137,95 @@ def lambda_handler(event, context): # Import the module specified in module_name handler_module = importlib.import_module(module_name) except ImportError: - print("Couldn't determine and locate default module handler: %s.%s" % (module_name, function_name)) + print( + f"Couldn't determine and locate default module handler: {module_name}.{function_name}" + ) else: # Now get the function and execute it if hasattr(handler_module, function_name): handler_function = getattr(handler_module, function_name) return handler_function(event, context) else: - print("Couldn't determine and locate default function handler: %s.%s" % (module_name, function_name)) + print( + f"Couldn't determine and locate default function handler: {module_name}.{function_name}" + ) def boot_agent(): """Initialize the Instana agent and conditionally load auto-instrumentation.""" - # Disable all the unused-import violations in this function - # pylint: disable=unused-import - # pylint: disable=import-outside-toplevel - import instana.singletons + import instana.singletons # noqa: F401 # Instrumentation if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ: - # Import & initialize instrumentation - from .instrumentation.aws import lambda_inst - - from .instrumentation import sanic_inst - - from .instrumentation import fastapi_inst - from .instrumentation import starlette_inst - - from .instrumentation import asyncio - from .instrumentation.aiohttp import client - from .instrumentation.aiohttp import server - from .instrumentation import boto3_inst - + # TODO: remove the following entries as the migration of the + # instrumentation codes are finalised. - from .instrumentation import mysqlclient - - from .instrumentation.google.cloud import storage - from .instrumentation.google.cloud import pubsub - - from .instrumentation.celery import hooks - - from .instrumentation import cassandra_inst - from .instrumentation import couchbase_inst - from .instrumentation import flask - from .instrumentation import gevent_inst - from .instrumentation import grpcio - from .instrumentation.tornado import client - from .instrumentation.tornado import server - from .instrumentation import logging - from .instrumentation import pika - from .instrumentation import pymysql - from .instrumentation import psycopg2 - from .instrumentation import redis - from .instrumentation import sqlalchemy - from .instrumentation import urllib3 - from .instrumentation.django import middleware - from .instrumentation import pymongo + # Import & initialize instrumentation + from instana.instrumentation import ( + asyncio, # noqa: F401 + boto3_inst, # noqa: F401 + cassandra_inst, # noqa: F401 + couchbase_inst, # noqa: F401 + fastapi_inst, # noqa: F401 + flask, # noqa: F401 + # gevent_inst, # noqa: F401 + # grpcio, # noqa: F401 + logging, # noqa: F401 + mysqlclient, # noqa: F401 + pika, # noqa: F401 + pep0249, # noqa: F401 + 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 + ) + from instana.instrumentation.aiohttp import ( + client, # noqa: F401 + server, # noqa: F401 + ) + + # from instana.instrumentation.aws import lambda_inst # noqa: F401 + # from instana.instrumentation.celery import hooks # noqa: F401 + from instana.instrumentation.django import middleware # noqa: F401 + # from instana.instrumentation.google.cloud import ( + # pubsub, # noqa: F401 + # storage, # noqa: F401 + # ) + # from instana.instrumentation.tornado import ( + # client, # noqa: F401 + # server, # noqa: F401 + # ) # Hooks - from .hooks import hook_uwsgi + # from instana.hooks import hook_uwsgi # noqa: F401 -if 'INSTANA_DISABLE' not in os.environ: +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. - if hasattr(sys, 'argv') and len(sys.argv) > 0 and (os.path.basename(sys.argv[0]) in do_not_load_list): + if ( + hasattr(sys, "argv") + and len(sys.argv) > 0 + and (os.path.basename(sys.argv[0]) in do_not_load_list) + ): if "INSTANA_DEBUG" in os.environ: - print("Instana: No use in monitoring this process type (%s). " - "Will go sit in a corner quietly." % os.path.basename(sys.argv[0])) + print( + f"Instana: No use in monitoring this process type ({os.path.basename(sys.argv[0])}). Will go sit in a corner quietly." + ) else: # Automatic gevent monkey patching # unless auto instrumentation is off, then the customer should do manual gevent monkey patching - if ((is_autowrapt_instrumented() or is_webhook_instrumented()) and - "INSTANA_DISABLE_AUTO_INSTR" not in os.environ and - importlib.util.find_spec("gevent")): + if ( + (is_autowrapt_instrumented() or is_webhook_instrumented()) + and "INSTANA_DISABLE_AUTO_INSTR" not in os.environ + and importlib.util.find_spec("gevent") + ): apply_gevent_monkey_patch() # AutoProfile if "INSTANA_AUTOPROFILE" in os.environ: diff --git a/src/instana/agent/host.py b/src/instana/agent/host.py index 89daff8e..b4943cdb 100644 --- a/src/instana/agent/host.py +++ b/src/instana/agent/host.py @@ -104,9 +104,6 @@ def can_send(self): Are we in a state where we can send data? @return: Boolean """ - if "INSTANA_TEST" in os.environ: - return True - # Watch for pid change (fork) self.last_fork_check = datetime.now() current_pid = os.getpid() @@ -280,11 +277,13 @@ def report_data_payload(self, payload): self.last_seen = datetime.now() # Report metrics - metric_bundle = payload["metrics"]["plugins"][0]["data"] - response = self.client.post(self.__data_url(), - data=to_json(metric_bundle), - headers={"Content-Type": "application/json"}, - timeout=0.8) + metric_count = len(payload['metrics']) + if metric_count > 0: + metric_bundle = payload["metrics"]["plugins"][0]["data"] + response = self.client.post(self.__data_url(), + data=to_json(metric_bundle), + headers={"Content-Type": "application/json"}, + timeout=0.8) if response is not None and 200 <= response.status_code <= 204: self.last_seen = datetime.now() diff --git a/src/instana/agent/test.py b/src/instana/agent/test.py deleted file mode 100644 index 06e70ec7..00000000 --- a/src/instana/agent/test.py +++ /dev/null @@ -1,26 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -""" -The in-process Instana agent (for testing & the test suite) that manages -monitoring state and reporting that data. -""" -import os -from ..log import logger -from .host import HostAgent - - -class TestAgent(HostAgent): - """ - Special Agent for the test suite. This agent is based on the StandardAgent. Overrides here are only for test - purposes and mocking. - """ - def get_from_structure(self): - """ - Retrieves the From data that is reported alongside monitoring data. - @return: dict() - """ - return {'e': os.getpid(), 'h': 'fake'} - - def report_traces(self, spans): - logger.warning("Tried to report_traces with a TestAgent!") diff --git a/src/instana/collector/aws_eks_fargate.py b/src/instana/collector/aws_eks_fargate.py index c6a2d8f0..9b0fd3c0 100644 --- a/src/instana/collector/aws_eks_fargate.py +++ b/src/instana/collector/aws_eks_fargate.py @@ -5,15 +5,17 @@ """ from time import time -from instana.log import logger + from instana.collector.base import BaseCollector from instana.collector.helpers.eks.process import EKSFargateProcessHelper from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.utils import format_span +from instana.log import logger from instana.util import DictionaryOfStan class EKSFargateCollector(BaseCollector): - """ Collector for EKS Pods on AWS Fargate """ + """Collector for EKS Pods on AWS Fargate""" def __init__(self, agent): super(EKSFargateCollector, self).__init__(agent) @@ -35,7 +37,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_span(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_fargate.py b/src/instana/collector/aws_fargate.py index 74c54c59..323ca563 100644 --- a/src/instana/collector/aws_fargate.py +++ b/src/instana/collector/aws_fargate.py @@ -4,25 +4,26 @@ """ AWS Fargate Collector: Manages the periodic collection of metrics & snapshot data """ -import os + import json +import os from time import time -import requests -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan, validate_url -from ..singletons import env_is_test +import requests -from .helpers.fargate.process import FargateProcessHelper -from .helpers.runtime import RuntimeHelper -from .helpers.fargate.task import TaskHelper -from .helpers.fargate.docker import DockerHelper -from .helpers.fargate.container import ContainerHelper +from instana.collector.base import BaseCollector +from instana.collector.helpers.fargate.container import ContainerHelper +from instana.collector.helpers.fargate.docker import DockerHelper +from instana.collector.helpers.fargate.process import FargateProcessHelper +from instana.collector.helpers.fargate.task import TaskHelper +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.utils import format_span +from instana.log import logger +from instana.util import DictionaryOfStan, validate_url class AWSFargateCollector(BaseCollector): - """ Collector for AWS Fargate """ + """Collector for AWS Fargate""" def __init__(self, agent): super(AWSFargateCollector, self).__init__(agent) @@ -35,14 +36,16 @@ def __init__(self, agent): self.ecmu = os.environ.get("ECS_CONTAINER_METADATA_URI", "") if self.ecmu == "" or validate_url(self.ecmu) is False: - logger.warning("AWSFargateCollector: ECS_CONTAINER_METADATA_URI not in environment or invalid URL. " - "Instana will not be able to monitor this environment") + logger.warning( + "AWSFargateCollector: ECS_CONTAINER_METADATA_URI not in environment or invalid URL. " + "Instana will not be able to monitor this environment" + ) self.ready_to_start = False self.ecmu_url_root = self.ecmu - self.ecmu_url_task = self.ecmu + '/task' - self.ecmu_url_stats = self.ecmu + '/stats' - self.ecmu_url_task_stats = self.ecmu + '/task/stats' + self.ecmu_url_task = self.ecmu + "/task" + self.ecmu_url_stats = self.ecmu + "/stats" + self.ecmu_url_task_stats = self.ecmu + "/task/stats" # Timestamp in seconds of the last time we fetched all ECMU data self.last_ecmu_full_fetch = 0 @@ -84,7 +87,9 @@ def __init__(self, agent): def start(self): if self.ready_to_start is False: - logger.warning("AWS Fargate Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "AWS Fargate Collector is missing requirements and cannot monitor this environment." + ) return super(AWSFargateCollector, self).start() @@ -94,10 +99,6 @@ def get_ecs_metadata(self): Get the latest data from the ECS metadata container API and store on the class @return: Boolean """ - if env_is_test is True: - # For test, we are using mock ECS metadata - return - try: self.fetching_start_time = int(time()) delta = self.fetching_start_time - self.last_ecmu_full_fetch @@ -122,7 +123,9 @@ def get_ecs_metadata(self): # Response from the last call to # ${ECS_CONTAINER_METADATA_URI}/task/stats - json_body = self.http_client.get(self.ecmu_url_task_stats, timeout=1).content + json_body = self.http_client.get( + self.ecmu_url_task_stats, timeout=1 + ).content self.task_stats_metadata = json.loads(json_body) except Exception: logger.debug("AWSFargateCollector.get_ecs_metadata", exc_info=True) @@ -137,7 +140,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_span(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_lambda.py b/src/instana/collector/aws_lambda.py index 5964e301..1d680739 100644 --- a/src/instana/collector/aws_lambda.py +++ b/src/instana/collector/aws_lambda.py @@ -4,14 +4,17 @@ """ AWS Lambda Collector: Manages the periodic collection of metrics & snapshot data """ -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan -from ..util.aws import normalize_aws_lambda_arn + +from instana.collector.base import BaseCollector +from instana.collector.utils import format_span +from instana.log import logger +from instana.util import DictionaryOfStan +from instana.util.aws import normalize_aws_lambda_arn class AWSLambdaCollector(BaseCollector): - """ Collector for AWS Lambda """ + """Collector for AWS Lambda""" + def __init__(self, agent): super(AWSLambdaCollector, self).__init__(agent) logger.debug("Loading AWS Lambda Collector") @@ -47,7 +50,7 @@ def prepare_payload(self): payload["metrics"] = None if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_span(self.queued_spans()) if self.should_send_snapshot_data(): payload["metrics"] = self.snapshot_data @@ -60,8 +63,10 @@ def get_fq_arn(self): return self._fq_arn if self.context is None: - logger.debug("Attempt to get qualified ARN before the context object is available") - return '' + logger.debug( + "Attempt to get qualified ARN before the context object is available" + ) + return "" self._fq_arn = normalize_aws_lambda_arn(self.context) return self._fq_arn diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index c1576688..99abfa3f 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -5,15 +5,12 @@ A Collector launches a background thread and continually collects & reports data. The data can be any combination of metrics, snapshot data and spans. """ -import sys -import threading - -from ..log import logger -from ..singletons import env_is_test -from ..util import every, DictionaryOfStan +import queue # pylint: disable=import-error +import threading -import queue # pylint: disable=import-error +from instana.log import logger +from instana.util import DictionaryOfStan, every class BaseCollector(object): @@ -21,6 +18,7 @@ class BaseCollector(object): Base class to handle the collection & reporting of snapshot and metric data This class launches a background thread to do this work. """ + def __init__(self, agent): # The agent for this process. Can be Standard, AWSLambda or Fargate self.agent = agent @@ -29,15 +27,7 @@ def __init__(self, agent): self.THREAD_NAME = "Instana Collector" # The Queue where we store finished spans before they are sent - if env_is_test: - # Override span queue with a multiprocessing version - # The test suite runs background applications - some in background threads, - # others in background processes. This multiprocessing queue allows us to collect - # up spans from all sources. - import multiprocessing - self.span_queue = multiprocessing.Queue() - else: - self.span_queue = queue.Queue() + self.span_queue = queue.Queue() # The Queue where we store finished profiles before they are sent self.profile_queue = queue.Queue() @@ -92,7 +82,10 @@ def start(self): timer.name = "Collector Timed Start" timer.start() return - logger.debug("BaseCollector.start non-fatal: call but thread already running (started: %s)", self.started) + logger.debug( + "BaseCollector.start non-fatal: call but thread already running (started: %s)", + self.started, + ) return if self.agent.can_send(): @@ -104,7 +97,9 @@ def start(self): self.reporting_thread.start() self.started = True else: - logger.warning("BaseCollector.start: the agent tells us we can't send anything out") + logger.warning( + "BaseCollector.start: the agent tells us we can't send anything out" + ) def shutdown(self, report_final=True): """ @@ -123,7 +118,11 @@ def thread_loop(self): Just a loop that is run in the background thread. @return: None """ - every(self.report_interval, self.background_report, "Instana Collector: prepare_and_report_data") + every( + self.report_interval, + self.background_report, + "Instana Collector: prepare_and_report_data", + ) def background_report(self): """ @@ -131,15 +130,13 @@ def background_report(self): @return: Boolean """ if self.thread_shutdown.is_set(): - logger.debug("Thread shutdown signal is active: Shutting down reporting thread") + logger.debug( + "Thread shutdown signal is active: Shutting down reporting thread" + ) return False self.prepare_and_report_data() - if self.thread_shutdown.is_set(): - logger.debug("Thread shutdown signal is active: Shutting down reporting thread") - return False - return True def prepare_and_report_data(self): @@ -147,8 +144,6 @@ def prepare_and_report_data(self): Prepare and report the data payload. @return: Boolean """ - if env_is_test: - return True with self.background_report_lock: payload = self.prepare_payload() self.agent.report_data_payload(payload) @@ -188,7 +183,6 @@ def queued_spans(self): spans.append(span) return spans - def queued_profiles(self): """ Get all of the queued profiles diff --git a/src/instana/collector/google_cloud_run.py b/src/instana/collector/google_cloud_run.py index 27f5ec29..65fdad02 100644 --- a/src/instana/collector/google_cloud_run.py +++ b/src/instana/collector/google_cloud_run.py @@ -4,19 +4,24 @@ """ Google Cloud Run Collector: Manages the periodic collection of metrics & snapshot data """ + import os from time import time + import requests -from instana.log import logger from instana.collector.base import BaseCollector -from instana.util import DictionaryOfStan, validate_url +from instana.collector.helpers.google_cloud_run.instance_entity import ( + InstanceEntityHelper, +) from instana.collector.helpers.google_cloud_run.process import GCRProcessHelper -from instana.collector.helpers.google_cloud_run.instance_entity import InstanceEntityHelper +from instana.collector.utils import format_span +from instana.log import logger +from instana.util import DictionaryOfStan, validate_url class GCRCollector(BaseCollector): - """ Collector for Google Cloud Run """ + """Collector for Google Cloud Run""" def __init__(self, agent, service, configuration, revision): super(GCRCollector, self).__init__(agent) @@ -29,15 +34,23 @@ def __init__(self, agent, service, configuration, revision): self.service = service self.configuration = configuration # Prepare the URLS that we will collect data from - self._gcr_md_uri = os.environ.get("GOOGLE_CLOUD_RUN_METADATA_ENDPOINT", "http://metadata.google.internal") + self._gcr_md_uri = os.environ.get( + "GOOGLE_CLOUD_RUN_METADATA_ENDPOINT", "http://metadata.google.internal" + ) if self._gcr_md_uri == "" or validate_url(self._gcr_md_uri) is False: - logger.warning("GCRCollector: GOOGLE_CLOUD_RUN_METADATA_ENDPOINT not in environment or invalid URL. " - "Instana will not be able to monitor this environment") + logger.warning( + "GCRCollector: GOOGLE_CLOUD_RUN_METADATA_ENDPOINT not in environment or invalid URL. " + "Instana will not be able to monitor this environment" + ) self.ready_to_start = False - self._gcr_md_project_uri = self._gcr_md_uri + '/computeMetadata/v1/project/?recursive=true' - self._gcr_md_instance_uri = self._gcr_md_uri + '/computeMetadata/v1/instance/?recursive=true' + self._gcr_md_project_uri = ( + self._gcr_md_uri + "/computeMetadata/v1/project/?recursive=true" + ) + self._gcr_md_instance_uri = ( + self._gcr_md_uri + "/computeMetadata/v1/instance/?recursive=true" + ) # Timestamp in seconds of the last time we fetched all GCR metadata self.__last_gcr_md_full_fetch = 0 @@ -65,7 +78,9 @@ def __init__(self, agent, service, configuration, revision): def start(self): if self.ready_to_start is False: - logger.warning("Google Cloud Run Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "Google Cloud Run Collector is missing requirements and cannot monitor this environment." + ) return super(GCRCollector, self).start() @@ -81,15 +96,19 @@ def __get_project_instance_metadata(self): headers = {"Metadata-Flavor": "Google"} # Response from the last call to # ${GOOGLE_CLOUD_RUN_METADATA_ENDPOINT}/computeMetadata/v1/project/?recursive=true - self.project_metadata = self._http_client.get(self._gcr_md_project_uri, timeout=1, - headers=headers).json() + self.project_metadata = self._http_client.get( + self._gcr_md_project_uri, timeout=1, headers=headers + ).json() # Response from the last call to # ${GOOGLE_CLOUD_RUN_METADATA_ENDPOINT}/computeMetadata/v1/instance/?recursive=true - self.instance_metadata = self._http_client.get(self._gcr_md_instance_uri, timeout=1, - headers=headers).json() + self.instance_metadata = self._http_client.get( + self._gcr_md_instance_uri, timeout=1, headers=headers + ).json() except Exception: - logger.debug("GoogleCloudRunCollector.get_project_instance_metadata", exc_info=True) + logger.debug( + "GoogleCloudRunCollector.get_project_instance_metadata", exc_info=True + ) def should_send_snapshot_data(self): return int(time()) - self.snapshot_data_last_sent > self.snapshot_data_interval @@ -100,9 +119,8 @@ def prepare_payload(self): payload["metrics"]["plugins"] = [] try: - if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_span(self.queued_spans()) self.fetching_start_time = int(time()) delta = self.fetching_start_time - self.__last_gcr_md_full_fetch @@ -119,8 +137,12 @@ def prepare_payload(self): plugins = [] for helper in self.helpers: plugins.extend( - helper.collect_metrics(with_snapshot=with_snapshot, instance_metadata=self.instance_metadata, - project_metadata=self.project_metadata)) + helper.collect_metrics( + with_snapshot=with_snapshot, + instance_metadata=self.instance_metadata, + project_metadata=self.project_metadata, + ) + ) payload["metrics"]["plugins"] = plugins diff --git a/src/instana/collector/helpers/base.py b/src/instana/collector/helpers/base.py index 682617a2..e2cea3d5 100644 --- a/src/instana/collector/helpers/base.py +++ b/src/instana/collector/helpers/base.py @@ -6,13 +6,15 @@ in the data collection for various entities such as host, hardware, AWS Task, ec2, memory, cpu, docker etc etc.. """ -from ...log import logger + +from instana.log import logger class BaseHelper(object): """ Base class for all helpers. Descendants must override and implement `self.collect_metrics`. """ + def __init__(self, collector): self.collector = collector @@ -73,6 +75,6 @@ def apply_delta(self, source, previous, new, metric, with_snapshot): if previous_value != new_value or with_snapshot is True: previous[dst_metric] = new[dst_metric] = new_value - + def collect_metrics(self, **kwargs): logger.debug("BaseHelper.collect_metrics must be overridden") diff --git a/src/instana/collector/helpers/eks/process.py b/src/instana/collector/helpers/eks/process.py index 09198532..86239520 100644 --- a/src/instana/collector/helpers/eks/process.py +++ b/src/instana/collector/helpers/eks/process.py @@ -1,13 +1,15 @@ # (c) Copyright IBM Corp. 2024 -""" Module to handle the collection of containerized process metrics for EKS Pods on AWS Fargate """ +"""Module to handle the collection of containerized process metrics for EKS Pods on AWS Fargate""" + import os + from instana.collector.helpers.process import ProcessHelper from instana.log import logger def get_pod_name(): - podname = os.environ.get('HOSTNAME', '') + podname = os.environ.get("HOSTNAME", "") if not podname: logger.warning("Failed to determine podname from EKS hostname.") @@ -15,7 +17,7 @@ def get_pod_name(): class EKSFargateProcessHelper(ProcessHelper): - """ Helper class to extend the generic process helper class with the corresponding fargate attributes """ + """Helper class to extend the generic process helper class with the corresponding fargate attributes""" def collect_metrics(self, **kwargs): plugin_data = dict() diff --git a/src/instana/collector/helpers/fargate/container.py b/src/instana/collector/helpers/fargate/container.py index 90981298..82ad7bea 100644 --- a/src/instana/collector/helpers/fargate/container.py +++ b/src/instana/collector/helpers/fargate/container.py @@ -1,14 +1,16 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -""" Module to handle the collection of container metrics in AWS Fargate """ -from ....log import logger -from ....util import DictionaryOfStan -from ..base import BaseHelper +"""Module to handle the collection of container metrics in AWS Fargate""" + +from instana.collector.helpers.base import BaseHelper +from instana.log import logger +from instana.util import DictionaryOfStan class ContainerHelper(BaseHelper): - """ This class acts as a helper to collect container snapshot and metric information """ + """This class acts as a helper to collect container snapshot and metric information""" + def collect_metrics(self, **kwargs): """ Collect and return metrics (and optionally snapshot data) for every container in this task @@ -31,27 +33,55 @@ def collect_metrics(self, **kwargs): plugin_data["data"] = DictionaryOfStan() if self.collector.root_metadata["Name"] == name: plugin_data["data"]["instrumented"] = True - plugin_data["data"]["dockerId"] = container.get("DockerId", None) - plugin_data["data"]["taskArn"] = labels.get("com.amazonaws.ecs.task-arn", None) + plugin_data["data"]["dockerId"] = container.get( + "DockerId", None + ) + plugin_data["data"]["taskArn"] = labels.get( + "com.amazonaws.ecs.task-arn", None + ) if kwargs.get("with_snapshot"): plugin_data["data"]["runtime"] = "python" - plugin_data["data"]["dockerName"] = container.get("DockerName", None) - plugin_data["data"]["containerName"] = container.get("Name", None) + plugin_data["data"]["dockerName"] = container.get( + "DockerName", None + ) + plugin_data["data"]["containerName"] = container.get( + "Name", None + ) plugin_data["data"]["image"] = container.get("Image", None) - plugin_data["data"]["imageId"] = container.get("ImageID", None) - plugin_data["data"]["taskDefinition"] = labels.get("com.amazonaws.ecs.task-definition-family", None) - plugin_data["data"]["taskDefinitionVersion"] = labels.get("com.amazonaws.ecs.task-definition-version", None) - plugin_data["data"]["clusterArn"] = labels.get("com.amazonaws.ecs.cluster", None) - plugin_data["data"]["desiredStatus"] = container.get("DesiredStatus", None) - plugin_data["data"]["knownStatus"] = container.get("KnownStatus", None) + plugin_data["data"]["imageId"] = container.get( + "ImageID", None + ) + plugin_data["data"]["taskDefinition"] = labels.get( + "com.amazonaws.ecs.task-definition-family", None + ) + plugin_data["data"]["taskDefinitionVersion"] = labels.get( + "com.amazonaws.ecs.task-definition-version", None + ) + plugin_data["data"]["clusterArn"] = labels.get( + "com.amazonaws.ecs.cluster", None + ) + plugin_data["data"]["desiredStatus"] = container.get( + "DesiredStatus", None + ) + plugin_data["data"]["knownStatus"] = container.get( + "KnownStatus", None + ) plugin_data["data"]["ports"] = container.get("Ports", None) - plugin_data["data"]["createdAt"] = container.get("CreatedAt", None) - plugin_data["data"]["startedAt"] = container.get("StartedAt", None) + plugin_data["data"]["createdAt"] = container.get( + "CreatedAt", None + ) + plugin_data["data"]["startedAt"] = container.get( + "StartedAt", None + ) plugin_data["data"]["type"] = container.get("Type", None) limits = container.get("Limits", {}) - plugin_data["data"]["limits"]["cpu"] = limits.get("CPU", None) - plugin_data["data"]["limits"]["memory"] = limits.get("Memory", None) + plugin_data["data"]["limits"]["cpu"] = limits.get( + "CPU", None + ) + plugin_data["data"]["limits"]["memory"] = limits.get( + "Memory", None + ) except Exception: logger.debug("_collect_container_snapshots: ", exc_info=True) finally: diff --git a/src/instana/collector/helpers/process.py b/src/instana/collector/helpers/process.py index 073842f2..2f2115cb 100644 --- a/src/instana/collector/helpers/process.py +++ b/src/instana/collector/helpers/process.py @@ -1,19 +1,21 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -""" Collection helper for the process """ +"""Collection helper for the process""" + +import grp import os import pwd -import grp + +from instana.collector.helpers.base import BaseHelper from instana.log import logger from instana.util import DictionaryOfStan from instana.util.runtime import get_proc_cmdline from instana.util.secrets import contains_secret -from .base import BaseHelper class ProcessHelper(BaseHelper): - """ Helper class to collect metrics for this process """ + """Helper class to collect metrics for this process""" def collect_metrics(self, **kwargs): plugin_data = dict() @@ -33,9 +35,11 @@ def _collect_process_snapshot(self, plugin_data): try: env = dict() for key in os.environ: - if contains_secret(key, - self.collector.agent.options.secrets_matcher, - self.collector.agent.options.secrets_list): + if contains_secret( + key, + self.collector.agent.options.secrets_matcher, + self.collector.agent.options.secrets_list, + ): env[key] = "" else: env[key] = os.environ[key] diff --git a/src/instana/collector/helpers/runtime.py b/src/instana/collector/helpers/runtime.py index f6111bb0..8aef48e3 100644 --- a/src/instana/collector/helpers/runtime.py +++ b/src/instana/collector/helpers/runtime.py @@ -2,21 +2,22 @@ # (c) Copyright Instana Inc. 2020 """ Collection helper for the Python runtime """ +import gc import importlib.metadata import os -import gc -import sys import platform import resource +import sys import threading from types import ModuleType +from instana.collector.helpers.base import BaseHelper from instana.log import logger -from instana.version import VERSION from instana.util import DictionaryOfStan from instana.util.runtime import determine_service_name +from instana.version import VERSION -from .base import BaseHelper +PATH_OF_DEPRECATED_INSTALLATION_VIA_HOST_AGENT = "/tmp/.instana/python" PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR = '/opt/instana/instrumentation/python/' @@ -29,7 +30,7 @@ def is_webhook_instrumented(): class RuntimeHelper(BaseHelper): - """ Helper class to collect snapshot and metrics for this Python runtime """ + """Helper class to collect snapshot and metrics for this Python runtime""" def __init__(self, collector): super(RuntimeHelper, self).__init__(collector) @@ -66,7 +67,7 @@ def collect_metrics(self, **kwargs): return [plugin_data] def _collect_runtime_metrics(self, plugin_data, with_snapshot): - if os.environ.get('INSTANA_DISABLE_METRICS_COLLECTION', False): + if os.environ.get("INSTANA_DISABLE_METRICS_COLLECTION", False): return """ Collect up and return the runtime metrics """ @@ -78,61 +79,141 @@ def _collect_runtime_metrics(self, plugin_data, with_snapshot): self._collect_thread_metrics(plugin_data, with_snapshot) value_diff = rusage.ru_utime - self.previous_rusage.ru_utime - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_utime", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_utime", + with_snapshot, + ) value_diff = rusage.ru_stime - self.previous_rusage.ru_stime - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_stime", with_snapshot) - - self.apply_delta(rusage.ru_maxrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_maxrss", with_snapshot) - self.apply_delta(rusage.ru_ixrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_ixrss", with_snapshot) - self.apply_delta(rusage.ru_idrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_idrss", with_snapshot) - self.apply_delta(rusage.ru_isrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_isrss", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_stime", + with_snapshot, + ) + + self.apply_delta( + rusage.ru_maxrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_maxrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_ixrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_ixrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_idrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_idrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_isrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_isrss", + with_snapshot, + ) value_diff = rusage.ru_minflt - self.previous_rusage.ru_minflt - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_minflt", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_minflt", + with_snapshot, + ) value_diff = rusage.ru_majflt - self.previous_rusage.ru_majflt - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_majflt", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_majflt", + with_snapshot, + ) value_diff = rusage.ru_nswap - self.previous_rusage.ru_nswap - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nswap", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nswap", + with_snapshot, + ) value_diff = rusage.ru_inblock - self.previous_rusage.ru_inblock - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_inblock", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_inblock", + with_snapshot, + ) value_diff = rusage.ru_oublock - self.previous_rusage.ru_oublock - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_oublock", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_oublock", + with_snapshot, + ) value_diff = rusage.ru_msgsnd - self.previous_rusage.ru_msgsnd - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_msgsnd", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_msgsnd", + with_snapshot, + ) value_diff = rusage.ru_msgrcv - self.previous_rusage.ru_msgrcv - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_msgrcv", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_msgrcv", + with_snapshot, + ) value_diff = rusage.ru_nsignals - self.previous_rusage.ru_nsignals - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nsignals", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nsignals", + with_snapshot, + ) value_diff = rusage.ru_nvcsw - self.previous_rusage.ru_nvcsw - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nvcsw", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nvcsw", + with_snapshot, + ) value_diff = rusage.ru_nivcsw - self.previous_rusage.ru_nivcsw - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nivcsw", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nivcsw", + with_snapshot, + ) except Exception: logger.debug("_collect_runtime_metrics", exc_info=True) finally: @@ -143,19 +224,49 @@ def _collect_gc_metrics(self, plugin_data, with_snapshot): gc_count = gc.get_count() gc_threshold = gc.get_threshold() - self.apply_delta(gc_count[0], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect0", with_snapshot) - self.apply_delta(gc_count[1], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect1", with_snapshot) - self.apply_delta(gc_count[2], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect2", with_snapshot) - - self.apply_delta(gc_threshold[0], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold0", with_snapshot) - self.apply_delta(gc_threshold[1], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold1", with_snapshot) - self.apply_delta(gc_threshold[2], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold2", with_snapshot) + self.apply_delta( + gc_count[0], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect0", + with_snapshot, + ) + self.apply_delta( + gc_count[1], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect1", + with_snapshot, + ) + self.apply_delta( + gc_count[2], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect2", + with_snapshot, + ) + + self.apply_delta( + gc_threshold[0], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold0", + with_snapshot, + ) + self.apply_delta( + gc_threshold[1], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold1", + with_snapshot, + ) + self.apply_delta( + gc_threshold[2], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold2", + with_snapshot, + ) except Exception: logger.debug("_collect_gc_metrics", exc_info=True) @@ -163,55 +274,77 @@ def _collect_thread_metrics(self, plugin_data, with_snapshot): try: threads = threading.enumerate() daemon_threads = [thread.daemon is True for thread in threads].count(True) - self.apply_delta(daemon_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "daemon_threads", with_snapshot) + self.apply_delta( + daemon_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "daemon_threads", + with_snapshot, + ) alive_threads = [thread.daemon is False for thread in threads].count(True) - self.apply_delta(alive_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "alive_threads", with_snapshot) - - dummy_threads = [isinstance(thread, threading._DummyThread) for thread in threads].count( - True) # pylint: disable=protected-access - self.apply_delta(dummy_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "dummy_threads", with_snapshot) + self.apply_delta( + alive_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "alive_threads", + with_snapshot, + ) + + dummy_threads = [ + isinstance(thread, threading._DummyThread) for thread in threads + ].count(True) # pylint: disable=protected-access + self.apply_delta( + dummy_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "dummy_threads", + with_snapshot, + ) except Exception: logger.debug("_collect_thread_metrics", exc_info=True) def _collect_runtime_snapshot(self, plugin_data): - """ Gathers Python specific Snapshot information for this process """ + """Gathers Python specific Snapshot information for this process""" snapshot_payload = {} try: - snapshot_payload['name'] = determine_service_name() - snapshot_payload['version'] = sys.version - snapshot_payload['f'] = platform.python_implementation() # flavor - snapshot_payload['a'] = platform.architecture()[0] # architecture - snapshot_payload['versions'] = self.gather_python_packages() - snapshot_payload['iv'] = VERSION + snapshot_payload["name"] = determine_service_name() + snapshot_payload["version"] = sys.version + snapshot_payload["f"] = platform.python_implementation() # flavor + snapshot_payload["a"] = platform.architecture()[0] # architecture + snapshot_payload["versions"] = self.gather_python_packages() + snapshot_payload["iv"] = VERSION if is_autowrapt_instrumented(): snapshot_payload['m'] = 'Autowrapt' elif is_webhook_instrumented(): snapshot_payload['m'] = 'AutoTrace' else: - snapshot_payload['m'] = 'Manual' + snapshot_payload["m"] = "Manual" try: - from django.conf import settings # pylint: disable=import-outside-toplevel - if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: - snapshot_payload['djmw'] = settings.MIDDLEWARE - elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: - snapshot_payload['djmw'] = settings.MIDDLEWARE_CLASSES + from django.conf import ( + settings, # pylint: disable=import-outside-toplevel + ) + + if hasattr(settings, "MIDDLEWARE") and settings.MIDDLEWARE is not None: + snapshot_payload["djmw"] = settings.MIDDLEWARE + elif ( + hasattr(settings, "MIDDLEWARE_CLASSES") + and settings.MIDDLEWARE_CLASSES is not None + ): + snapshot_payload["djmw"] = settings.MIDDLEWARE_CLASSES except Exception: pass except Exception: logger.debug("collect_snapshot: ", exc_info=True) - plugin_data['data']['snapshot'] = snapshot_payload + plugin_data["data"]["snapshot"] = snapshot_payload def gather_python_packages(self): - """ Collect up the list of modules in use """ - if os.environ.get('INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION'): - return {'instana': VERSION} + """Collect up the list of modules in use""" + if os.environ.get("INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"): + return {"instana": VERSION} versions = {} try: @@ -220,7 +353,7 @@ def gather_python_packages(self): for pkg_name in sys_packages: # Don't report submodules (e.g. django.x, django.y, django.z) # Skip modules that begin with underscore - if ('.' in pkg_name) or pkg_name[0] == '_': + if ("." in pkg_name) or pkg_name[0] == "_": continue # Skip builtins @@ -234,7 +367,9 @@ def gather_python_packages(self): if isinstance(pkg_info["__version__"], str): versions[pkg_name] = pkg_info["__version__"] else: - versions[pkg_name] = self.jsonable(pkg_info["__version__"]) + versions[pkg_name] = self.jsonable( + pkg_info["__version__"] + ) elif "version" in pkg_info: versions[pkg_name] = self.jsonable(pkg_info["version"]) else: @@ -242,10 +377,13 @@ def gather_python_packages(self): except importlib.metadata.PackageNotFoundError: pass except Exception: - logger.debug("gather_python_packages: could not process module: %s", pkg_name) + logger.debug( + "gather_python_packages: could not process module: %s", + pkg_name, + ) # Manually set our package version - versions['instana'] = VERSION + versions["instana"] = VERSION except Exception: logger.debug("gather_python_packages", exc_info=True) @@ -257,7 +395,7 @@ def jsonable(self, value): try: result = value() except Exception: - result = 'Unknown' + result = "Unknown" elif isinstance(value, ModuleType): result = value else: diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index 2ec2bd8b..dfb2aacd 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -4,16 +4,21 @@ """ Host Collector: Manages the periodic collection of metrics & snapshot data """ + from time import time -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan -from .helpers.runtime import RuntimeHelper +from typing import DefaultDict, Any + +from instana.collector.base import BaseCollector +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.utils import format_span +from instana.log import logger +from instana.util import DictionaryOfStan class HostCollector(BaseCollector): - """ Collector for host agent """ - def __init__(self, agent): + """Collector for host agent""" + + def __init__(self, agent) -> None: super(HostCollector, self).__init__(agent) logger.debug("Loading Host Collector") @@ -23,14 +28,16 @@ def __init__(self, agent): # Populate the collection helpers self.helpers.append(RuntimeHelper(self)) - def start(self): + def start(self) -> None: if self.ready_to_start is False: - logger.warning("Host Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "Host Collector is missing requirements and cannot monitor this environment." + ) return super(HostCollector, self).start() - def prepare_and_report_data(self): + def prepare_and_report_data(self) -> None: """ We override this method from the base class so that we can handle the wait4init state machine case. @@ -45,21 +52,28 @@ def prepare_and_report_data(self): else: return - if self.agent.machine.fsm.current == "good2go" and self.agent.is_timed_out(): - logger.info("The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically.") + if ( + self.agent.machine.fsm.current == "good2go" + and self.agent.is_timed_out() + ): + logger.info( + "The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically." + ) self.agent.reset() except Exception: - logger.debug('Harmless state machine thread disagreement. Will self-correct on next timer cycle.') + logger.debug( + "Harmless state machine thread disagreement. Will self-correct on next timer cycle." + ) super(HostCollector, self).prepare_and_report_data() - def should_send_snapshot_data(self): + def should_send_snapshot_data(self) -> bool: delta = int(time()) - self.snapshot_data_last_sent if delta > self.snapshot_data_interval: return True return False - def prepare_payload(self): + def prepare_payload(self) -> DefaultDict[Any, Any]: payload = DictionaryOfStan() payload["spans"] = [] payload["profiles"] = [] @@ -67,7 +81,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_span(self.queued_spans()) if not self.profile_queue.empty(): payload["profiles"] = self.queued_profiles() diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py new file mode 100644 index 00000000..74e3021f --- /dev/null +++ b/src/instana/collector/utils.py @@ -0,0 +1,28 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import TYPE_CHECKING, Type, List + +from opentelemetry.trace.span import format_span_id +from opentelemetry.trace import SpanKind + +if TYPE_CHECKING: + from instana.span.base_span import BaseSpan + + +def format_span( + queued_spans: List[Type["BaseSpan"]], +) -> List[Type["BaseSpan"]]: + """ + Format Span Kind and the Trace, Parent Span and Span IDs of the Spans to be a 64-bit + Hexadecimal String instead of Integer before being pushed to a + Collector (or Instana Agent). + """ + spans = [] + for span in queued_spans: + span.t = format_span_id(span.t) + span.s = format_span_id(span.s) + span.p = format_span_id(span.p) if span.p else None + if isinstance(span.k, SpanKind): + span.k = span.k.value if span.k is not SpanKind.INTERNAL else 3 + spans.append(span) + return spans diff --git a/src/instana/configurator.py b/src/instana/configurator.py index 167aa4c1..65efb35d 100644 --- a/src/instana/configurator.py +++ b/src/instana/configurator.py @@ -5,7 +5,8 @@ This file contains a config object that will hold configuration options for the package. Defaults are set and can be overridden after package load. """ -from .util import DictionaryOfStan + +from instana.util import DictionaryOfStan # La Protagonista config = DictionaryOfStan() @@ -13,8 +14,4 @@ # This option determines if tasks created via asyncio (with ensure_future or create_task) will # automatically carry existing context into the created task. -config['asyncio_task_context_propagation']['enabled'] = False - - - - +config["asyncio_task_context_propagation"]["enabled"] = False diff --git a/src/instana/fsm.py b/src/instana/fsm.py index 3cdd25ee..1897cf30 100644 --- a/src/instana/fsm.py +++ b/src/instana/fsm.py @@ -67,10 +67,7 @@ def __init__(self, agent): self.timer = threading.Timer(1, self.fsm.lookup) self.timer.daemon = True self.timer.name = self.THREAD_NAME - - # Only start the announce process when not in Test - if not "INSTANA_TEST" in os.environ: - self.timer.start() + self.timer.start() @staticmethod def print_state_change(e): diff --git a/src/instana/instrumentation/aiohttp/client.py b/src/instana/instrumentation/aiohttp/client.py index 3b5b4eb1..4b307dc4 100644 --- a/src/instana/instrumentation/aiohttp/client.py +++ b/src/instana/instrumentation/aiohttp/client.py @@ -2,85 +2,111 @@ # (c) Copyright Instana Inc. 2019 -import opentracing +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple import wrapt -from ...log import logger -from ...singletons import agent, async_tracer -from ...util.secrets import strip_secrets_from_query -from ...util.traceutils import tracing_is_off +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent +from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import aiohttp - import asyncio + if TYPE_CHECKING: + from aiohttp.client import ClientSession + from instana.span.span import InstanaSpan - async def stan_request_start(session, trace_config_ctx, params): + async def stan_request_start( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: # If we're not tracing, just return if tracing_is_off(): - trace_config_ctx.scope = None + trace_config_ctx.span_context = None return - scope = async_tracer.start_active_span("aiohttp-client", child_of=async_tracer.active_span) - trace_config_ctx.scope = scope + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None + + span = tracer.start_span("aiohttp-client", span_context=parent_context) - async_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, params.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, params.headers) - parts = str(params.url).split('?') + parts = str(params.url).split("?") if len(parts) > 1: - cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", cleaned_qp) - scope.span.set_tag("http.url", parts[0]) - scope.span.set_tag('http.method', params.method) + cleaned_qp = strip_secrets_from_query( + parts[1], agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", cleaned_qp) + span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) + span.set_attribute(SpanAttributes.HTTP_METHOD, params.method) + trace_config_ctx.span_context = span except Exception: - logger.debug("stan_request_start", exc_info=True) + logger.debug("aiohttp-client stan_request_start error:", exc_info=True) - - async def stan_request_end(session, trace_config_ctx, params): + async def stan_request_end( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: - scope = trace_config_ctx.scope - if scope is not None: - scope.span.set_tag('http.status_code', params.response.status) + span: "InstanaSpan" = trace_config_ctx.span_context + if span: + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, params.response.status + ) - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: for custom_header in agent.options.extra_http_headers: if custom_header in params.response.headers: - scope.span.set_tag("http.header.%s" % custom_header, params.response.headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, + params.response.headers[custom_header], + ) if 500 <= params.response.status: - scope.span.mark_as_errored({"http.error": params.response.reason}) + span.mark_as_errored({"http.error": params.response.reason}) - scope.close() + if span.is_recording(): + span.end() + trace_config_ctx = None except Exception: - logger.debug("stan_request_end", exc_info=True) - + logger.debug("aiohttp-client stan_request_end error:", exc_info=True) - async def stan_request_exception(session, trace_config_ctx, params): + async def stan_request_exception( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: - scope = trace_config_ctx.scope - if scope is not None: - scope.span.log_exception(params.exception) - scope.span.set_tag("http.error", str(params.exception)) - scope.close() + span: "InstanaSpan" = trace_config_ctx.span_context + if span: + span.record_exception(params.exception) + span.set_attribute("http.error", str(params.exception)) + if span.is_recording(): + span.end() + trace_config_ctx = None except Exception: - logger.debug("stan_request_exception", exc_info=True) - - - @wrapt.patch_function_wrapper('aiohttp.client', 'ClientSession.__init__') - def init_with_instana(wrapped, instance, argv, kwargs): + logger.debug("aiohttp-client stan_request_exception error:", exc_info=True) + + @wrapt.patch_function_wrapper("aiohttp.client", "ClientSession.__init__") + def init_with_instana( + wrapped: Callable[..., Awaitable["ClientSession"]], + instance: aiohttp.client.ClientSession, + args: Tuple[int, str, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: instana_trace_config = aiohttp.TraceConfig() instana_trace_config.on_request_start.append(stan_request_start) instana_trace_config.on_request_end.append(stan_request_end) instana_trace_config.on_request_exception.append(stan_request_exception) - if 'trace_configs' in kwargs: - kwargs['trace_configs'].append(instana_trace_config) + if "trace_configs" in kwargs: + kwargs["trace_configs"].append(instana_trace_config) else: - kwargs['trace_configs'] = [instana_trace_config] - - return wrapped(*argv, **kwargs) + kwargs["trace_configs"] = [instana_trace_config] + return wrapped(*args, **kwargs) logger.debug("Instrumenting aiohttp client") except ImportError: diff --git a/src/instana/instrumentation/aiohttp/server.py b/src/instana/instrumentation/aiohttp/server.py index 93dcb256..3036e81d 100644 --- a/src/instana/instrumentation/aiohttp/server.py +++ b/src/instana/instrumentation/aiohttp/server.py @@ -2,44 +2,58 @@ # (c) Copyright Instana Inc. 2019 -import opentracing +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple + import wrapt +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query -from ...log import logger -from ...singletons import agent, async_tracer -from ...util.secrets import strip_secrets_from_query +if TYPE_CHECKING: + from instana.span.span import InstanaSpan try: import aiohttp - import asyncio - from aiohttp.web import middleware + if TYPE_CHECKING: + import aiohttp.web @middleware - async def stan_middleware(request, handler): + async def stan_middleware( + request: "aiohttp.web.Request", + handler: Callable[..., object], + ) -> Awaitable["aiohttp.web.Response"]: try: - ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, request.headers) - request['scope'] = async_tracer.start_active_span('aiohttp-server', child_of=ctx) - scope = request['scope'] + span_context = tracer.extract(Format.HTTP_HEADERS, request.headers) + span: "InstanaSpan" = tracer.start_span( + "aiohttp-server", span_context=span_context + ) + request["span"] = span # Query param scrubbing url = str(request.url) - parts = url.split('?') + parts = url.split("?") if len(parts) > 1: - cleaned_qp = strip_secrets_from_query(parts[1], - agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", cleaned_qp) + cleaned_qp = strip_secrets_from_query( + parts[1], agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", cleaned_qp) - scope.span.set_tag("http.url", parts[0]) - scope.span.set_tag("http.method", request.method) + span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) # Custom header tracking support - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: for custom_header in agent.options.extra_http_headers: if custom_header in request.headers: - scope.span.set_tag("http.header.%s" % custom_header, request.headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, + request.headers[custom_header], + ) response = None try: @@ -52,33 +66,38 @@ async def stan_middleware(request, handler): if response is not None: # Mark 500 responses as errored if 500 <= response.status: - scope.span.mark_as_errored() + span.mark_as_errored() - scope.span.set_tag("http.status_code", response.status) - async_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + f"intid;desc={span.context.trace_id}" + ) return response except Exception as exc: - logger.debug("aiohttp stan_middleware", exc_info=True) - if scope is not None: - scope.span.set_tag("http.status_code", 500) - scope.span.log_exception(exc) + logger.debug("aiohttp server stan_middleware:", exc_info=True) + if span: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + span.record_exception(exc) raise finally: - if scope is not None: - scope.close() - - - @wrapt.patch_function_wrapper('aiohttp.web', 'Application.__init__') - def init_with_instana(wrapped, instance, argv, kwargs): + if span and span.is_recording(): + span.end() + + @wrapt.patch_function_wrapper("aiohttp.web", "Application.__init__") + def init_with_instana( + wrapped: Callable[..., "aiohttp.web.Application.__init__"], + instance: "aiohttp.web.Application", + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> object: if "middlewares" in kwargs: kwargs["middlewares"].insert(0, stan_middleware) else: kwargs["middlewares"] = [stan_middleware] - return wrapped(*argv, **kwargs) - + return wrapped(*args, **kwargs) logger.debug("Instrumenting aiohttp server") except ImportError: diff --git a/src/instana/instrumentation/asgi.py b/src/instana/instrumentation/asgi.py index 27c20e9e..ed0866ae 100644 --- a/src/instana/instrumentation/asgi.py +++ b/src/instana/instrumentation/asgi.py @@ -4,11 +4,20 @@ """ Instana ASGI Middleware """ -import opentracing -from ..log import logger -from ..singletons import async_tracer, agent -from ..util.secrets import strip_secrets_from_query +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict + +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query + +if TYPE_CHECKING: + from starlette.middleware.exceptions import ExceptionMiddleware + from instana.span.span import InstanaSpan class InstanaASGIMiddleware: @@ -16,94 +25,115 @@ class InstanaASGIMiddleware: Instana ASGI Middleware """ - def __init__(self, app): + def __init__(self, app: "ExceptionMiddleware") -> None: self.app = app - def _extract_custom_headers(self, span, headers): + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: if agent.options.extra_http_headers is None: - return + return try: for custom_header in agent.options.extra_http_headers: # Headers are in the following format: b'x-header-1' for header_pair in headers: - if header_pair[0].decode('utf-8').lower() == custom_header.lower(): - span.set_tag("http.header.%s" % custom_header, header_pair[1].decode('utf-8')) + if header_pair[0].decode("utf-8").lower() == custom_header.lower(): + span.set_attribute( + f"http.header.{custom_header}", + header_pair[1].decode("utf-8"), + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def _collect_kvs(self, scope, span): + def _collect_kvs(self, scope: Dict[str, Any], span: "InstanaSpan") -> None: try: - span.set_tag('span.kind', 'entry') - span.set_tag('http.path', scope.get('path')) - span.set_tag('http.method', scope.get('method')) + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.path", scope.get("path")) + span.set_attribute(SpanAttributes.HTTP_METHOD, scope.get("method")) - server = scope.get('server') - if isinstance(server, tuple): - span.set_tag('http.host', server[0]) + server = scope.get("server") + if isinstance(server, tuple) or isinstance(server, list): + span.set_attribute(SpanAttributes.HTTP_HOST, server[0]) - query = scope.get('query_string') + query = scope.get("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) - span.set_tag("http.params", scrubbed_params) - - app = scope.get('app') - if app is not None and hasattr(app, 'routes'): + query = query.decode("utf-8") + scrubbed_params = strip_secrets_from_query( + query, agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", scrubbed_params) + + app = scope.get("app") + if app and hasattr(app, "routes"): # Attempt to detect the Starlette routes registered. # If Starlette isn't present, we harmlessly dump out. from starlette.routing import Match - for route in scope['app'].routes: + + for route in scope["app"].routes: if route.matches(scope)[0] == Match.FULL: - span.set_tag("http.path_tpl", route.path) + span.set_attribute("http.path_tpl", route.path) except Exception: logger.debug("ASGI collect_kvs: ", exc_info=True) - async def __call__(self, scope, receive, send): + async def __call__( + self, + scope: Dict[str, Any], + receive: Callable[[], Awaitable[Dict[str, Any]]], + send: Callable[[Dict[str, Any]], Awaitable[None]], + ) -> None: request_context = None if scope["type"] not in ("http", "websocket"): - await self.app(scope, receive, send) - return + return await self.app(scope, receive, send) - request_headers = scope.get('headers') + request_headers = scope.get("headers") if isinstance(request_headers, list): - request_context = async_tracer.extract(opentracing.Format.BINARY, request_headers) + request_context = tracer.extract(Format.BINARY, request_headers) - async def send_wrapper(response): - span = async_tracer.active_span - if span is None: - await send(response) - else: - if response['type'] == 'http.response.start': - try: - status_code = response.get('status') - if status_code is not None: - if 500 <= int(status_code): - span.mark_as_errored() - span.set_tag('http.status_code', status_code) - - headers = response.get('headers') - if headers is not None: - self._extract_custom_headers(span, headers) - async_tracer.inject(span.context, opentracing.Format.BINARY, headers) - except Exception: - logger.debug("send_wrapper: ", exc_info=True) + with tracer.start_as_current_span("asgi", span_context=request_context) as span: + self._collect_kvs(scope, span) + if "headers" in scope and agent.options.extra_http_headers: + self._extract_custom_headers(span, scope["headers"]) - try: - await send(response) - except Exception as exc: - span.log_exception(exc) - raise - - with async_tracer.start_active_span("asgi", child_of=request_context) as tracing_scope: - self._collect_kvs(scope, tracing_scope.span) - if 'headers' in scope and agent.options.extra_http_headers is not None: - self._extract_custom_headers(tracing_scope.span, scope['headers']) + instana_send = self._send_with_instana( + span, + scope, + send, + ) try: - await self.app(scope, receive, send_wrapper) + await self.app(scope, receive, instana_send) except Exception as exc: - tracing_scope.span.log_exception(exc) + span.record_exception(exc) raise exc + + def _send_with_instana( + self, + current_span: "InstanaSpan", + scope: Dict[str, Any], + send: Callable[[Dict[str, Any]], Awaitable[None]], + ) -> Awaitable[None]: + async def send_wrapper(response: Dict[str, Any]) -> Awaitable[None]: + if response["type"] == "http.response.start": + try: + status_code = response.get("status") + if status_code: + if 500 <= int(status_code): + current_span.mark_as_errored() + current_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + + headers = response.get("headers") + if headers: + self._extract_custom_headers(current_span, headers) + tracer.inject(current_span.context, Format.BINARY, headers) + except Exception: + logger.debug("ASGI send_wrapper error: ", exc_info=True) + + try: + await send(response) + except Exception as exc: + current_span.record_exception(exc) + raise + + return send_wrapper diff --git a/src/instana/instrumentation/asyncio.py b/src/instana/instrumentation/asyncio.py index 146f7c90..070dfe85 100644 --- a/src/instana/instrumentation/asyncio.py +++ b/src/instana/instrumentation/asyncio.py @@ -2,46 +2,89 @@ # (c) Copyright Instana Inc. 2019 +import time +from contextlib import contextmanager +from typing import Any, Callable, Dict, Iterator, Tuple + import wrapt -from opentracing.scope_managers.constants import ACTIVE_ATTR -from opentracing.scope_managers.contextvars import no_parent_scope +from opentelemetry.trace import use_span +from opentelemetry.trace.status import StatusCode -from ..configurator import config -from ..log import logger -from ..singletons import async_tracer +from instana.configurator import config +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import asyncio @wrapt.patch_function_wrapper("asyncio", "ensure_future") - def ensure_future_with_instana(wrapped, instance, argv, kwargs): - if config["asyncio_task_context_propagation"]["enabled"] is False: - with no_parent_scope(): - return wrapped(*argv, **kwargs) - - scope = async_tracer.scope_manager.active - task = wrapped(*argv, **kwargs) - - if scope is not None: - setattr(task, ACTIVE_ATTR, scope) + def ensure_future_with_instana( + wrapped: Callable[..., asyncio.ensure_future], + instance: object, + argv: Tuple[object, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: + if ( + not config["asyncio_task_context_propagation"]["enabled"] + or tracing_is_off() + ): + return wrapped(*argv, **kwargs) - return task + with _start_as_current_async_span() as span: + try: + span.set_status(StatusCode.OK) + return wrapped(*argv, **kwargs) + except Exception as exc: + logger.debug(f"asyncio ensure_future_with_instana error: {exc}") if hasattr(asyncio, "create_task"): @wrapt.patch_function_wrapper("asyncio", "create_task") - def create_task_with_instana(wrapped, instance, argv, kwargs): - if config["asyncio_task_context_propagation"]["enabled"] is False: - with no_parent_scope(): + def create_task_with_instana( + wrapped: Callable[..., asyncio.create_task], + instance: object, + argv: Tuple[object, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: + if ( + not config["asyncio_task_context_propagation"]["enabled"] + or tracing_is_off() + ): + return wrapped(*argv, **kwargs) + + with _start_as_current_async_span() as span: + try: + span.set_status(StatusCode.OK) return wrapped(*argv, **kwargs) + except Exception as exc: + logger.debug(f"asyncio create_task_with_instana error: {exc}") - scope = async_tracer.scope_manager.active - task = wrapped(*argv, **kwargs) + @contextmanager + def _start_as_current_async_span() -> Iterator[InstanaSpan]: + """ + Creates and yield a special InstanaSpan to only propagate the Asyncio + context. + """ + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - if scope is not None: - setattr(task, ACTIVE_ATTR, scope) + _time = time.time_ns() - return task + span = InstanaSpan( + name="asyncio", + context=parent_context, + span_processor=tracer.span_processor, + start_time=_time, + end_time=_time, + ) + with use_span( + span, + end_on_exit=False, + record_exception=False, + set_status_on_exception=False, + ) as span: + yield span logger.debug("Instrumenting asyncio") except ImportError: diff --git a/src/instana/instrumentation/boto3_inst.py b/src/instana/instrumentation/boto3_inst.py index e4099595..fb7a3233 100644 --- a/src/instana/instrumentation/boto3_inst.py +++ b/src/instana/instrumentation/boto3_inst.py @@ -5,142 +5,184 @@ import json import wrapt import inspect +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Sequence, Type, Optional +from opentelemetry.semconv.trace import SpanAttributes -from ..log import logger -from ..singletons import tracer, agent -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.singletons import tracer, agent +from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.propagators.format import Format +from instana.span.span import get_current_span + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from botocore.auth import SigV4Auth + from botocore.client import BaseClient try: - import opentracing as ot import boto3 from boto3.s3 import inject - def extract_custom_headers(span, headers): - if agent.options.extra_http_headers is None or headers is None: + def extract_custom_headers( + span: "InstanaSpan", headers: Optional[Dict[str, Any]] = None + ) -> None: + if not agent.options.extra_http_headers or not 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 lambda_inject_context(payload, scope): + def lambda_inject_context(payload: Dict[str, Any], span: "InstanaSpan") -> None: """ When boto3 lambda client 'Invoke' is called, we want to inject the tracing context. boto3/botocore has specific requirements: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.invoke """ try: - invoke_payload = payload.get('Payload', {}) + invoke_payload = payload.get("Payload", {}) if not isinstance(invoke_payload, dict): invoke_payload = json.loads(invoke_payload) - tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, invoke_payload) - payload['Payload'] = json.dumps(invoke_payload) + tracer.inject(span.context, Format.HTTP_HEADERS, invoke_payload) + payload["Payload"] = json.dumps(invoke_payload) except Exception: logger.debug("non-fatal lambda_inject_context: ", exc_info=True) - @wrapt.patch_function_wrapper("botocore.auth", "SigV4Auth.add_auth") - def emit_add_auth_with_instana(wrapped, instance, args, kwargs): - if not tracing_is_off() and tracer.active_span: - extract_custom_headers(tracer.active_span, args[0].headers) + def emit_add_auth_with_instana( + wrapped: Callable[..., None], + instance: "SigV4Auth", + args: Tuple[object], + kwargs: Dict[str, Any], + ) -> Callable[..., None]: + current_span = get_current_span() + if not tracing_is_off() and current_span and current_span.is_recording(): + extract_custom_headers(current_span, args[0].headers) return wrapped(*args, **kwargs) - - @wrapt.patch_function_wrapper('botocore.client', 'BaseClient._make_api_call') - def make_api_call_with_instana(wrapped, instance, arg_list, kwargs): + @wrapt.patch_function_wrapper("botocore.client", "BaseClient._make_api_call") + def make_api_call_with_instana( + wrapped: Callable[..., Dict[str, Any]], + instance: Type["BaseClient"], + arg_list: Sequence[Dict[str, Any]], + kwargs: Dict[str, Any], + ) -> Dict[str, Any]: # If we're not tracing, just return if tracing_is_off(): return wrapped(*arg_list, **kwargs) tracer, parent_span, _ = get_tracer_tuple() - with tracer.start_active_span("boto3", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span("boto3", span_context=parent_context) as span: try: operation = arg_list[0] payload = arg_list[1] - scope.span.set_tag('op', operation) - scope.span.set_tag('ep', instance._endpoint.host) - scope.span.set_tag('reg', instance._client_config.region_name) + span.set_attribute("op", operation) + span.set_attribute("ep", instance._endpoint.host) + span.set_attribute("reg", instance._client_config.region_name) - scope.span.set_tag('http.url', instance._endpoint.host + ':443/' + arg_list[0]) - scope.span.set_tag('http.method', 'POST') + span.set_attribute( + SpanAttributes.HTTP_URL, + instance._endpoint.host + ":443/" + arg_list[0], + ) + span.set_attribute(SpanAttributes.HTTP_METHOD, "POST") # Don't collect payload for SecretsManager - if not hasattr(instance, 'get_secret_value'): - scope.span.set_tag('payload', payload) + if not hasattr(instance, "get_secret_value"): + span.set_attribute("payload", payload) # Inject context when invoking lambdas - if 'lambda' in instance._endpoint.host and operation == 'Invoke': - lambda_inject_context(payload, scope) + if "lambda" in instance._endpoint.host and operation == "Invoke": + lambda_inject_context(payload, span) - except Exception as exc: + except Exception: logger.debug("make_api_call_with_instana: collect error", exc_info=True) try: result = wrapped(*arg_list, **kwargs) if isinstance(result, dict): - http_dict = result.get('ResponseMetadata') + http_dict = result.get("ResponseMetadata") if isinstance(http_dict, dict): - status = http_dict.get('HTTPStatusCode') + status = http_dict.get("HTTPStatusCode") if status is not None: - scope.span.set_tag('http.status_code', status) - headers = http_dict.get('HTTPHeaders') - extract_custom_headers(scope.span, headers) + span.set_attribute("http.status_code", status) + headers = http_dict.get("HTTPHeaders") + extract_custom_headers(span, headers) return result except Exception as exc: - scope.span.mark_as_errored({'error': exc}) + span.mark_as_errored({"error": exc}) raise - - def s3_inject_method_with_instana(wrapped, instance, arg_list, kwargs): + def s3_inject_method_with_instana( + wrapped: Callable[..., object], + instance: Type["BaseClient"], + arg_list: Sequence[object], + kwargs: Dict[str, Any], + ) -> Callable[..., object]: # If we're not tracing, just return if tracing_is_off(): return wrapped(*arg_list, **kwargs) fas = inspect.getfullargspec(wrapped) fas_args = fas.args - fas_args.remove('self') + fas_args.remove("self") tracer, parent_span, _ = get_tracer_tuple() - with tracer.start_active_span("boto3", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span("boto3", span_context=parent_context) as span: try: operation = wrapped.__name__ - scope.span.set_tag('op', operation) - scope.span.set_tag('ep', instance._endpoint.host) - scope.span.set_tag('reg', instance._client_config.region_name) + span.set_attribute("op", operation) + span.set_attribute("ep", instance._endpoint.host) + span.set_attribute("reg", instance._client_config.region_name) - scope.span.set_tag('http.url', instance._endpoint.host + ':443/' + operation) - scope.span.set_tag('http.method', 'POST') + span.set_attribute( + SpanAttributes.HTTP_URL, + instance._endpoint.host + ":443/" + operation, + ) + span.set_attribute(SpanAttributes.HTTP_METHOD, "POST") arg_length = len(arg_list) if arg_length > 0: payload = {} for index in range(arg_length): - if fas_args[index] in ['Filename', 'Bucket', 'Key']: + if fas_args[index] in ["Filename", "Bucket", "Key"]: payload[fas_args[index]] = arg_list[index] - scope.span.set_tag('payload', payload) - except Exception as exc: - logger.debug("s3_inject_method_with_instana: collect error", exc_info=True) + span.set_attribute("payload", payload) + except Exception: + logger.debug( + "s3_inject_method_with_instana: collect error", exc_info=True + ) try: return wrapped(*arg_list, **kwargs) except Exception as exc: - scope.span.mark_as_errored({'error': exc}) + span.mark_as_errored({"error": exc}) raise - - for method in ['upload_file', 'upload_fileobj', 'download_file', 'download_fileobj']: - wrapt.wrap_function_wrapper('boto3.s3.inject', method, s3_inject_method_with_instana) + for method in [ + "upload_file", + "upload_fileobj", + "download_file", + "download_fileobj", + ]: + wrapt.wrap_function_wrapper( + "boto3.s3.inject", method, s3_inject_method_with_instana + ) logger.debug("Instrumenting boto3") except ImportError: diff --git a/src/instana/instrumentation/cassandra_inst.py b/src/instana/instrumentation/cassandra_inst.py index da828d18..3b6e9713 100644 --- a/src/instana/instrumentation/cassandra_inst.py +++ b/src/instana/instrumentation/cassandra_inst.py @@ -6,78 +6,103 @@ https://docs.datastax.com/en/developer/python-driver/3.20/ https://github.com/datastax/python-driver """ + +from typing import Any, Callable, Dict, Tuple import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import cassandra - - consistency_levels = dict({0: "ANY", - 1: "ONE", - 2: "TWO", - 3: "THREE", - 4: "QUORUM", - 5: "ALL", - 6: "LOCAL_QUORUM", - 7: "EACH_QUORUM", - 8: "SERIAL", - 9: "LOCAL_SERIAL", - 10: "LOCAL_ONE"}) - - - def collect_response(span, fn): - tried_hosts = list() + from cassandra.cluster import ResponseFuture, Session + + consistency_levels = dict( + { + 0: "ANY", + 1: "ONE", + 2: "TWO", + 3: "THREE", + 4: "QUORUM", + 5: "ALL", + 6: "LOCAL_QUORUM", + 7: "EACH_QUORUM", + 8: "SERIAL", + 9: "LOCAL_SERIAL", + 10: "LOCAL_ONE", + } + ) + + def collect_attributes( + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + tried_hosts = [] for host in fn.attempted_hosts: - tried_hosts.append("%s:%d" % (host.endpoint.address, host.endpoint.port)) + tried_hosts.append(f"{host.endpoint.address}:{host.endpoint.port}") - span.set_tag("cassandra.triedHosts", tried_hosts) - span.set_tag("cassandra.coordHost", fn.coordinator_host) + span.set_attribute("cassandra.triedHosts", tried_hosts) + span.set_attribute("cassandra.coordHost", fn.coordinator_host) cl = fn.query.consistency_level if cl and cl in consistency_levels: - span.set_tag("cassandra.achievedConsistency", consistency_levels[cl]) - - - def cb_request_finish(results, span, fn): - collect_response(span, fn) - span.finish() - - - def cb_request_error(results, span, fn): - collect_response(span, fn) + span.set_attribute("cassandra.achievedConsistency", consistency_levels[cl]) + + def cb_request_finish( + _, + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + collect_attributes(span, fn) + span.end() + + def cb_request_error( + results: Dict[str, Any], + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + collect_attributes(span, fn) span.mark_as_errored({"cassandra.error": results.summary}) - span.finish() + span.end() - - def request_init_with_instana(fn): + def request_init_with_instana( + fn: ResponseFuture, + ) -> None: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None if tracing_is_off(): return - ctags = {} + attributes = {} if isinstance(fn.query, cassandra.query.SimpleStatement): - ctags["cassandra.query"] = fn.query.query_string + attributes["cassandra.query"] = fn.query.query_string elif isinstance(fn.query, cassandra.query.BoundStatement): - ctags["cassandra.query"] = fn.query.prepared_statement.query_string - - ctags["cassandra.keyspace"] = fn.session.keyspace - ctags["cassandra.cluster"] = fn.session.cluster.metadata.cluster_name - - with tracer.start_active_span("cassandra", child_of=parent_span, - tags=ctags, finish_on_close=False) as scope: - fn.add_callback(cb_request_finish, scope.span, fn) - fn.add_errback(cb_request_error, scope.span, fn) - - - @wrapt.patch_function_wrapper('cassandra.cluster', 'Session.__init__') - def init_with_instana(wrapped, instance, args, kwargs): + attributes["cassandra.query"] = fn.query.prepared_statement.query_string + + attributes["cassandra.keyspace"] = fn.session.keyspace + attributes["cassandra.cluster"] = fn.session.cluster.metadata.cluster_name + + with tracer.start_as_current_span( + "cassandra", + span_context=parent_context, + attributes=attributes, + end_on_exit=False, + ) as span: + fn.add_callback(cb_request_finish, span, fn) + fn.add_errback(cb_request_error, span, fn) + + @wrapt.patch_function_wrapper("cassandra.cluster", "Session.__init__") + def init_with_instana( + wrapped: Callable[..., object], + instance: Session, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: session = wrapped(*args, **kwargs) instance.add_request_init_listener(request_init_with_instana) return session - logger.debug("Instrumenting cassandra") except ImportError: diff --git a/src/instana/instrumentation/couchbase_inst.py b/src/instana/instrumentation/couchbase_inst.py index f65c639a..cb97e042 100644 --- a/src/instana/instrumentation/couchbase_inst.py +++ b/src/instana/instrumentation/couchbase_inst.py @@ -6,18 +6,21 @@ https://docs.couchbase.com/python-sdk/2.5/start-using-sdk.html """ +from typing import Any, Callable, Dict, Tuple, Union + import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import couchbase + from couchbase.bucket import Bucket - if not (hasattr(couchbase, '__version__') and couchbase.__version__[0] == '2' - and (couchbase.__version__[2] > '3' - or (couchbase.__version__[2] == '3' and couchbase.__version__[4] >= '4')) - ): + if not hasattr(couchbase, "__version__") and ( + couchbase.__version__ < "2.3.4" or couchbase.__version__ >= "3.0.0" + ): logger.debug("Instana supports 2.3.4 <= couchbase_versions < 3.0.0. Skipping.") raise ImportError @@ -25,71 +28,121 @@ # List of operations to instrument # incr, incr_multi, decr, decr_multi, retrieve_in are wrappers around operations above - operations = ['upsert', 'insert', 'replace', 'append', 'prepend', 'get', 'rget', - 'touch', 'lock', 'unlock', 'remove', 'counter', 'mutate_in', 'lookup_in', - 'stats', 'ping', 'diagnostics', 'observe', - - 'upsert_multi', 'insert_multi', 'replace_multi', 'append_multi', - 'prepend_multi', 'get_multi', 'touch_multi', 'lock_multi', 'unlock_multi', - 'observe_multi', 'endure_multi', 'remove_multi', 'counter_multi'] - - def capture_kvs(scope, instance, query_arg, op): + operations = [ + "upsert", + "insert", + "replace", + "append", + "prepend", + "get", + "rget", + "touch", + "lock", + "unlock", + "remove", + "counter", + "mutate_in", + "lookup_in", + "stats", + "ping", + "diagnostics", + "observe", + "upsert_multi", + "insert_multi", + "replace_multi", + "append_multi", + "prepend_multi", + "get_multi", + "touch_multi", + "lock_multi", + "unlock_multi", + "observe_multi", + "endure_multi", + "remove_multi", + "counter_multi", + ] + + def collect_attributes( + span: InstanaSpan, + instance: Bucket, + query_arg: Union[N1QLQuery, object], + op: str, + ) -> None: try: - scope.span.set_tag('couchbase.hostname', instance.server_nodes[0]) - scope.span.set_tag('couchbase.bucket', instance.bucket) - scope.span.set_tag('couchbase.type', op) + span.set_attribute("couchbase.hostname", instance.server_nodes[0]) + span.set_attribute("couchbase.bucket", instance.bucket) + span.set_attribute("couchbase.type", op) - if query_arg is not None: + if query_arg: query = None if type(query_arg) is N1QLQuery: query = query_arg.statement else: query = query_arg - scope.span.set_tag('couchbase.sql', query) - except: + span.set_attribute("couchbase.sql", query) + except Exception: # No fail on key capture - best effort pass - def make_wrapper(op): - def wrapper(wrapped, instance, args, kwargs): + def make_wrapper(op: str) -> Callable: + def wrapper( + wrapped: Callable[..., object], + instance: couchbase.bucket.Bucket, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - with tracer.start_active_span("couchbase", child_of=parent_span) as scope: - capture_kvs(scope, instance, None, op) + with tracer.start_as_current_span( + "couchbase", span_context=parent_context + ) as span: + collect_attributes(span, instance, None, op) try: return wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - scope.span.set_tag('couchbase.error', repr(e)) - raise + except Exception as exc: + span.record_exception(exc) + span.set_attribute("couchbase.error", repr(exc)) + logger.debug("Instana couchbase @ wrapper", exc_info=True) + return wrapper - def query_with_instana(wrapped, instance, args, kwargs): + def query_with_instana( + wrapped: Callable[..., object], + instance: couchbase.bucket.Bucket, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - with tracer.start_active_span("couchbase", child_of=parent_span) as scope: - capture_kvs(scope, instance, args[0], 'n1ql_query') + with tracer.start_as_current_span( + "couchbase", span_context=parent_context + ) as span: try: + collect_attributes(span, instance, args[0], "n1ql_query") return wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - scope.span.set_tag('couchbase.error', repr(e)) - raise + except Exception as exc: + span.record_exception(exc) + span.set_attribute("couchbase.error", repr(exc)) + logger.debug("Instana couchbase @ query_with_instana", exc_info=True) logger.debug("Instrumenting couchbase") - wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.n1ql_query', query_with_instana) + wrapt.wrap_function_wrapper( + "couchbase.bucket", "Bucket.n1ql_query", query_with_instana + ) for op in operations: f = make_wrapper(op) - wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.%s' % op, f) + wrapt.wrap_function_wrapper("couchbase.bucket", f"Bucket.{op}", f) except ImportError: pass diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index d1485163..82ef5ed5 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -2,18 +2,24 @@ # (c) Copyright Instana Inc. 2018 -import os import sys -import opentracing as ot -import opentracing.ext.tags as ext +from opentelemetry import context, trace +from opentelemetry.semconv.trace import SpanAttributes import wrapt +from typing import TYPE_CHECKING, Dict, Any, Callable, Optional, List, Tuple -from ...log import logger -from ...singletons import agent, tracer -from ...util.secrets import strip_secrets_from_query +from instana.log import logger +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query +from instana.propagators.format import Format -DJ_INSTANA_MIDDLEWARE = 'instana.instrumentation.django.middleware.InstanaMiddleware' +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from django.core.handlers.wsgi import WSGIRequest, WSGIHandler + from django.http import HttpRequest, HttpResponse + +DJ_INSTANA_MIDDLEWARE = "instana.instrumentation.django.middleware.InstanaMiddleware" try: from django.utils.deprecation import MiddlewareMixin @@ -22,125 +28,178 @@ class InstanaMiddleware(MiddlewareMixin): - """ Django Middleware to provide request tracing for Instana """ + """Django Middleware to provide request tracing for Instana""" - def __init__(self, get_response=None): + def __init__( + self, get_response: Optional[Callable[["HttpRequest"], "HttpResponse"]] = None + ) -> None: super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response - def _extract_custom_headers(self, span, headers, format): + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any], format: bool + ) -> None: if agent.options.extra_http_headers is None: return - try: + try: for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS - django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header + django_header = ( + ("HTTP_" + custom_header.upper()).replace("-", "_") + if format + else custom_header + ) if django_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[django_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[django_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def process_request(self, request): + def process_request(self, request: "WSGIRequest") -> None: try: env = request.environ - ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) - request.iscope = tracer.start_active_span('django', child_of=ctx) - - self._extract_custom_headers(request.iscope.span, env, format=True) - - request.iscope.span.set_tag(ext.HTTP_METHOD, request.method) - if 'PATH_INFO' in env: - request.iscope.span.set_tag(ext.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - request.iscope.span.set_tag("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - request.iscope.span.set_tag("http.host", env['HTTP_HOST']) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("django", span_context=span_context) + request.span = span + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + request.token = token + + self._extract_custom_headers(span, env, format=True) + + request.span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + if "PATH_INFO" in env: + request.span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + request.span.set_attribute("http.params", scrubbed_params) + if "HTTP_HOST" in env: + request.span.set_attribute("http.host", env["HTTP_HOST"]) except Exception: logger.debug("Django middleware @ process_request", exc_info=True) - def process_response(self, request, response): + def process_response( + self, request: "WSGIRequest", response: "HttpResponse" + ) -> "HttpResponse": try: - if request.iscope is not None: + if request.span: if 500 <= response.status_code: - request.iscope.span.assure_errored() + request.span.assure_errored() # for django >= 2.2 - if request.resolver_match is not None and hasattr(request.resolver_match, 'route'): + if request.resolver_match is not None and hasattr( + request.resolver_match, "route" + ): path_tpl = request.resolver_match.route # django < 2.2 or in case of 404 else: try: from django.urls import resolve + view_name = resolve(request.path)._func_path - path_tpl = "".join(self.__url_pattern_route(view_name)) + path_tpl = "".join(url_pattern_route(view_name)) except Exception: # the resolve method can fire a Resolver404 exception, in this case there is no matching route # so the path_tpl is set to None in order not to be added as a tag path_tpl = None if path_tpl: - request.iscope.span.set_tag("http.path_tpl", path_tpl) - - request.iscope.span.set_tag(ext.HTTP_STATUS_CODE, response.status_code) - self._extract_custom_headers(request.iscope.span, response.headers, format=False) - tracer.inject(request.iscope.span.context, ot.Format.HTTP_HEADERS, response) - response['Server-Timing'] = "intid;desc=%s" % request.iscope.span.context.trace_id + request.span.set_attribute("http.path_tpl", path_tpl) + + request.span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_code + ) + self._extract_custom_headers( + request.span, response.headers, format=False + ) + tracer.inject(request.span.context, Format.HTTP_HEADERS, response) + response["Server-Timing"] = ( + "intid;desc=%s" % request.span.context.trace_id + ) except Exception: logger.debug("Instana middleware @ process_response", exc_info=True) finally: - if request.iscope is not None: - request.iscope.close() - request.iscope = None + if request.span: + if request.span.is_recording(): + request.span.end() + request.span = None + if request.token: + context.detach(request.token) + request.token = None return response - def process_exception(self, request, exception): + def process_exception(self, request: "WSGIRequest", exception: Exception) -> None: from django.http.response import Http404 if isinstance(exception, Http404): return None - if request.iscope is not None: - request.iscope.span.log_exception(exception) + if request.span: + request.span.record_exception(exception) - def __url_pattern_route(self, view_name): - from django.conf import settings - from django.urls import RegexURLResolver as URLResolver - - urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) - - def list_urls(urlpatterns, parent_pattern=None): - if not urlpatterns: - return - if parent_pattern is None: - parent_pattern = [] - first = urlpatterns[0] - if isinstance(first, URLPattern): - if first.lookup_str == view_name: - if hasattr(first, "regex"): - return parent_pattern + [str(first.regex.pattern)] - else: - return parent_pattern + [str(first.pattern)] - elif isinstance(first, URLResolver): + +def url_pattern_route(view_name: str) -> Callable[..., object]: + from django.conf import settings + + try: + from django.urls import ( + RegexURLPattern as URLPattern, + RegexURLResolver as URLResolver, + ) + except ImportError: + from django.urls import URLPattern, URLResolver + + urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [""]) + + def list_urls( + urlpatterns: List[str], parent_pattern: Optional[List[str]] = None + ) -> Callable[..., object]: + if not urlpatterns: + return + if parent_pattern is None: + parent_pattern = [] + first = urlpatterns[0] + if isinstance(first, URLPattern): + if first.lookup_str == view_name: if hasattr(first, "regex"): - return list_urls(first.url_patterns, parent_pattern + [str(first.regex.pattern)]) + return parent_pattern + [str(first.regex.pattern)] else: - return list_urls(first.url_patterns, parent_pattern + [str(first.pattern)]) - return list_urls(urlpatterns[1:], parent_pattern) + return parent_pattern + [str(first.pattern)] + elif isinstance(first, URLResolver): + if hasattr(first, "regex"): + return list_urls( + first.url_patterns, parent_pattern + [str(first.regex.pattern)] + ) + else: + return list_urls( + first.url_patterns, parent_pattern + [str(first.pattern)] + ) + return list_urls(urlpatterns[1:], parent_pattern) - return list_urls(urlconf.urlpatterns) + return list_urls(urlconf.urlpatterns) -def load_middleware_wrapper(wrapped, instance, args, kwargs): +def load_middleware_wrapper( + wrapped: Callable[..., None], + instance: "WSGIHandler", + args: Tuple[object, ...], + kwargs: Dict[str, Any], +) -> Callable[..., None]: try: from django.conf import settings # Django >=1.10 to <2.0 support old-style MIDDLEWARE_CLASSES so we # do as well here - if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: + if hasattr(settings, "MIDDLEWARE") and settings.MIDDLEWARE is not None: if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE: return wrapped(*args, **kwargs) @@ -151,31 +210,44 @@ def load_middleware_wrapper(wrapped, instance, args, kwargs): else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: + elif ( + hasattr(settings, "MIDDLEWARE_CLASSES") + and settings.MIDDLEWARE_CLASSES is not None + ): # pragma: no cover if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: return wrapped(*args, **kwargs) if isinstance(settings.MIDDLEWARE_CLASSES, tuple): - settings.MIDDLEWARE_CLASSES = (DJ_INSTANA_MIDDLEWARE,) + settings.MIDDLEWARE_CLASSES + settings.MIDDLEWARE_CLASSES = ( + DJ_INSTANA_MIDDLEWARE, + ) + settings.MIDDLEWARE_CLASSES elif isinstance(settings.MIDDLEWARE_CLASSES, list): - settings.MIDDLEWARE_CLASSES = [DJ_INSTANA_MIDDLEWARE] + settings.MIDDLEWARE_CLASSES + settings.MIDDLEWARE_CLASSES = [ + DJ_INSTANA_MIDDLEWARE + ] + settings.MIDDLEWARE_CLASSES else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - else: + else: # pragma: no cover logger.warning("Instana: Couldn't find middleware settings") return wrapped(*args, **kwargs) except Exception: - logger.warning("Instana: Couldn't add InstanaMiddleware to Django: ", exc_info=True) + logger.warning( + "Instana: Couldn't add InstanaMiddleware to Django: ", exc_info=True + ) try: - if 'django' in sys.modules: + if "django" in sys.modules: logger.debug("Instrumenting django") - wrapt.wrap_function_wrapper('django.core.handlers.base', 'BaseHandler.load_middleware', load_middleware_wrapper) + wrapt.wrap_function_wrapper( + "django.core.handlers.base", + "BaseHandler.load_middleware", + load_middleware_wrapper, + ) - if '/tmp/.instana/python' in sys.path: + if "/tmp/.instana/python" in sys.path: # pragma: no cover # If we are instrumenting via AutoTrace (in an already running process), then the # WSGI middleware has to be live reloaded. from django.core.servers.basehttp import get_internal_wsgi_application diff --git a/src/instana/instrumentation/fastapi_inst.py b/src/instana/instrumentation/fastapi_inst.py index c2d56d84..5edee85c 100644 --- a/src/instana/instrumentation/fastapi_inst.py +++ b/src/instana/instrumentation/fastapi_inst.py @@ -5,65 +5,86 @@ Instrumentation for FastAPI https://fastapi.tiangolo.com/ """ + +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple + try: - import fastapi import os - import wrapt import signal - from ..log import logger - from ..util.gunicorn import running_in_gunicorn - from .asgi import InstanaASGIMiddleware - from starlette.middleware import Middleware + import fastapi + import wrapt from fastapi import HTTPException from fastapi.exception_handlers import http_exception_handler + from starlette.middleware import Middleware + + from instana.instrumentation.asgi import InstanaASGIMiddleware + from instana.log import logger + from instana.util.gunicorn import running_in_gunicorn + from instana.util.traceutils import get_tracer_tuple + + from opentelemetry.semconv.trace import SpanAttributes - from instana.singletons import async_tracer + if TYPE_CHECKING: + from starlette.requests import Request + from starlette.responses import Response - if not(hasattr(fastapi, '__version__') - and (fastapi.__version__[0] > '0' or - int(fastapi.__version__.split('.')[1]) >= 51)): - logger.debug('Instana supports FastAPI package versions 0.51.0 and newer. Skipping.') + if not ( # pragma: no cover + hasattr(fastapi, "__version__") + and ( + fastapi.__version__[0] > "0" or int(fastapi.__version__.split(".")[1]) >= 51 + ) + ): + logger.debug( + "Instana supports FastAPI package versions 0.51.0 and newer. Skipping." + ) raise ImportError - async def instana_exception_handler(request, exc): + async def instana_exception_handler( + request: "Request", exc: HTTPException + ) -> "Response": """ We capture FastAPI HTTPException, log the error and pass it on to the default exception handler. """ try: - span = async_tracer.active_span + _, span, _ = get_tracer_tuple() - if span is not None: - if hasattr(exc, 'detail') and 500 <= exc.status_code: - span.set_tag('http.error', exc.detail) - span.set_tag('http.status_code', exc.status_code) + if span: + if hasattr(exc, "detail") and 500 <= exc.status_code: + span.set_attribute("http.error", exc.detail) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, exc.status_code) except Exception: logger.debug("FastAPI instana_exception_handler: ", exc_info=True) return await http_exception_handler(request, exc) - @wrapt.patch_function_wrapper('fastapi.applications', 'FastAPI.__init__') - def init_with_instana(wrapped, instance, args, kwargs): - middleware = kwargs.get('middleware') + @wrapt.patch_function_wrapper("fastapi.applications", "FastAPI.__init__") + def init_with_instana( + wrapped: Callable[..., fastapi.applications.FastAPI.__init__], + instance: fastapi.applications.FastAPI, + args: Tuple, + kwargs: Dict[str, Any], + ) -> None: + middleware = kwargs.get("middleware") if middleware is None: - kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + kwargs["middleware"] = [Middleware(InstanaASGIMiddleware)] elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) - exception_handlers = kwargs.get('exception_handlers') + exception_handlers = kwargs.get("exception_handlers") if exception_handlers is None: - kwargs['exception_handlers'] = dict() + kwargs["exception_handlers"] = dict() - if isinstance(kwargs['exception_handlers'], dict): - kwargs['exception_handlers'][HTTPException] = instana_exception_handler + if isinstance(kwargs["exception_handlers"], dict): + kwargs["exception_handlers"][HTTPException] = instana_exception_handler return wrapped(*args, **kwargs) logger.debug("Instrumenting FastAPI") # Reload GUnicorn when we are instrumenting an already running application - if "INSTANA_MAGIC" in os.environ and running_in_gunicorn(): + if "INSTANA_MAGIC" in os.environ and running_in_gunicorn(): # pragma: no cover os.kill(os.getpid(), signal.SIGHUP) except ImportError: diff --git a/src/instana/instrumentation/flask/__init__.py b/src/instana/instrumentation/flask/__init__.py index 3ec4b3e9..7d85abcd 100644 --- a/src/instana/instrumentation/flask/__init__.py +++ b/src/instana/instrumentation/flask/__init__.py @@ -11,15 +11,15 @@ # Blinker support is preferred but we do the best we can when it's not available. # if hasattr(flask.signals, 'signals_available'): - from flask.signals import signals_available + from flask.signals import signals_available else: - # Beginning from 2.3.0 as stated in the notes - # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 - # "Signals are always available. blinker>=1.6.2 is a required dependency. - # The signals_available attribute is deprecated. #5056" - signals_available = True + # Beginning from 2.3.0 as stated in the notes + # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 + # "Signals are always available. blinker>=1.6.2 is a required dependency. + # The signals_available attribute is deprecated. #5056" + signals_available = True - from . import common + from instana.instrumentation.flask import common if signals_available is True: import instana.instrumentation.flask.with_blinker diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 58de6ae2..cd966986 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -4,51 +4,77 @@ import wrapt import flask -import opentracing -import opentracing.ext.tags as ext +from importlib.metadata import version +from typing import Callable, Tuple, Dict, Any, TYPE_CHECKING, Union -from ...log import logger -from ...singletons import tracer, agent +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.singletons import tracer, agent +from instana.propagators.format import Format +from instana.instrumentation.flask import signals_available + + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from werkzeug.exceptions import HTTPException + from flask.typing import ResponseReturnValue + from jinja2.environment import Template + + if signals_available: + from werkzeug.datastructures.headers import Headers + else: + from werkzeug.datastructures import Headers @wrapt.patch_function_wrapper('flask', 'templating._render') -def render_with_instana(wrapped, instance, argv, kwargs): +def render_with_instana( + wrapped: Callable[..., str], + instance: object, + argv: Tuple[flask.app.Flask, "Template", Dict[str, Any]], + kwargs: Dict[str, Any], +) -> str: # If we're not tracing, just return - if not (hasattr(flask, 'g') and hasattr(flask.g, 'scope')): + if not (hasattr(flask, "g") and hasattr(flask.g, "span")): return wrapped(*argv, **kwargs) - parent_span = flask.g.scope.span + parent_span = flask.g.span + parent_context = parent_span.get_span_context() - with tracer.start_active_span("render", child_of=parent_span) as rscope: + with tracer.start_as_current_span("render", span_context=parent_context) as span: try: - flask_version = tuple(map(int, flask.__version__.split('.'))) + flask_version = tuple(map(int, version("flask").split("."))) template = argv[1] if flask_version >= (2, 2, 0) else argv[0] - rscope.span.set_tag("type", "template") + span.set_attribute("type", "template") if template.name is None: - rscope.span.set_tag("name", '(from string)') + span.set_attribute("name", "(from string)") else: - rscope.span.set_tag("name", template.name) + span.set_attribute("name", template.name) return wrapped(*argv, **kwargs) except Exception as e: - rscope.span.log_exception(e) + span.record_exception(e) raise @wrapt.patch_function_wrapper('flask', 'Flask.handle_user_exception') -def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): +def handle_user_exception_with_instana( + wrapped: Callable[..., Union["HTTPException", "ResponseReturnValue"]], + instance: flask.app.Flask, + argv: Tuple[Exception], + kwargs: Dict[str, Any], +) -> Union["HTTPException", "ResponseReturnValue"]: # Call original and then try to do post processing response = wrapped(*argv, **kwargs) try: exc = argv[0] - if hasattr(flask.g, 'scope') and flask.g.scope is not None: - scope = flask.g.scope - span = scope.span + if hasattr(flask.g, "span") and flask.g.span: + span = flask.g.span - if response is not None: + if response: if isinstance(response, tuple): status_code = response[1] else: @@ -58,27 +84,29 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): status_code = response.status_code if 500 <= status_code: - span.log_exception(exc) + span.record_exception(exc) - span.set_tag(ext.HTTP_STATUS_CODE, int(status_code)) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, int(status_code)) if hasattr(response, 'headers'): - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - value = "intid;desc=%s" % scope.span.context.trace_id + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + value = "intid;desc=%s" % span.context.trace_id if hasattr(response.headers, 'add'): response.headers.add('Server-Timing', value) elif type(response.headers) is dict or hasattr(response.headers, "__dict__"): response.headers['Server-Timing'] = value - - scope.close() - flask.g.scope = None + if span and span.is_recording(): + span.end() + flask.g.span = None except: logger.debug("handle_user_exception_with_instana:", exc_info=True) return response -def extract_custom_headers(span, headers, format): +def extract_custom_headers( + span: "InstanaSpan", headers: Union[Dict[str, Any], "Headers"], format: bool +) -> None: if agent.options.extra_http_headers is None: return try: @@ -86,7 +114,9 @@ def extract_custom_headers(span, headers, format): # Headers are available in this format: HTTP_X_CAPTURE_THIS flask_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header if flask_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[flask_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[flask_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index 9775f1db..9e21b033 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -4,100 +4,125 @@ import re import flask - -import opentracing -import opentracing.ext.tags as ext import wrapt +from typing import Callable, Tuple, Dict, Type, Union + +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry import context, trace -from ...log import logger -from ...singletons import agent, tracer -from ...util.secrets import strip_secrets_from_query -from .common import extract_custom_headers +from instana.log import logger +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query +from instana.instrumentation.flask.common import extract_custom_headers +from instana.propagators.format import Format path_tpl_re = re.compile('<.*>') -def before_request_with_instana(*argv, **kwargs): +def before_request_with_instana() -> None: try: env = flask.request.environ - ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("wsgi", span_context=span_context) + flask.g.span = span - flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) - span = flask.g.scope.span + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + flask.g.token = token extract_custom_headers(span, env, format=True) - span.set_tag(ext.HTTP_METHOD, flask.request.method) - if 'PATH_INFO' in env: - span.set_tag(ext.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - span.set_tag("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - span.set_tag("http.host", env['HTTP_HOST']) - - if hasattr(flask.request.url_rule, 'rule') and \ - path_tpl_re.search(flask.request.url_rule.rule) is not None: + span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) + if "PATH_INFO" in env: + span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + + if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( + flask.request.url_rule.rule + ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") - span.set_tag("http.path_tpl", path_tpl) + span.set_attribute("http.path_tpl", path_tpl) except: logger.debug("Flask before_request", exc_info=True) return None -def after_request_with_instana(response): - scope = None +def after_request_with_instana( + response: flask.wrappers.Response, +) -> flask.wrappers.Response: + span = None try: # If we're not tracing, just return - if not hasattr(flask.g, 'scope'): + if not hasattr(flask.g, "span"): return response - scope = flask.g.scope - if scope is not None: - span = scope.span + span = flask.g.span + if span: if 500 <= response.status_code: span.mark_as_errored() - span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) + ) extract_custom_headers(span, response.headers, format=False) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers.add( + "Server-Timing", "intid;desc=%s" % span.context.trace_id + ) except: logger.debug("Flask after_request", exc_info=True) finally: - if scope is not None: - scope.close() - flask.g.scope = None + if span and span.is_recording(): + span.end() + flask.g.span = None return response -def teardown_request_with_instana(*argv, **kwargs): +def teardown_request_with_instana(*argv: Union[Exception, Type[Exception]]) -> None: """ In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, 'scope') and flask.g.scope is not None: - if len(argv) > 0 and argv[0] is not None: - scope = flask.g.scope - scope.span.log_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in scope.span.tags: - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - flask.g.scope.close() - flask.g.scope = None + if hasattr(flask.g, "span") and flask.g.span: + if len(argv) > 0 and argv[0]: + span = flask.g.span + span.record_exception(argv[0]) + if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + if flask.g.span.is_recording(): + flask.g.span.end() + flask.g.span = None + + if hasattr(flask.g, "token") and flask.g.token: + context.detach(flask.g.token) + flask.g.token = None @wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') -def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): +def full_dispatch_request_with_instana( + wrapped: Callable[..., flask.wrappers.Response], + instance: flask.app.Flask, + argv: Tuple, + kwargs: Dict, +) -> flask.wrappers.Response: if not hasattr(instance, '_stan_wuz_here'): logger.debug("Flask(vanilla): Applying flask before/after instrumentation funcs") setattr(instance, "_stan_wuz_here", True) - instance.after_request(after_request_with_instana) instance.before_request(before_request_with_instana) + instance.after_request(after_request_with_instana) instance.teardown_request(teardown_request_with_instana) return wrapped(*argv, **kwargs) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index cac55c96..211f2173 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -4,109 +4,138 @@ import re import wrapt -import opentracing -import opentracing.ext.tags as ext +from typing import Any, Tuple, Dict, Callable -from ...log import logger -from ...util.secrets import strip_secrets_from_query -from ...singletons import agent, tracer -from .common import extract_custom_headers +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry import context, trace + +from instana.log import logger +from instana.util.secrets import strip_secrets_from_query +from instana.singletons import agent, tracer +from instana.instrumentation.flask.common import extract_custom_headers +from instana.propagators.format import Format import flask from flask import request_started, request_finished, got_request_exception -path_tpl_re = re.compile('<.*>') +path_tpl_re = re.compile("<.*>") -def request_started_with_instana(sender, **extra): +def request_started_with_instana(sender: flask.app.Flask, **extra: Any) -> None: try: env = flask.request.environ - ctx = None - ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("wsgi", span_context=span_context) + flask.g.span = span - flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) - span = flask.g.scope.span + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + flask.g.token = token extract_custom_headers(span, env, format=True) - span.set_tag(ext.HTTP_METHOD, flask.request.method) - if 'PATH_INFO' in env: - span.set_tag(ext.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - span.set_tag("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - span.set_tag("http.host", env['HTTP_HOST']) - - if hasattr(flask.request.url_rule, 'rule') and \ - path_tpl_re.search(flask.request.url_rule.rule) is not None: + span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) + if "PATH_INFO" in env: + span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + + if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( + flask.request.url_rule.rule + ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") - span.set_tag("http.path_tpl", path_tpl) + span.set_attribute("http.path_tpl", path_tpl) except: - logger.debug("Flask before_request", exc_info=True) + logger.debug("Flask request_started_with_instana", exc_info=True) -def request_finished_with_instana(sender, response, **extra): - scope = None +def request_finished_with_instana( + sender: flask.app.Flask, response: flask.wrappers.Response, **extra: Any +) -> None: + span = None try: - if not hasattr(flask.g, 'scope'): + if not hasattr(flask.g, "span"): return - scope = flask.g.scope - if scope is not None: - span = scope.span - + span = flask.g.span + if span: if 500 <= response.status_code: span.mark_as_errored() - span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) + ) extract_custom_headers(span, response.headers, format=False) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers.add( + "Server-Timing", "intid;desc=%s" % span.context.trace_id + ) except: - logger.debug("Flask after_request", exc_info=True) + logger.debug("Flask request_finished_with_instana", exc_info=True) finally: - if scope is not None: - scope.close() + if span and span.is_recording(): + span.end() -def log_exception_with_instana(sender, exception, **extra): - if hasattr(flask.g, 'scope') and flask.g.scope is not None: - scope = flask.g.scope - if scope.span is not None: - scope.span.log_exception(exception) +def log_exception_with_instana( + sender: flask.app.Flask, exception: Exception, **extra: Any +) -> None: + if hasattr(flask.g, "span") and flask.g.span: + span = flask.g.span + if span: + span.record_exception(exception) # As of Flask 2.3.x: # https://github.com/pallets/flask/blob/ # d0bf462866289ad8bfe29b6e4e1e0f531003ab34/src/flask/app.py#L1379 # The `got_request_exception` signal, is only sent by # the `handle_exception` method which "always causes a 500" - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - scope.close() + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + if span.is_recording(): + span.end() -def teardown_request_with_instana(*argv, **kwargs): +def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: """ - In the case of exceptions, after_request_with_instana isn't called + In the case of exceptions, request_finished_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, 'scope') and flask.g.scope is not None: - if len(argv) > 0 and argv[0] is not None: - scope = flask.g.scope - scope.span.log_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in scope.span.tags: - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - flask.g.scope.close() - flask.g.scope = None - - -@wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') -def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): - if not hasattr(instance, '_stan_wuz_here'): - logger.debug("Flask(blinker): Applying flask before/after instrumentation funcs") + if hasattr(flask.g, "span") and flask.g.span: + if len(argv) > 0 and argv[0]: + span = flask.g.span + span.record_exception(argv[0]) + if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + if flask.g.span.is_recording(): + flask.g.span.end() + flask.g.span = None + + if hasattr(flask.g, "token") and flask.g.token: + context.detach(flask.g.token) + flask.g.token = None + + +@wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") +def full_dispatch_request_with_instana( + wrapped: Callable[..., flask.wrappers.Response], + instance: flask.app.Flask, + argv: Tuple, + kwargs: Dict, +) -> flask.wrappers.Response: + if not hasattr(instance, "_stan_wuz_here"): + logger.debug( + "Flask(blinker): Applying flask before/after instrumentation funcs" + ) setattr(instance, "_stan_wuz_here", True) got_request_exception.connect(log_exception_with_instana, instance) request_started.connect(request_started_with_instana, instance) diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index 77d11051..0f2280a0 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -6,13 +6,19 @@ import wrapt import logging from collections.abc import Mapping +from typing import Any, Tuple, Dict, Callable -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off -@wrapt.patch_function_wrapper('logging', 'Logger._log') -def log_with_instana(wrapped, instance, argv, kwargs): +@wrapt.patch_function_wrapper("logging", "Logger._log") +def log_with_instana( + wrapped: Callable[..., None], + instance: logging.Logger, + argv: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], +) -> Callable[..., None]: # argv[0] = level # argv[1] = message # argv[2] = args for message @@ -39,21 +45,24 @@ def log_with_instana(wrapped, instance, argv, kwargs): parameters = None (t, v, tb) = sys.exc_info() if t is not None and v is not None: - parameters = '{} {}'.format(t , v) + parameters = "{} {}".format(t, v) + + parent_context = parent_span.get_span_context() if parent_span else None # create logging span - with tracer.start_active_span('log', child_of=parent_span) as scope: - scope.span.log_kv({ 'message': msg }) + with tracer.start_as_current_span("log", span_context=parent_context) as span: + event_attributes = {"message": msg} if parameters is not None: - scope.span.log_kv({ 'parameters': parameters }) + event_attributes.update({"parameters": parameters}) + span.add_event(name="log_with_instana", attributes=event_attributes) # extra tags for an error if argv[0] >= logging.ERROR: - scope.span.mark_as_errored() + span.mark_as_errored() + except Exception: - logger.debug('log_with_instana:', exc_info=True) + logger.debug("log_with_instana:", exc_info=True) return wrapped(*argv, **kwargs, stacklevel=stacklevel) -logger.debug('Instrumenting logging') - +logger.debug("Instrumenting logging") diff --git a/src/instana/instrumentation/mysqlclient.py b/src/instana/instrumentation/mysqlclient.py index 5b7270f8..82165869 100644 --- a/src/instana/instrumentation/mysqlclient.py +++ b/src/instana/instrumentation/mysqlclient.py @@ -2,17 +2,17 @@ # (c) Copyright Instana Inc. 2019 -from ..log import logger -from .pep0249 import ConnectionFactory +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: import MySQLdb - cf = ConnectionFactory(connect_func=MySQLdb.connect, module_name='mysql') + cf = ConnectionFactory(connect_func=MySQLdb.connect, module_name="mysql") - setattr(MySQLdb, 'connect', cf) - if hasattr(MySQLdb, 'Connect'): - setattr(MySQLdb, 'Connect', cf) + setattr(MySQLdb, "connect", cf) + if hasattr(MySQLdb, "Connect"): + setattr(MySQLdb, "Connect", cf) logger.debug("Instrumenting mysqlclient") except ImportError: diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index d07dc5ef..a6ad5642 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -2,139 +2,205 @@ # (c) Copyright Instana Inc. 2018 # This is a wrapper for PEP-0249: Python Database API Specification v2.0 -import opentracing.ext.tags as ext import wrapt +from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Union, Callable, Optional +from typing_extensions import Self -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off -from ..util.sql import sql_sanitizer +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.util.sql import sql_sanitizer + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan -class CursorWrapper(wrapt.ObjectProxy): - __slots__ = ('_module_name', '_connect_params', '_cursor_params') - def __init__(self, cursor, module_name, - connect_params=None, cursor_params=None): +class CursorWrapper(wrapt.ObjectProxy): + __slots__ = ("_module_name", "_connect_params", "_cursor_params") + + def __init__( + self, + cursor: Any, + module_name: str, + connect_params: Optional[List[Union[str, Dict[str, Any]]]] = None, + cursor_params: Optional[Dict[str, Any]] = None, + ) -> None: super(CursorWrapper, self).__init__(wrapped=cursor) self._module_name = module_name self._connect_params = connect_params self._cursor_params = cursor_params - def _collect_kvs(self, span, sql): + def _collect_kvs( + self, + span: "InstanaSpan", + sql: str, + ) -> None: try: - span.set_tag(ext.SPAN_KIND, 'exit') - - db_parameter_name = next((p for p in ('db', 'database', 'dbname') if p in self._connect_params[1]), None) + span.set_attribute("span.kind", SpanKind.CLIENT) + + db_parameter_name = next( + ( + p + for p in ("db", "database", "dbname") + if p in self._connect_params[1] + ), + None, + ) if db_parameter_name: - span.set_tag(ext.DATABASE_INSTANCE, self._connect_params[1][db_parameter_name]) - - span.set_tag(ext.DATABASE_STATEMENT, sql_sanitizer(sql)) - span.set_tag(ext.DATABASE_USER, self._connect_params[1]['user']) - span.set_tag('host', self._connect_params[1]['host']) - span.set_tag('port', self._connect_params[1]['port']) + span.set_attribute( + SpanAttributes.DB_NAME, + self._connect_params[1][db_parameter_name], + ) + + span.set_attribute(SpanAttributes.DB_STATEMENT, sql_sanitizer(sql)) + span.set_attribute(SpanAttributes.DB_USER, self._connect_params[1]["user"]) + span.set_attribute("host", self._connect_params[1]["host"]) + span.set_attribute("port", self._connect_params[1]["port"]) except Exception as e: logger.debug(e) - return span - def __enter__(self): + def __enter__(self) -> Self: return self - def execute(self, sql, params=None): + def execute( + self, + sql: str, + params: Optional[Dict[str, Any]] = None, + ) -> Callable[[str, Dict[str, Any]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(sql, params) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: - self._collect_kvs(scope.span, sql) - + self._collect_kvs(span, sql) result = self.__wrapped__.execute(sql, params) except Exception as e: - if scope.span: - scope.span.log_exception(e) + if span: + span.record_exception(e) raise else: return result - def executemany(self, sql, seq_of_parameters): + def executemany( + self, + sql: str, + seq_of_parameters: List[Dict[str, Any]], + ) -> Callable[[str, List[Dict[str, Any]]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.executemany(sql, seq_of_parameters) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: - self._collect_kvs(scope.span, sql) - + self._collect_kvs(span, sql) result = self.__wrapped__.executemany(sql, seq_of_parameters) except Exception as e: - if scope.span: - scope.span.log_exception(e) + if span: + span.record_exception(e) raise else: return result - def callproc(self, proc_name, params): + def callproc( + self, + proc_name: str, + params: Dict[str, Any], + ) -> Callable[[str, Dict[str, Any]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(proc_name, params) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: - self._collect_kvs(scope.span, proc_name) - + self._collect_kvs(span, proc_name) result = self.__wrapped__.callproc(proc_name, params) - except Exception as e: - if scope.span: - scope.span.log_exception(e) - raise + except Exception: + try: + result = self.__wrapped__.execute(proc_name, params) + except Exception as e_execute: + if span: + span.record_exception(e_execute) + raise + else: + return result else: return result class ConnectionWrapper(wrapt.ObjectProxy): - __slots__ = ('_module_name', '_connect_params') - - def __init__(self, connection, module_name, connect_params): + __slots__ = ("_module_name", "_connect_params") + + def __init__( + self, + connection: "ConnectionWrapper", + module_name: str, + connect_params: List[Union[str, Dict[str, Any]]], + ) -> None: super(ConnectionWrapper, self).__init__(wrapped=connection) self._module_name = module_name self._connect_params = connect_params - def __enter__(self): + def __enter__(self) -> Self: return self - def cursor(self, *args, **kwargs): + def cursor( + self, + *args: Tuple[int, str, Dict[str, Any]], + **kwargs: Dict[str, Any], + ) -> CursorWrapper: return CursorWrapper( cursor=self.__wrapped__.cursor(*args, **kwargs), module_name=self._module_name, connect_params=self._connect_params, - cursor_params=(args, kwargs) if args or kwargs else None) + cursor_params=(args, kwargs) if args or kwargs else None, + ) - def begin(self): - return self.__wrapped__.begin() + def close(self) -> Callable[[], None]: + return self.__wrapped__.close() - def commit(self): + def commit(self) -> Callable[[], None]: return self.__wrapped__.commit() - def rollback(self): + def rollback(self) -> Callable[[], None]: return self.__wrapped__.rollback() class ConnectionFactory(object): - def __init__(self, connect_func, module_name): + def __init__( + self, + connect_func: CursorWrapper, + module_name: str, + ) -> None: self._connect_func = connect_func self._module_name = module_name self._wrapper_ctor = ConnectionWrapper - def __call__(self, *args, **kwargs): + def __call__( + self, + *args: Tuple[int, str, Dict[str, Any]], + **kwargs: Dict[str, Any], + ) -> ConnectionWrapper: connect_params = (args, kwargs) if args or kwargs else None - return self._wrapper_ctor( connection=self._connect_func(*args, **kwargs), module_name=self._module_name, - connect_params=connect_params) + connect_params=connect_params, + ) diff --git a/src/instana/instrumentation/pika.py b/src/instana/instrumentation/pika.py index cc9478cb..c8c74a5d 100644 --- a/src/instana/instrumentation/pika.py +++ b/src/instana/instrumentation/pika.py @@ -2,167 +2,261 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 - - -import wrapt -import opentracing -import types - -from ..log import logger -from ..singletons import tracer -from ..util.traceutils import get_tracer_tuple, tracing_is_off - try: - import pika - - - def _extract_broker_tags(span, conn): - span.set_tag("address", "%s:%d" % (conn.params.host, conn.params.port)) - - - def _extract_publisher_tags(span, conn, exchange, routing_key): - _extract_broker_tags(span, conn) - - span.set_tag("sort", "publish") - span.set_tag("key", routing_key) - span.set_tag("exchange", exchange) - - - def _extract_consumer_tags(span, conn, queue): - _extract_broker_tags(span, conn) + import types + from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterator, + Optional, + Tuple, + Union, + ) - span.set_tag("sort", "consume") - span.set_tag("queue", queue) - - - @wrapt.patch_function_wrapper('pika.channel', 'Channel.basic_publish') - def basic_publish_with_instana(wrapped, instance, args, kwargs): - def _bind_args(exchange, routing_key, body, properties=None, *args, **kwargs): + import pika + import wrapt + + from instana.log import logger + from instana.propagators.format import Format + from instana.singletons import tracer + from instana.util.traceutils import get_tracer_tuple, tracing_is_off + + if TYPE_CHECKING: + import pika.adapters.blocking_connection + import pika.channel + import pika.connection + + from instana.span.span import InstanaSpan + + def _extract_broker_attributes( + span: "InstanaSpan", conn: pika.connection.Connection + ) -> None: + span.set_attribute("address", f"{conn.params.host}:{conn.params.port}") + + def _extract_publisher_attributes( + span: "InstanaSpan", + conn: pika.connection.Connection, + exchange: str, + routing_key: str, + ) -> None: + _extract_broker_attributes(span, conn) + + span.set_attribute("sort", "publish") + span.set_attribute("key", routing_key) + span.set_attribute("exchange", exchange) + + def _extract_consumer_tags( + span: "InstanaSpan", conn: pika.connection.Connection, queue: str + ) -> None: + _extract_broker_attributes(span, conn) + + span.set_attribute("sort", "consume") + span.set_attribute("queue", queue) + + @wrapt.patch_function_wrapper("pika.channel", "Channel.basic_publish") + def basic_publish_with_instana( + wrapped: Callable[..., pika.channel.Channel.basic_publish], + instance: pika.channel.Channel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + exchange: str, + routing_key: str, + body: str, + properties: Optional[object] = None, + *args: object, + **kwargs: object, + ) -> Tuple[object, ...]: return (exchange, routing_key, body, properties, args, kwargs) - tracer, parent_span, _ = get_tracer_tuple() - + # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - (exchange, routing_key, body, properties, args, kwargs) = (_bind_args(*args, **kwargs)) + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None + + (exchange, routing_key, body, properties, args, kwargs) = _bind_args( + *args, **kwargs + ) - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_publisher_tags(scope.span, - conn=instance.connection, - routing_key=routing_key, - exchange=exchange) - except: - logger.debug("publish_with_instana: ", exc_info=True) + _extract_publisher_attributes( + span, + conn=instance.connection, + routing_key=routing_key, + exchange=exchange, + ) + except Exception: + logger.debug("pika publish_with_instana error: ", exc_info=True) # context propagation properties = properties or pika.BasicProperties() properties.headers = properties.headers or {} - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) + tracer.inject( + span.context, + Format.HTTP_HEADERS, + properties.headers, + disable_w3c_trace_context=True, + ) args = (exchange, routing_key, body, properties) + args try: rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - def basic_get_with_instana(wrapped, instance, args, kwargs): - def _bind_args(*args, **kwargs): + def basic_get_with_instana( + wrapped: Callable[ + ..., + Union[pika.channel.Channel.basic_get, pika.channel.Channel.basic_consume], + ], + instance: pika.channel.Channel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args(*args: object, **kwargs: object) -> Tuple[object, ...]: args = list(args) - queue = kwargs.pop('queue', None) or args.pop(0) - callback = kwargs.pop('callback', None) or kwargs.pop('on_message_callback', None) or args.pop(0) + queue = kwargs.pop("queue", None) or args.pop(0) + callback = ( + kwargs.pop("callback", None) + or kwargs.pop("on_message_callback", None) + or args.pop(0) + ) return (queue, callback, tuple(args), kwargs) queue, callback, args, kwargs = _bind_args(*args, **kwargs) - def _cb_wrapper(channel, method, properties, body): - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + def _cb_wrapper( + channel: pika.channel.Channel, + method: pika.spec.Basic, + properties: pika.BasicProperties, + body: str, + ) -> None: + parent_context = tracer.extract( + Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True + ) + + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection, - queue=queue) - except: - logger.debug("basic_get_with_instana: ", exc_info=True) + _extract_consumer_tags(span, conn=instance.connection, queue=queue) + except Exception: + logger.debug("pika basic_get_with_instana error: ", exc_info=True) try: callback(channel, method, properties, body) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) args = (queue, _cb_wrapper) + args return wrapped(*args, **kwargs) - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.basic_consume') - def basic_consume_with_instana(wrapped, instance, args, kwargs): - def _bind_args(queue, on_message_callback, *args, **kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.basic_consume" + ) + def basic_consume_with_instana( + wrapped: Callable[ + ..., pika.adapters.blocking_connection.BlockingChannel.basic_consume + ], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + queue: str, + on_message_callback: object, + *args: object, + **kwargs: object, + ) -> Tuple[object, ...]: return (queue, on_message_callback, args, kwargs) queue, on_message_callback, args, kwargs = _bind_args(*args, **kwargs) - def _cb_wrapper(channel, method, properties, body): - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + def _cb_wrapper( + channel: pika.channel.Channel, + method: pika.spec.Basic, + properties: pika.BasicProperties, + body: str, + ) -> None: + parent_context = tracer.extract( + Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True + ) + + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection._impl, - queue=queue) - except: - logger.debug("basic_consume_with_instana: ", exc_info=True) + _extract_consumer_tags( + span, conn=instance.connection._impl, queue=queue + ) + except Exception: + logger.debug( + "pika basic_consume_with_instana error:", exc_info=True + ) try: on_message_callback(channel, method, properties, body) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) args = (queue, _cb_wrapper) + args return wrapped(*args, **kwargs) - - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.consume') - def consume_with_instana(wrapped, instance, args, kwargs): - def _bind_args(queue, *args, **kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.consume" + ) + def consume_with_instana( + wrapped: Callable[..., pika.adapters.blocking_connection.BlockingChannel], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + queue: str, *args: object, **kwargs: object + ) -> Tuple[object, ...]: return (queue, args, kwargs) - (queue, args, kwargs) = (_bind_args(*args, **kwargs)) + (queue, args, kwargs) = _bind_args(*args, **kwargs) - def _consume(gen): - for yilded in gen: + def _consume(gen: Iterator[object]) -> object: + for yielded in gen: # Bypass the delivery created due to inactivity timeout - if yilded is None or not any(yilded): - yield yilded + if not yielded or not any(yielded): + yield yielded continue - (method_frame, properties, body) = yilded + (method_frame, properties, body) = yielded - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + parent_context = tracer.extract( + Format.HTTP_HEADERS, + properties.headers, + disable_w3c_trace_context=True, + ) + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection._impl, - queue=queue) - except: + _extract_consumer_tags( + span, conn=instance.connection._impl, queue=queue + ) + except Exception: logger.debug("consume_with_instana: ", exc_info=True) try: - yield yilded - except Exception as e: - scope.span.log_exception(e) - raise + yield yielded + except Exception as exc: + span.record_exception(exc) args = (queue,) + args res = wrapped(*args, **kwargs) @@ -172,20 +266,31 @@ def _consume(gen): else: return res - - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.__init__') - def _BlockingChannel___init__(wrapped, instance, args, kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.__init__" + ) + def _BlockingChannel___init__( + wrapped: Callable[ + ..., pika.adapters.blocking_connection.BlockingChannel.__init__ + ], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: ret = wrapped(*args, **kwargs) - impl = getattr(instance, '_impl', None) + impl = getattr(instance, "_impl", None) - if impl and hasattr(impl.basic_consume, '__wrapped__'): + if impl and hasattr(impl.basic_consume, "__wrapped__"): impl.basic_consume = impl.basic_consume.__wrapped__ return ret - - wrapt.wrap_function_wrapper('pika.channel', 'Channel.basic_get', basic_get_with_instana) - wrapt.wrap_function_wrapper('pika.channel', 'Channel.basic_consume', basic_get_with_instana) + wrapt.wrap_function_wrapper( + "pika.channel", "Channel.basic_get", basic_get_with_instana + ) + wrapt.wrap_function_wrapper( + "pika.channel", "Channel.basic_consume", basic_get_with_instana + ) logger.debug("Instrumenting pika") except ImportError: diff --git a/src/instana/instrumentation/psycopg2.py b/src/instana/instrumentation/psycopg2.py index 86e35b10..6045c4c9 100644 --- a/src/instana/instrumentation/psycopg2.py +++ b/src/instana/instrumentation/psycopg2.py @@ -5,33 +5,44 @@ import copy import wrapt -from ..log import logger -from .pep0249 import ConnectionFactory +from typing import Callable, Optional, Any, Tuple, Dict +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: import psycopg2 import psycopg2.extras - cf = ConnectionFactory(connect_func=psycopg2.connect, module_name='postgres') + cf = ConnectionFactory(connect_func=psycopg2.connect, module_name="postgres") - setattr(psycopg2, 'connect', cf) - if hasattr(psycopg2, 'Connect'): - setattr(psycopg2, 'Connect', cf) + setattr(psycopg2, "connect", cf) + if hasattr(psycopg2, "Connect"): + setattr(psycopg2, "Connect", cf) - @wrapt.patch_function_wrapper('psycopg2.extensions', 'register_type') - def register_type_with_instana(wrapped, instance, args, kwargs): + @wrapt.patch_function_wrapper("psycopg2.extensions", "register_type") + def register_type_with_instana( + wrapped: Callable[..., Any], + instance: Optional[Any], + args: Tuple[Any, ...], + kwargs: Dict[str, Any], + ) -> Callable[..., object]: args_clone = list(copy.copy(args)) - if (len(args_clone) >= 2) and hasattr(args_clone[1], '__wrapped__'): + if (len(args_clone) >= 2) and hasattr(args_clone[1], "__wrapped__"): args_clone[1] = args_clone[1].__wrapped__ return wrapped(*args_clone, **kwargs) - @wrapt.patch_function_wrapper('psycopg2._json', 'register_json') - def register_json_with_instana(wrapped, instance, args, kwargs): - if 'conn_or_curs' in kwargs: - if hasattr(kwargs['conn_or_curs'], '__wrapped__'): - kwargs['conn_or_curs'] = kwargs['conn_or_curs'].__wrapped__ + @wrapt.patch_function_wrapper("psycopg2._json", "register_json") + def register_json_with_instana( + wrapped: Callable[..., Any], + instance: Optional[Any], + args: Tuple[Any, ...], + kwargs: Dict[str, Any], + ) -> Callable[..., object]: + if "conn_or_curs" in kwargs: + if hasattr(kwargs["conn_or_curs"], "__wrapped__"): + kwargs["conn_or_curs"] = kwargs["conn_or_curs"].__wrapped__ return wrapped(*args, **kwargs) diff --git a/src/instana/instrumentation/pymongo.py b/src/instana/instrumentation/pymongo.py index 264fd658..2c0bc203 100644 --- a/src/instana/instrumentation/pymongo.py +++ b/src/instana/instrumentation/pymongo.py @@ -2,43 +2,49 @@ # (c) Copyright Instana Inc. 2020 -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.span.span import InstanaSpan +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import pymongo - from pymongo import monitoring from bson import json_util + from opentelemetry.semconv.trace import SpanAttributes - - class MongoCommandTracer(monitoring.CommandListener): - def __init__(self): + class MongoCommandTracer(pymongo.monitoring.CommandListener): + def __init__(self) -> None: self.__active_commands = {} - def started(self, event): + def started(self, event: pymongo.monitoring.CommandStartedEvent) -> None: tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing if tracing_is_off(): return + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span("mongo", child_of=parent_span) as scope: - self._collect_connection_tags(scope.span, event) - self._collect_command_tags(scope.span, event) + with tracer.start_as_current_span( + "mongo", span_context=parent_context + ) as span: + self._collect_connection_tags(span, event) + self._collect_command_tags(span, event) # include collection name into the namespace if provided if event.command_name in event.command: - scope.span.set_tag("collection", event.command.get(event.command_name)) + span.set_attribute( + SpanAttributes.DB_MONGODB_COLLECTION, + event.command.get(event.command_name), + ) - self.__active_commands[event.request_id] = scope + self.__active_commands[event.request_id] = span - def succeeded(self, event): + def succeeded(self, event: pymongo.monitoring.CommandStartedEvent) -> None: active_span = self.__active_commands.pop(event.request_id, None) # return early if we're not tracing if active_span is None: return - def failed(self, event): + def failed(self, event: pymongo.monitoring.CommandStartedEvent) -> None: active_span = self.__active_commands.pop(event.request_id, None) # return early if we're not tracing @@ -47,23 +53,27 @@ def failed(self, event): active_span.log_exception(event.failure) - def _collect_connection_tags(self, span, event): + def _collect_connection_tags( + self, span: InstanaSpan, event: pymongo.monitoring.CommandStartedEvent + ) -> None: (host, port) = event.connection_id - span.set_tag("host", host) - span.set_tag("port", str(port)) - span.set_tag("db", event.database_name) + span.set_attribute(SpanAttributes.SERVER_ADDRESS, host) + span.set_attribute(SpanAttributes.SERVER_PORT, str(port)) + span.set_attribute(SpanAttributes.DB_NAME, event.database_name) - def _collect_command_tags(self, span, event): + def _collect_command_tags(self, span, event) -> None: """ Extract MongoDB command name and arguments and attach it to the span """ cmd = event.command_name - span.set_tag("command", cmd) + span.set_attribute("command", cmd) for key in ["filter", "query"]: if key in event.command: - span.set_tag("filter", json_util.dumps(event.command.get(key))) + span.set_attribute( + "filter", json_util.dumps(event.command.get(key)) + ) break # The location of command documents within the command object depends on the name @@ -72,24 +82,25 @@ def _collect_command_tags(self, span, event): "insert": "documents", "update": "updates", "delete": "deletes", - "aggregate": "pipeline" + "aggregate": "pipeline", } cmd_doc = None if cmd in cmd_doc_locations: cmd_doc = event.command.get(cmd_doc_locations[cmd]) - elif cmd.lower() == "mapreduce": # mapreduce command was renamed to mapReduce in pymongo 3.9.0 + elif ( + cmd.lower() == "mapreduce" + ): # mapreduce command was renamed to mapReduce in pymongo 3.9.0 # mapreduce command consists of two mandatory parts: map and reduce cmd_doc = { "map": event.command.get("map"), - "reduce": event.command.get("reduce") + "reduce": event.command.get("reduce"), } if cmd_doc is not None: - span.set_tag("json", json_util.dumps(cmd_doc)) - + span.set_attribute("json", json_util.dumps(cmd_doc)) - monitoring.register(MongoCommandTracer()) + pymongo.monitoring.register(MongoCommandTracer()) logger.debug("Instrumenting pymongo") diff --git a/src/instana/instrumentation/pymysql.py b/src/instana/instrumentation/pymysql.py index c4939cc4..50cf9b3d 100644 --- a/src/instana/instrumentation/pymysql.py +++ b/src/instana/instrumentation/pymysql.py @@ -2,17 +2,17 @@ # (c) Copyright Instana Inc. 2019 -from ..log import logger -from .pep0249 import ConnectionFactory +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: - import pymysql # + import pymysql - cf = ConnectionFactory(connect_func=pymysql.connect, module_name='mysql') + cf = ConnectionFactory(connect_func=pymysql.connect, module_name="mysql") - setattr(pymysql, 'connect', cf) - if hasattr(pymysql, 'Connect'): - setattr(pymysql, 'Connect', cf) + setattr(pymysql, "connect", cf) + if hasattr(pymysql, "Connect"): + setattr(pymysql, "Connect", cf) logger.debug("Instrumenting pymysql") except ImportError: 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/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/src/instana/instrumentation/redis.py b/src/instana/instrumentation/redis.py index 5c9ed522..ec439ef4 100644 --- a/src/instana/instrumentation/redis.py +++ b/src/instana/instrumentation/redis.py @@ -2,94 +2,115 @@ # (c) Copyright Instana Inc. 2018 +from typing import Any, Callable, Dict, Tuple import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off - +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import redis EXCLUDED_PARENT_SPANS = ["redis", "celery-client", "celery-worker"] - def collect_tags(span, instance, args, kwargs): + def collect_attributes( + span: InstanaSpan, + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> None: try: ckw = instance.connection_pool.connection_kwargs - span.set_tag("driver", "redis-py") + span.set_attribute("driver", "redis-py") - host = ckw.get('host', None) - port = ckw.get('port', '6379') - db = ckw.get('db', None) + host = ckw.get("host", None) + port = ckw.get("port", "6379") + db = ckw.get("db", None) - if host is not None: - url = "redis://%s:%s" % (host, port) + if host: + url = f"redis://{host}:{port}" if db is not None: - url = url + "/%s" % db - span.set_tag('connection', url) - - except: - logger.debug("redis.collect_tags non-fatal error", exc_info=True) - - return span - - - def execute_command_with_instana(wrapped, instance, args, kwargs): + url = f"{url}/{db}" + span.set_attribute("connection", url) + except Exception: + logger.debug("redis.collect_attributes non-fatal error", exc_info=True) + + def execute_command_with_instana( + wrapped: Callable[..., object], + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if (tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS)): + if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) - with tracer.start_active_span("redis", child_of=parent_span) as scope: + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: - collect_tags(scope.span, instance, args, kwargs) - if (len(args) > 0): - scope.span.set_tag("command", args[0]) + collect_attributes(span, instance, args, kwargs) + if len(args) > 0: + span.set_attribute("command", args[0]) rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) + except Exception as exc: + span.record_exception(exc) raise else: return rv - - def execute_with_instana(wrapped, instance, args, kwargs): + def execute_with_instana( + wrapped: Callable[..., object], + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if (tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS)): + if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) - with tracer.start_active_span("redis", child_of=parent_span) as scope: + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: - collect_tags(scope.span, instance, args, kwargs) - scope.span.set_tag("command", 'PIPELINE') + collect_attributes(span, instance, args, kwargs) + span.set_attribute("command", "PIPELINE") pipe_cmds = [] for e in instance.command_stack: pipe_cmds.append(e[0][0]) - scope.span.set_tag("subCommands", pipe_cmds) + span.set_attribute("subCommands", pipe_cmds) except Exception as e: # If anything breaks during K/V collection, just log a debug message logger.debug("Error collecting pipeline commands", exc_info=True) try: rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) + except Exception as exc: + span.record_exception(exc) raise else: return rv - if redis.VERSION < (3,0,0): - wrapt.wrap_function_wrapper('redis.client', 'BasePipeline.execute', execute_with_instana) - wrapt.wrap_function_wrapper('redis.client', 'StrictRedis.execute_command', execute_command_with_instana) + if redis.VERSION < (3, 0, 0): + wrapt.wrap_function_wrapper( + "redis.client", "BasePipeline.execute", execute_with_instana + ) + wrapt.wrap_function_wrapper( + "redis.client", "StrictRedis.execute_command", execute_command_with_instana + ) else: - wrapt.wrap_function_wrapper('redis.client', 'Pipeline.execute', execute_with_instana) - wrapt.wrap_function_wrapper('redis.client', 'Redis.execute_command', execute_command_with_instana) + wrapt.wrap_function_wrapper( + "redis.client", "Pipeline.execute", execute_with_instana + ) + wrapt.wrap_function_wrapper( + "redis.client", "Redis.execute_command", execute_command_with_instana + ) logger.debug("Instrumenting redis") except ImportError: diff --git a/src/instana/instrumentation/sanic_inst.py b/src/instana/instrumentation/sanic_inst.py index 39d44549..acc3f621 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -5,140 +5,122 @@ 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): - try: - message = kwargs.get("message") or args[0] - status_code = kwargs.get("status_code") - span = async_tracer.active_span - - if all([span, status_code, message]) and 500 <= status_code: - span.set_tag("http.error", message) - try: - wrapped(*args, **kwargs) - except Exception as exc: - span.log_exception(exc) - else: - wrapped(*args, **kwargs) - except Exception: - logger.debug("exception_with_instana: ", exc_info=True) - wrapped(*args, **kwargs) - - - def response_details(span, response): - try: - status_code = response.status - if status_code is not None: - if 500 <= int(status_code): - span.mark_as_errored() - span.set_tag('http.status_code', status_code) - - if response.headers is not None: - extract_custom_headers(span, response.headers) - 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 + from typing import Callable, Tuple, Dict, Any + from sanic.exceptions import SanicException + + from opentelemetry import context, trace + from opentelemetry.trace import SpanKind + from opentelemetry.semconv.trace import SpanAttributes + + from instana.log import logger + from instana.singletons import tracer, agent + from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers + from instana.propagators.format import Format + + from sanic.request import Request + from sanic.response import HTTPResponse + + @wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__") + def init_with_instana( + wrapped: Callable[..., sanic.app.Sanic.__init__], + instance: sanic.app.Sanic, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> None: + wrapped(*args, **kwargs) + app = instance + + @app.middleware("request") + def request_with_instana(request: Request) -> None: + try: 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) + return + + headers = request.headers.copy() + parent_context = tracer.extract(Format.HTTP_HEADERS, headers) + + span = tracer.start_span("asgi", span_context=parent_context) + request.ctx.span = span + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + request.ctx.token = token + + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.path", request.path) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_HOST, request.host) if hasattr(request, "url"): - scope.span.set_tag("http.url", request.url) + span.set_attribute(SpanAttributes.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.span, headers) - await wrapped(*args, **kwargs) + query = query.decode("utf-8") + scrubbed_params = strip_secrets_from_query( + query, agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", scrubbed_params) + + if agent.options.extra_http_headers: + extract_custom_headers(span, headers) if hasattr(request, "uri_template") and 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") + span.set_attribute("http.path_tpl", request.uri_template) + except Exception: + logger.debug("request_with_instana: ", exc_info=True) + + @app.exception(Exception) + def exception_with_instana(request: Request, exception: Exception) -> None: + try: + if not hasattr(request.ctx, "span"): # pragma: no cover + return + span = request.ctx.span + + if isinstance(exception, SanicException): + # Handle Sanic-specific exceptions + status_code = exception.status_code + message = str(exception) + + if all([span, status_code, message]) and 500 <= status_code: + span.set_attribute("http.error", message) + except Exception: + logger.debug("exception_with_instana: ", exc_info=True) + + @app.middleware("response") + def response_with_instana(request: Request, response: HTTPResponse) -> None: + try: + if not hasattr(request.ctx, "span"): # pragma: no cover + return + span = request.ctx.span + + status_code = response.status + if status_code: + if int(status_code) >= 500: + span.mark_as_errored() + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + + if hasattr(response, "headers"): + 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 + ) + + if span.is_recording(): + span.end() + request.ctx.span = None + + if request.ctx.token: + context.detach(request.ctx.token) + request.ctx.token = None + except Exception: + logger.debug("response_with_instana: ", exc_info=True) except ImportError: pass -except AttributeError: - logger.debug("Not supported Sanic version") diff --git a/src/instana/instrumentation/sqlalchemy.py b/src/instana/instrumentation/sqlalchemy.py index 2adf705a..3f44b526 100644 --- a/src/instana/instrumentation/sqlalchemy.py +++ b/src/instana/instrumentation/sqlalchemy.py @@ -3,90 +3,119 @@ import re -from operator import attrgetter +from typing import Any, Dict -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from opentelemetry import context, trace + +from instana.log import logger +from instana.span.span import InstanaSpan, get_current_span +from instana.span_context import SpanContext +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: - import sqlalchemy + from sqlalchemy import __version__ as sqlalchemy_version from sqlalchemy import event from sqlalchemy.engine import Engine url_regexp = re.compile(r"\/\/(\S+@)") - - @event.listens_for(Engine, 'before_cursor_execute', named=True) - def receive_before_cursor_execute(**kw): + @event.listens_for(Engine, "before_cursor_execute", named=True) + def receive_before_cursor_execute( + **kw: Dict[str, Any], + ) -> None: try: # If we're not tracing, just return if tracing_is_off(): return tracer, parent_span, _ = get_tracer_tuple() - scope = tracer.start_active_span("sqlalchemy", child_of=parent_span) - context = kw['context'] - if context: - context._stan_scope = scope - - conn = kw['conn'] - url = str(conn.engine.url) - scope.span.set_tag('sqlalchemy.sql', kw['statement']) - scope.span.set_tag('sqlalchemy.eng', conn.engine.name) - scope.span.set_tag('sqlalchemy.url', url_regexp.sub('//', url)) - except Exception as e: - logger.debug(e) - return - - - @event.listens_for(Engine, 'after_cursor_execute', named=True) - def receive_after_cursor_execute(**kw): - context = kw['context'] - - if context is not None and hasattr(context, '_stan_scope'): - scope = context._stan_scope - if scope is not None: - scope.close() + parent_context = parent_span.get_span_context() if parent_span else None + + span = tracer.start_span("sqlalchemy", span_context=parent_context) + conn = kw["conn"] + conn.span = span + span.set_attribute("sqlalchemy.sql", kw["statement"]) + span.set_attribute("sqlalchemy.eng", conn.engine.name) + span.set_attribute( + "sqlalchemy.url", url_regexp.sub("//", str(conn.engine.url)) + ) + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + conn.token = token + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_before_cursor_execute", + exc_info=True, + ) + + @event.listens_for(Engine, "after_cursor_execute", named=True) + def receive_after_cursor_execute( + **kw: Dict[str, Any], + ) -> None: + try: + # If we're not tracing, just return + if tracing_is_off(): + return + current_span = get_current_span() + conn = kw["conn"] + if current_span.is_recording(): + current_span.end() + if hasattr(conn, "token"): + context.detach(conn.token) + conn.token = None + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_after_cursor_execute", + exc_info=True, + ) error_event = "handle_error" # Handle dbapi_error event; deprecated since version 0.9 - if sqlalchemy.__version__[0] == "0": + if sqlalchemy_version[0] == "0": error_event = "dbapi_error" - - def _set_error_tags(context, exception_string, scope_string): - scope, context_exception = None, None - if attrgetter(scope_string)(context) and attrgetter(exception_string)(context): - scope = attrgetter(scope_string)(context) - context_exception = attrgetter(exception_string)(context) - if scope and context_exception: - scope.span.log_exception(context_exception) - scope.close() + def _set_error_attributes( + context: SpanContext, + exception_string: str, + span: InstanaSpan, + ) -> None: + context_exception = None, None + if hasattr(context, exception_string): + context_exception = getattr(context, exception_string) + if span and context_exception: + span.record_exception(context_exception) else: - scope.span.log_exception("No %s specified." % error_event) - scope.close() - + span.record_exception(f"No {error_event} specified.") + if span.is_recording(): + span.end() @event.listens_for(Engine, error_event, named=True) - def receive_handle_db_error(**kw): - - if tracing_is_off(): - return + def receive_handle_db_error( + **kw: Dict[str, Any], + ) -> None: + try: + if tracing_is_off(): + return - # support older db error event - if error_event == "dbapi_error": - context = kw.get('context') - exception_string = 'exception' - scope_string = '_stan_scope' - else: - context = kw.get('exception_context') - exception_string = 'sqlalchemy_exception' - scope_string = 'execution_context._stan_scope' + current_span = get_current_span() - if context: - _set_error_tags(context, exception_string, scope_string) + # support older db error event + if error_event == "dbapi_error": + context = kw.get("context") + exception_string = "exception" + else: + context = kw.get("exception_context") + exception_string = "sqlalchemy_exception" + if context: + _set_error_attributes(context, exception_string, current_span) + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_handle_db_error", + exc_info=True, + ) logger.debug("Instrumenting sqlalchemy") diff --git a/src/instana/instrumentation/starlette_inst.py b/src/instana/instrumentation/starlette_inst.py index 66c3d0b3..9d4b1f8e 100644 --- a/src/instana/instrumentation/starlette_inst.py +++ b/src/instana/instrumentation/starlette_inst.py @@ -5,23 +5,34 @@ Instrumentation for Starlette https://www.starlette.io/ """ + +from typing import Any, Callable, Dict, Tuple + try: import starlette import wrapt - from ..log import logger - from .asgi import InstanaASGIMiddleware from starlette.middleware import Middleware + import starlette.applications - @wrapt.patch_function_wrapper('starlette.applications', 'Starlette.__init__') - def init_with_instana(wrapped, instance, args, kwargs): - middleware = kwargs.get('middleware') + from instana.instrumentation.asgi import InstanaASGIMiddleware + from instana.log import logger + + @wrapt.patch_function_wrapper("starlette.applications", "Starlette.__init__") + def init_with_instana( + wrapped: Callable[..., starlette.applications.Starlette.__init__], + instance: starlette.applications.Starlette, + args: Tuple, + kwargs: Dict[str, Any], + ) -> None: + middleware = kwargs.get("middleware") if middleware is None: - kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + kwargs["middleware"] = [Middleware(InstanaASGIMiddleware)] elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) return wrapped(*args, **kwargs) logger.debug("Instrumenting Starlette") + except ImportError: pass diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 12542bc5..00ee7648 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -2,109 +2,137 @@ # (c) Copyright Instana Inc. 2017 -import opentracing -import opentracing.ext.tags as ext +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union + import wrapt +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent +from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import get_tracer_tuple, tracing_is_off -from ..log import logger -from ..singletons import agent -from ..util.traceutils import get_tracer_tuple, tracing_is_off -from ..util.secrets import strip_secrets_from_query +if TYPE_CHECKING: + from instana.span.span import InstanaSpan try: import urllib3 - - def extract_custom_headers(span, headers): + def _extract_custom_headers(span: "InstanaSpan", headers: Dict[str, Any]) -> None: 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]) - + span.set_attribute( + f"http.header.{custom_header}", headers[custom_header] + ) except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - - - def collect(instance, args, kwargs): - """ Build and return a fully qualified URL for this request """ + logger.debug("urllib3 _extract_custom_headers error: ", exc_info=True) + + def _collect_kvs( + instance: Union[ + urllib3.connectionpool.HTTPConnectionPool, + urllib3.connectionpool.HTTPSConnectionPool, + ], + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> Dict[str, Any]: kvs = dict() try: - kvs['host'] = instance.host - kvs['port'] = instance.port + kvs["host"] = instance.host + kvs["port"] = instance.port - if args is not None and len(args) == 2: - kvs['method'] = args[0] - kvs['path'] = args[1] + if args and len(args) == 2: + kvs["method"] = args[0] + kvs["path"] = args[1] else: - kvs['method'] = kwargs.get('method') - kvs['path'] = kwargs.get('path') - if kvs['path'] is None: - kvs['path'] = kwargs.get('url') + kvs["method"] = kwargs.get("method") + kvs["path"] = ( + kwargs.get("path") if kwargs.get("path") else kwargs.get("url") + ) # Strip any secrets from potential query params - if kvs.get('path') is not None and ('?' in kvs['path']): - parts = kvs['path'].split('?') - kvs['path'] = parts[0] + if kvs.get("path") and ("?" in kvs["path"]): + parts = kvs["path"].split("?") + kvs["path"] = parts[0] if len(parts) == 2: - kvs['query'] = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, - agent.options.secrets_list) - - if type(instance) is urllib3.connectionpool.HTTPSConnectionPool: - kvs['url'] = 'https://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) + kvs["query"] = strip_secrets_from_query( + parts[1], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + + url = kvs["host"] + ":" + str(kvs["port"]) + kvs["path"] + if isinstance(instance, urllib3.connectionpool.HTTPSConnectionPool): + kvs["url"] = f"https://{url}" else: - kvs['url'] = 'http://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) + kvs["url"] = f"http://{url}" except Exception: - logger.debug("urllib3 collect error", exc_info=True) + logger.debug("urllib3 _collect_kvs error: ", exc_info=True) return kvs else: return kvs - - def collect_response(scope, response): + def collect_response( + span: "InstanaSpan", response: urllib3.response.HTTPResponse + ) -> None: try: - scope.span.set_tag(ext.HTTP_STATUS_CODE, response.status) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) - extract_custom_headers(scope.span, response.headers) + _extract_custom_headers(span, response.headers) if 500 <= response.status: - scope.span.mark_as_errored() + span.mark_as_errored() except Exception: - logger.debug("collect_response", exc_info=True) + logger.debug("urllib3 collect_response error: ", exc_info=True) + + @wrapt.patch_function_wrapper("urllib3", "HTTPConnectionPool.urlopen") + def urlopen_with_instana( + wrapped: Callable[ + ..., Union[urllib3.HTTPConnectionPool, urllib3.HTTPSConnectionPool] + ], + instance: Union[ + urllib3.connectionpool.HTTPConnectionPool, + urllib3.connectionpool.HTTPSConnectionPool, + ], + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> urllib3.response.HTTPResponse: + tracer, parent_span, span_name = get_tracer_tuple() - - @wrapt.patch_function_wrapper('urllib3', 'HTTPConnectionPool.urlopen') - def urlopen_with_instana(wrapped, instance, args, kwargs): - tracer, parent_span, operation_name = get_tracer_tuple() # If we're not tracing, just return; boto3 has it's own visibility - if (tracing_is_off() or (operation_name == 'boto3')): + if tracing_is_off() or (span_name == "boto3"): return wrapped(*args, **kwargs) - with tracer.start_active_span("urllib3", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "urllib3", span_context=parent_context + ) as span: try: - kvs = collect(instance, args, kwargs) - if 'url' in kvs: - scope.span.set_tag(ext.HTTP_URL, kvs['url']) - if 'query' in kvs: - scope.span.set_tag("http.params", kvs['query']) - if 'method' in kvs: - scope.span.set_tag(ext.HTTP_METHOD, kvs['method']) - - if 'headers' in kwargs: - extract_custom_headers(scope.span, kwargs['headers']) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, kwargs['headers']) + kvs = _collect_kvs(instance, args, kwargs) + if "url" in kvs: + span.set_attribute(SpanAttributes.HTTP_URL, kvs["url"]) + if "query" in kvs: + span.set_attribute("http.params", kvs["query"]) + if "method" in kvs: + span.set_attribute(SpanAttributes.HTTP_METHOD, kvs["method"]) + if "headers" in kwargs: + _extract_custom_headers(span, kwargs["headers"]) + tracer.inject(span.context, Format.HTTP_HEADERS, kwargs["headers"]) response = wrapped(*args, **kwargs) - collect_response(scope, response) + collect_response(span, response) return response except Exception as e: - scope.span.mark_as_errored({'message': e}) + span.record_exception(e) raise - logger.debug("Instrumenting urllib3") except ImportError: pass diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 3a981d2f..40d7e340 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -4,56 +4,73 @@ """ Instana WSGI Middleware """ -import opentracing as ot -import opentracing.ext.tags as tags +from typing import Dict, Any, Callable, List, Tuple, Optional -from ..singletons import agent, tracer -from ..util.secrets import strip_secrets_from_query +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry import context, trace + +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query class InstanaWSGIMiddleware(object): - """ Instana WSGI middleware """ + """Instana WSGI middleware""" - def __init__(self, app): + def __init__(self, app: object) -> None: self.app = app - def __call__(self, environ, start_response): + def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ - def new_start_response(status, headers, exc_info=None): + def new_start_response(status: str, headers: List[Tuple[object, ...]], exc_info: Optional[Exception] = None) -> object: """Modified start response with additional headers.""" - tracer.inject(self.scope.span.context, ot.Format.HTTP_HEADERS, headers) - headers.append(('Server-Timing', "intid;desc=%s" % self.scope.span.context.trace_id)) + tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) + headers.append( + ("Server-Timing", "intid;desc=%s" % self.span.context.trace_id) + ) - res = start_response(status, headers, exc_info) + headers_str = [(header[0], str(header[1])) if not isinstance(header[1], str) else header for header in headers] + res = start_response(status, headers_str, exc_info) - sc = status.split(' ')[0] + sc = status.split(" ")[0] if 500 <= int(sc): - self.scope.span.mark_as_errored() + self.span.mark_as_errored() - self.scope.span.set_tag(tags.HTTP_STATUS_CODE, sc) - self.scope.close() + self.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) + if self.span and self.span.is_recording(): + self.span.end() + if self.token: + context.detach(self.token) return res - ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) - self.scope = tracer.start_active_span("wsgi", child_of=ctx) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + self.span = tracer.start_span("wsgi", span_context=span_context) + + ctx = trace.set_span_in_context(self.span) + self.token = context.attach(ctx) if agent.options.extra_http_headers is not None: for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS - wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') + wsgi_header = ("HTTP_" + custom_header.upper()).replace("-", "_") if wsgi_header in env: - self.scope.span.set_tag("http.header.%s" % custom_header, env[wsgi_header]) - - if 'PATH_INFO' in env: - self.scope.span.set_tag('http.path', env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - self.scope.span.set_tag("http.params", scrubbed_params) - if 'REQUEST_METHOD' in env: - self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) - if 'HTTP_HOST' in env: - self.scope.span.set_tag("http.host", env['HTTP_HOST']) + self.span.set_attribute( + "http.header.%s" % custom_header, env[wsgi_header] + ) + + if "PATH_INFO" in env: + self.span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + self.span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + self.span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + self.span.set_attribute("http.host", env["HTTP_HOST"]) return self.app(environ, new_start_response) diff --git a/src/instana/middleware.py b/src/instana/middleware.py index f731931d..ef9be47d 100644 --- a/src/instana/middleware.py +++ b/src/instana/middleware.py @@ -2,5 +2,5 @@ # (c) Copyright Instana Inc. 2017 -from .instrumentation.wsgi import InstanaWSGIMiddleware -from .instrumentation.asgi import InstanaASGIMiddleware \ No newline at end of file +from instana.instrumentation.asgi import InstanaASGIMiddleware # noqa: F401 +from instana.instrumentation.wsgi import InstanaWSGIMiddleware # noqa: F401 diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index 18e379b7..cc49ec7e 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -2,17 +2,22 @@ # (c) Copyright Instana Inc. 2020 -import sys import os +from typing import Any, Optional, TypeVar, Dict, List, Tuple + from instana.log import logger from instana.util.ids import header_to_id, header_to_long_id from instana.span_context import SpanContext from instana.w3c_trace_context.traceparent import Traceparent from instana.w3c_trace_context.tracestate import Tracestate +from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, +) -# The carrier can be a dict or a list. +# The carrier, typed here as CarrierT, can be a dict, a list, or a tuple. # Using the trace header as an example, it can be in the following forms # for extraction: # X-Instana-T @@ -23,55 +28,58 @@ # # For injection, we only support the standard format: # X-Instana-T +CarrierT = TypeVar("CarrierT", Dict, List, Tuple) class BasePropagator(object): - HEADER_KEY_T = 'X-INSTANA-T' - HEADER_KEY_S = 'X-INSTANA-S' - HEADER_KEY_L = 'X-INSTANA-L' - HEADER_KEY_SYNTHETIC = 'X-INSTANA-SYNTHETIC' + HEADER_KEY_T = "X-INSTANA-T" + HEADER_KEY_S = "X-INSTANA-S" + HEADER_KEY_L = "X-INSTANA-L" + HEADER_KEY_SYNTHETIC = "X-INSTANA-SYNTHETIC" HEADER_KEY_TRACEPARENT = "traceparent" HEADER_KEY_TRACESTATE = "tracestate" - LC_HEADER_KEY_T = 'x-instana-t' - LC_HEADER_KEY_S = 'x-instana-s' - LC_HEADER_KEY_L = 'x-instana-l' - LC_HEADER_KEY_SYNTHETIC = 'x-instana-synthetic' + LC_HEADER_KEY_T = "x-instana-t" + LC_HEADER_KEY_S = "x-instana-s" + LC_HEADER_KEY_L = "x-instana-l" + LC_HEADER_KEY_SYNTHETIC = "x-instana-synthetic" - ALT_LC_HEADER_KEY_T = 'http_x_instana_t' - ALT_LC_HEADER_KEY_S = 'http_x_instana_s' - ALT_LC_HEADER_KEY_L = 'http_x_instana_l' - ALT_LC_HEADER_KEY_SYNTHETIC = 'http_x_instana_synthetic' + ALT_LC_HEADER_KEY_T = "http_x_instana_t" + ALT_LC_HEADER_KEY_S = "http_x_instana_s" + ALT_LC_HEADER_KEY_L = "http_x_instana_l" + ALT_LC_HEADER_KEY_SYNTHETIC = "http_x_instana_synthetic" ALT_HEADER_KEY_TRACEPARENT = "http_traceparent" ALT_HEADER_KEY_TRACESTATE = "http_tracestate" # ByteArray variations - B_HEADER_KEY_T = b'x-instana-t' - B_HEADER_KEY_S = b'x-instana-s' - B_HEADER_KEY_L = b'x-instana-l' - B_HEADER_KEY_SYNTHETIC = b'x-instana-synthetic' - B_HEADER_SERVER_TIMING = b'server-timing' - B_HEADER_KEY_TRACEPARENT = b'traceparent' - B_HEADER_KEY_TRACESTATE = b'tracestate' - - B_ALT_LC_HEADER_KEY_T = b'http_x_instana_t' - B_ALT_LC_HEADER_KEY_S = b'http_x_instana_s' - B_ALT_LC_HEADER_KEY_L = b'http_x_instana_l' - B_ALT_LC_HEADER_KEY_SYNTHETIC = b'http_x_instana_synthetic' - B_ALT_HEADER_KEY_TRACEPARENT = b'http_traceparent' - B_ALT_HEADER_KEY_TRACESTATE = b'http_tracestate' + B_HEADER_KEY_T = b"x-instana-t" + B_HEADER_KEY_S = b"x-instana-s" + B_HEADER_KEY_L = b"x-instana-l" + B_HEADER_KEY_SYNTHETIC = b"x-instana-synthetic" + B_HEADER_SERVER_TIMING = b"server-timing" + B_HEADER_KEY_TRACEPARENT = b"traceparent" + B_HEADER_KEY_TRACESTATE = b"tracestate" + + B_ALT_LC_HEADER_KEY_T = b"http_x_instana_t" + B_ALT_LC_HEADER_KEY_S = b"http_x_instana_s" + B_ALT_LC_HEADER_KEY_L = b"http_x_instana_l" + B_ALT_LC_HEADER_KEY_SYNTHETIC = b"http_x_instana_synthetic" + B_ALT_HEADER_KEY_TRACEPARENT = b"http_traceparent" + B_ALT_HEADER_KEY_TRACESTATE = b"http_tracestate" def __init__(self): self._tp = Traceparent() self._ts = Tracestate() @staticmethod - def extract_headers_dict(carrier): + def extract_headers_dict(carrier: CarrierT) -> Optional[Dict]: """ - This method converts the incoming carrier into a dict - :param carrier: - :return: dc dictionary + This method converts the incoming carrier into a dict. + + :param carrier: CarrierT + :return: Dict | None """ + dc = None try: if isinstance(carrier, dict): dc = carrier @@ -80,17 +88,19 @@ def extract_headers_dict(carrier): else: dc = dict(carrier) except Exception: - logger.debug("extract: Couldn't convert %s", carrier) - dc = None + logger.debug( + f"base_propagator extract_headers_dict: Couldn't convert - {carrier}" + ) return dc @staticmethod - def _get_ctx_level(level): + def _get_ctx_level(level: str) -> int: """ - Extract the level value and return it, as it may include correlation values - :param level: - :return: + Extract the level value and return it, as it may include correlation values. + + :param level: str + :return: int """ try: ctx_level = int(level.split(",")[0]) if level else 1 @@ -99,24 +109,32 @@ def _get_ctx_level(level): return ctx_level @staticmethod - def _set_correlation_properties(level, ctx): + def _get_correlation_properties(level: str): """ - Set the correlation values if they are present - :param level: - :param ctx: - :return: + Get the correlation values if they are present. + + :param level: str + :return: Tuple[Any, Any] - correlation_type, correlation_id """ + correlation_type, correlation_id = [None] * 2 try: - ctx.correlation_type = level.split(",")[1].split("correlationType=")[1].split(";")[0] + correlation_type = ( + level.split(",")[1].split("correlationType=")[1].split(";")[0] + ) if "correlationId" in level: - ctx.correlation_id = level.split(",")[1].split("correlationId=")[1].split(";")[0] + correlation_id = ( + level.split(",")[1].split("correlationId=")[1].split(";")[0] + ) except Exception: logger.debug("extract instana correlation type/id error:", exc_info=True) - def _get_participating_trace_context(self, span_context): + return correlation_type, correlation_id + + def _get_participating_trace_context(self, span_context: SpanContext): """ - This method is called for getting the updated traceparent and tracestate values - :param span_context: + This method is called for getting the updated traceparent and tracestate values. + + :param span_context: SpanContext :return: traceparent, tracestate """ if span_context.long_trace_id and not span_context.trace_parent: @@ -125,7 +143,9 @@ def _get_participating_trace_context(self, span_context): tp_trace_id = span_context.trace_id traceparent = span_context.traceparent tracestate = span_context.tracestate - traceparent = self._tp.update_traceparent(traceparent, tp_trace_id, span_context.span_id, span_context.level) + traceparent = self._tp.update_traceparent( + traceparent, tp_trace_id, span_context.span_id, span_context.level + ) # In suppression mode do not update the tracestate and # do not add the 'in=' key-value pair to the incoming tracestate @@ -133,110 +153,177 @@ def _get_participating_trace_context(self, span_context): if span_context.suppression: return traceparent, tracestate - tracestate = self._ts.update_tracestate(tracestate, span_context.trace_id, span_context.span_id) + tracestate = self._ts.update_tracestate( + tracestate, span_context.trace_id, span_context.span_id + ) return traceparent, tracestate - def __determine_span_context(self, trace_id, span_id, level, synthetic, traceparent, tracestate, - disable_w3c_trace_context): + def __determine_span_context( + self, + trace_id: int, + span_id: int, + level: str, + synthetic: bool, + traceparent, + tracestate, + disable_w3c_trace_context: bool, + ) -> SpanContext: """ This method determines the span context depending on a set of conditions being met Detailed description of the conditions can be found in the instana internal technical-documentation, - under section http-processing-for-instana-tracers - :param trace_id: instana trace id - :param span_id: instana span id - :param level: instana level - :param synthetic: instana synthetic + under section http-processing-for-instana-tracers. + + :param trace_id: int - instana trace id + :param span_id: int - instana span id + :param level: str - instana level + :param synthetic: bool - instana synthetic :param traceparent: :param tracestate: - :param disable_w3c_trace_context: flag used to enable w3c trace context only on HTTP requests - :return: ctx + :param disable_w3c_trace_context: bool - flag used to enable w3c trace context only on HTTP requests + :return: SpanContext """ correlation = False - disable_traceparent = os.environ.get("INSTANA_DISABLE_W3C_TRACE_CORRELATION", "") + disable_traceparent = os.environ.get( + "INSTANA_DISABLE_W3C_TRACE_CORRELATION", "" + ) instana_ancestor = None - ctx = SpanContext() + if level and "correlationType" in level: trace_id, span_id = [None] * 2 correlation = True - ctx_level = self._get_ctx_level(level) - if ctx_level == 0 or level == '0': - trace_id = ctx.trace_id = None - span_id = ctx.span_id = None - ctx.correlation_type = None - ctx.correlation_id = None - - if trace_id and span_id: - ctx.trace_id = trace_id[-16:] # only the last 16 chars - ctx.span_id = span_id[-16:] # only the last 16 chars - ctx.synthetic = synthetic is not None + ( + ctx_level, + ctx_synthetic, + ctx_trace_parent, + ctx_instana_ancestor, + ctx_long_trace_id, + ctx_correlation_type, + ctx_correlation_id, + ctx_traceparent, + ctx_tracestate, + ) = [None] * 9 - if len(trace_id) > 16: - ctx.long_trace_id = trace_id - - elif not disable_w3c_trace_context and traceparent and trace_id is None and span_id is None: - _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields(traceparent) + ctx_level = self._get_ctx_level(level) + ctx_trace_id = trace_id + ctx_span_id = span_id + + if ( + trace_id + and span_id + and trace_id != INVALID_TRACE_ID + and span_id != INVALID_SPAN_ID + ): + # ctx.trace_id = trace_id[-16:] # only the last 16 chars + # ctx.span_id = span_id[-16:] # only the last 16 chars + ctx_synthetic = synthetic + + # if len(trace_id) > 16: + ctx_long_trace_id = trace_id + + elif ( + not disable_w3c_trace_context + and traceparent + and not trace_id + and not span_id + ): + _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields( + traceparent + ) if tracestate and "in=" in tracestate: instana_ancestor = self._ts.get_instana_ancestor(tracestate) if disable_traceparent == "": - ctx.trace_id = tp_trace_id[-16:] - ctx.span_id = tp_parent_id - ctx.synthetic = synthetic is not None - ctx.trace_parent = True - ctx.instana_ancestor = instana_ancestor - ctx.long_trace_id = tp_trace_id + ctx_trace_id = int(tp_trace_id[-16:]) + ctx_span_id = int(tp_parent_id) + ctx_synthetic = synthetic + ctx_trace_parent = True + ctx_instana_ancestor = instana_ancestor + ctx_long_trace_id = tp_trace_id else: if instana_ancestor: - ctx.trace_id = instana_ancestor.t - ctx.span_id = instana_ancestor.p - ctx.synthetic = synthetic is not None + ctx_trace_id = int(instana_ancestor.t) + ctx_span_id = int(instana_ancestor.p) + ctx_synthetic = synthetic elif synthetic: - ctx.synthetic = synthetic + ctx_synthetic = synthetic if correlation: - self._set_correlation_properties(level, ctx) + ctx_correlation_type, ctx_correlation_id = self._get_correlation_properties( + level + ) if traceparent: - ctx.traceparent = traceparent - ctx.tracestate = tracestate - - ctx.level = ctx_level - - return ctx - - def extract_instana_headers(self, dc): + ctx_traceparent = traceparent + ctx_tracestate = tracestate + + return SpanContext( + trace_id=ctx_trace_id, + span_id=ctx_span_id, + is_remote=False, + level=ctx_level, + synthetic=ctx_synthetic, + trace_parent=ctx_trace_parent, + instana_ancestor=ctx_instana_ancestor, + long_trace_id=ctx_long_trace_id, + correlation_type=ctx_correlation_type, + correlation_id=ctx_correlation_id, + traceparent=ctx_traceparent, + tracestate=ctx_tracestate, + ) + + def extract_instana_headers( + self, dc: Dict[str, Any] + ) -> Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]]: """ - Search carrier for the *HEADER* keys and return the tracing key-values + Search carrier for the *HEADER* keys and return the tracing key-values. - :param dc: The dict or list potentially containing context - :return: trace_id, span_id, level, synthetic + :param dc: Dict - The dict potentially containing context + :return: Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]] - trace_id, span_id, level, synthetic """ trace_id, span_id, level, synthetic = [None] * 4 # Headers can exist in the standard X-Instana-T/S format or the alternate HTTP_X_INSTANA_T/S style try: - trace_id = dc.get(self.LC_HEADER_KEY_T) or dc.get(self.ALT_LC_HEADER_KEY_T) or dc.get( - self.B_HEADER_KEY_T) or dc.get(self.B_ALT_LC_HEADER_KEY_T) + trace_id = ( + dc.get(self.LC_HEADER_KEY_T) + or dc.get(self.ALT_LC_HEADER_KEY_T) + or dc.get(self.B_HEADER_KEY_T) + or dc.get(self.B_ALT_LC_HEADER_KEY_T) + ) if trace_id: - trace_id = header_to_long_id(trace_id) - - span_id = dc.get(self.LC_HEADER_KEY_S) or dc.get(self.ALT_LC_HEADER_KEY_S) or dc.get( - self.B_HEADER_KEY_S) or dc.get(self.B_ALT_LC_HEADER_KEY_S) + # trace_id = header_to_long_id(trace_id) + trace_id = int(trace_id) + + span_id = ( + dc.get(self.LC_HEADER_KEY_S) + or dc.get(self.ALT_LC_HEADER_KEY_S) + or dc.get(self.B_HEADER_KEY_S) + or dc.get(self.B_ALT_LC_HEADER_KEY_S) + ) if span_id: - span_id = header_to_id(span_id) - - level = dc.get(self.LC_HEADER_KEY_L) or dc.get(self.ALT_LC_HEADER_KEY_L) or dc.get( - self.B_HEADER_KEY_L) or dc.get(self.B_ALT_LC_HEADER_KEY_L) + # span_id = header_to_id(span_id) + span_id = int(span_id) + + level = ( + dc.get(self.LC_HEADER_KEY_L) + or dc.get(self.ALT_LC_HEADER_KEY_L) + or dc.get(self.B_HEADER_KEY_L) + or dc.get(self.B_ALT_LC_HEADER_KEY_L) + ) if level and isinstance(level, bytes): level = level.decode("utf-8") - synthetic = dc.get(self.LC_HEADER_KEY_SYNTHETIC) or dc.get(self.ALT_LC_HEADER_KEY_SYNTHETIC) or dc.get( - self.B_HEADER_KEY_SYNTHETIC) or dc.get(self.B_ALT_LC_HEADER_KEY_SYNTHETIC) + synthetic = ( + dc.get(self.LC_HEADER_KEY_SYNTHETIC) + or dc.get(self.ALT_LC_HEADER_KEY_SYNTHETIC) + or dc.get(self.B_HEADER_KEY_SYNTHETIC) + or dc.get(self.B_ALT_LC_HEADER_KEY_SYNTHETIC) + ) if synthetic: - synthetic = synthetic in ['1', b'1'] + synthetic = synthetic in ["1", b"1"] except Exception: logger.debug("extract error:", exc_info=True) @@ -253,13 +340,21 @@ def __extract_w3c_trace_context_headers(self, dc): traceparent, tracestate = [None] * 2 try: - traceparent = dc.get(self.HEADER_KEY_TRACEPARENT) or dc.get(self.ALT_HEADER_KEY_TRACEPARENT) or dc.get( - self.B_HEADER_KEY_TRACEPARENT) or dc.get(self.B_ALT_HEADER_KEY_TRACEPARENT) + traceparent = ( + dc.get(self.HEADER_KEY_TRACEPARENT) + or dc.get(self.ALT_HEADER_KEY_TRACEPARENT) + or dc.get(self.B_HEADER_KEY_TRACEPARENT) + or dc.get(self.B_ALT_HEADER_KEY_TRACEPARENT) + ) if traceparent and isinstance(traceparent, bytes): traceparent = traceparent.decode("utf-8") - tracestate = dc.get(self.HEADER_KEY_TRACESTATE) or dc.get(self.ALT_HEADER_KEY_TRACESTATE) or dc.get( - self.B_HEADER_KEY_TRACESTATE) or dc.get(self.B_ALT_HEADER_KEY_TRACESTATE) + tracestate = ( + dc.get(self.HEADER_KEY_TRACESTATE) + or dc.get(self.ALT_HEADER_KEY_TRACESTATE) + or dc.get(self.B_HEADER_KEY_TRACESTATE) + or dc.get(self.B_ALT_HEADER_KEY_TRACESTATE) + ) if tracestate and isinstance(tracestate, bytes): tracestate = tracestate.decode("utf-8") @@ -268,10 +363,14 @@ def __extract_w3c_trace_context_headers(self, dc): return traceparent, tracestate - def extract(self, carrier, disable_w3c_trace_context=False): + def extract( + self, carrier: CarrierT, disable_w3c_trace_context: bool = False + ) -> Optional[SpanContext]: """ - This method overrides one of the Baseclasses as with the introduction of W3C trace context for the HTTP - requests more extracting steps and logic was required + This method overrides one of the Base classes as with the introduction + of W3C trace context for the HTTP requests more extracting steps and + logic was required. + :param disable_w3c_trace_context: :param carrier: :return: the context or None @@ -283,16 +382,32 @@ def extract(self, carrier, disable_w3c_trace_context=False): return None headers = {k.lower(): v for k, v in headers.items()} - trace_id, span_id, level, synthetic = self.extract_instana_headers(dc=headers) + trace_id, span_id, level, synthetic = self.extract_instana_headers( + dc=headers + ) if not disable_w3c_trace_context: - traceparent, tracestate = self.__extract_w3c_trace_context_headers(dc=headers) + traceparent, tracestate = self.__extract_w3c_trace_context_headers( + dc=headers + ) if traceparent: traceparent = self._tp.validate(traceparent) - ctx = self.__determine_span_context(trace_id, span_id, level, synthetic, traceparent, tracestate, - disable_w3c_trace_context) + if trace_id is None: + trace_id = INVALID_TRACE_ID + if span_id is None: + span_id = INVALID_SPAN_ID + + span_context = self.__determine_span_context( + trace_id, + span_id, + level, + synthetic, + traceparent, + tracestate, + disable_w3c_trace_context, + ) + return span_context - return ctx except Exception: - logger.debug("extract error:", exc_info=True) + logger.debug("base_propagator extract error:", exc_info=True) diff --git a/src/instana/propagators/binary_propagator.py b/src/instana/propagators/binary_propagator.py index 92a294d9..89c9002f 100644 --- a/src/instana/propagators/binary_propagator.py +++ b/src/instana/propagators/binary_propagator.py @@ -25,10 +25,10 @@ def __init__(self): def inject(self, span_context, carrier, disable_w3c_trace_context=True): try: - trace_id = str.encode(span_context.trace_id) - span_id = str.encode(span_context.span_id) - level = str.encode(str(span_context.level)) - server_timing = str.encode("intid;desc=%s" % span_context.trace_id) + trace_id = str(span_context.trace_id).encode() + span_id = str(span_context.span_id).encode() + level = str(span_context.level).encode() + server_timing = f"intid;desc={span_context.trace_id}".encode() if disable_w3c_trace_context: traceparent, tracestate = [None] * 2 diff --git a/src/instana/propagators/exceptions.py b/src/instana/propagators/exceptions.py new file mode 100644 index 00000000..7e613c5f --- /dev/null +++ b/src/instana/propagators/exceptions.py @@ -0,0 +1,11 @@ +# (c) Copyright IBM Corp. 2024 + + +class UnsupportedFormatException(Exception): + """UnsupportedFormatException should be used when the provided format + value is unknown or disallowed by the :class:`InstanaTracer`. + + See :meth:`InstanaTracer.inject()` and :meth:`InstanaTracer.extract()`. + """ + + pass diff --git a/src/instana/propagators/format.py b/src/instana/propagators/format.py new file mode 100644 index 00000000..9049c4e1 --- /dev/null +++ b/src/instana/propagators/format.py @@ -0,0 +1,52 @@ +# (c) Copyright IBM Corp. 2024 + + +class Format(object): + """A namespace for builtin carrier formats. + + These static constants are intended for use in the :meth:`Tracer.inject()` + and :meth:`Tracer.extract()` methods. E.g.:: + + tracer.inject(span.context, Format.BINARY, binary_carrier) + + """ + + BINARY = "binary" + """ + The BINARY format represents SpanContexts in an opaque bytearray carrier. + + For both :meth:`Tracer.inject()` and :meth:`Tracer.extract()` the carrier + should be a bytearray instance. :meth:`Tracer.inject()` must append to the + bytearray carrier (rather than replace its contents). + """ + + TEXT_MAP = "text_map" + """ + The TEXT_MAP format represents :class:`SpanContext`\\ s in a python + ``dict`` mapping from strings to strings. + + Both the keys and the values have unrestricted character sets (unlike the + HTTP_HEADERS format). + + NOTE: The TEXT_MAP carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. + """ + + HTTP_HEADERS = "http_headers" + """ + The HTTP_HEADERS format represents :class:`SpanContext`\\ s in a python + ``dict`` mapping from character-restricted strings to strings. + + Keys and values in the HTTP_HEADERS carrier must be suitable for use as + HTTP headers (without modification or further escaping). That is, the + keys have a greatly restricted character set, casing for the keys may not + be preserved by various intermediaries, and the values should be + URL-escaped. + + NOTE: The HTTP_HEADERS carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. + """ diff --git a/src/instana/propagators/http_propagator.py b/src/instana/propagators/http_propagator.py index b0326001..483f2765 100644 --- a/src/instana/propagators/http_propagator.py +++ b/src/instana/propagators/http_propagator.py @@ -53,8 +53,8 @@ def inject_key_value(carrier, key, value): if span_context.suppression: return - inject_key_value(carrier, self.HEADER_KEY_T, trace_id) - inject_key_value(carrier, self.HEADER_KEY_S, span_id) + inject_key_value(carrier, self.HEADER_KEY_T, str(trace_id)) + inject_key_value(carrier, self.HEADER_KEY_S, str(span_id)) except Exception: logger.debug("inject error:", exc_info=True) diff --git a/src/instana/recorder.py b/src/instana/recorder.py index b5875805..9ec882f7 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -5,51 +5,42 @@ import os import queue -import sys +from typing import TYPE_CHECKING, List, Optional, Type -from basictracer import Sampler - -from .span import RegisteredSpan, SDKSpan +from instana.span.kind import REGISTERED_SPANS +from instana.span.readable_span import ReadableSpan +from instana.span.registered_span import RegisteredSpan +from instana.span.sdk_span import SDKSpan +if TYPE_CHECKING: + from instana.agent.base import BaseAgent class StanRecorder(object): - THREAD_NAME = "Instana Span Reporting" - - REGISTERED_SPANS = ("aiohttp-client", "aiohttp-server", "aws.lambda.entry", - "boto3", "cassandra", "celery-client", "celery-worker", - "couchbase", "django", "gcs", "gcps-producer", - "gcps-consumer", "log", "memcache", "mongo", "mysql", - "postgres", "pymongo", "rabbitmq", "redis","render", - "rpc-client", "rpc-server", "sqlalchemy", "tornado-client", - "tornado-server", "urllib3", "wsgi", "asgi") + THREAD_NAME = "InstanaSpan Recorder" # Recorder thread for collection/reporting of spans thread = None - def __init__(self, agent = None): + def __init__(self, agent: Optional[Type["BaseAgent"]] = None) -> None: if agent is None: # Late import to avoid circular import # pylint: disable=import-outside-toplevel - from .singletons import get_agent + from instana.singletons import get_agent + self.agent = get_agent() else: self.agent = agent - def queue_size(self): - """ Return the size of the queue; how may spans are queued, """ + def queue_size(self) -> int: + """Return the size of the queue; how may spans are queued,""" return self.agent.collector.span_queue.qsize() - def queued_spans(self): - """ Get all of the spans in the queue """ + def queued_spans(self) -> List[ReadableSpan]: + """Get all of the spans in the queue.""" span = None spans = [] - import time - from .singletons import env_is_test - if env_is_test is True: - time.sleep(1) - if self.agent.collector.span_queue.empty() is True: return spans @@ -63,13 +54,13 @@ def queued_spans(self): return spans def clear_spans(self): - """ Clear the queue of spans """ - if self.agent.collector.span_queue.empty() == False: + """Clear the queue of spans.""" + if not self.agent.collector.span_queue.empty(): self.queued_spans() - def record_span(self, span): + def record_span(self, span: ReadableSpan) -> None: """ - Convert the passed BasicSpan into and add it to the span queue + Convert the passed span into JSON and add it to the span queue. """ if span.context.suppression: return @@ -80,7 +71,7 @@ def record_span(self, span): if "INSTANA_SERVICE_NAME" in os.environ: service_name = self.agent.options.service_name - if span.operation_name in self.REGISTERED_SPANS: + if span.name in REGISTERED_SPANS: json_span = RegisteredSpan(span, source, service_name) else: service_name = self.agent.options.service_name @@ -88,9 +79,3 @@ def record_span(self, span): # logger.debug("Recorded span: %s", json_span) self.agent.collector.span_queue.put(json_span) - - -class InstanaSampler(Sampler): - def sampled(self, _): - # We never sample - return False diff --git a/src/instana/sampling.py b/src/instana/sampling.py new file mode 100644 index 00000000..96d46d8e --- /dev/null +++ b/src/instana/sampling.py @@ -0,0 +1,43 @@ +# (c) Copyright IBM Corp. 2024 + +import abc +import enum + + +class SamplingPolicy(enum.Enum): + # IsRecording() == False + # Span will not be recorded and all events and attributes will be dropped. + # https://opentelemetry.io/docs/specs/otel/trace/api/#isrecording + DROP = 0 + # IsRecording() == True, but Sampled flag MUST NOT be set. + RECORD_ONLY = 1 + # IsRecording() == True AND Sampled flag MUST be set. + RECORD_AND_SAMPLE = 2 + + +class Sampler(abc.ABC): + """Samplers choose whether the span is recorded or dropped. + + A variety of sampling algorithms are available, and choosing which sampler + to use and how to configure it is one of the most confusing parts of + setting up a tracing system. + """ + + @abc.abstractmethod + def sampled(self) -> bool: + """ + Returns if a span was dropped (False) or recorded (True). + + Calling a span “sampled” can mean it was “sampled out” (dropped) + or “sampled in” (recorded). + """ + pass # pragma: no cover + + +class InstanaSampler(Sampler): + def __init__(self) -> None: + # Instana never samples. + self._sampled: SamplingPolicy = SamplingPolicy.DROP + + def sampled(self) -> bool: + return False if self._sampled == SamplingPolicy.DROP else True diff --git a/src/instana/singletons.py b/src/instana/singletons.py index c93416ca..9cc328ac 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -3,21 +3,18 @@ import os -import opentracing +from opentelemetry import trace -from .autoprofile.profiler import Profiler -from .log import logger -from .tracer import InstanaTracer +from instana.autoprofile.profiler import Profiler +from instana.tracer import InstanaTracerProvider agent = None tracer = None -async_tracer = None profiler = None span_recorder = None # Detect the environment where we are running ahead of time aws_env = os.environ.get("AWS_EXECUTION_ENV", "") -env_is_test = "INSTANA_TEST" in os.environ env_is_aws_fargate = aws_env == "AWS_ECS_FARGATE" env_is_aws_eks_fargate = ( os.environ.get("INSTANA_TRACER_ENVIRONMENT") == "AWS_EKS_FARGATE" @@ -31,15 +28,7 @@ (k_service, k_configuration, k_revision, instana_endpoint_url) ) -if env_is_test: - from .agent.test import TestAgent - from .recorder import StanRecorder - - agent = TestAgent() - span_recorder = StanRecorder(agent) - profiler = Profiler(agent) - -elif env_is_aws_lambda: +if env_is_aws_lambda: from .agent.aws_lambda import AWSLambdaAgent from .recorder import StanRecorder @@ -96,34 +85,15 @@ def set_agent(new_agent): agent = new_agent -# The global OpenTracing compatible tracer used internally by +# The global OpenTelemetry compatible tracer used internally by # this package. -tracer = InstanaTracer(recorder=span_recorder) - -try: - from opentracing.scope_managers.contextvars import ContextVarsScopeManager - - async_tracer = InstanaTracer( - scope_manager=ContextVarsScopeManager(), recorder=span_recorder - ) -except Exception: - logger.debug("Error setting up async_tracer:", exc_info=True) - -# Mock the tornado tracer until tornado is detected and instrumented first -tornado_tracer = tracer - - -def setup_tornado_tracer(): - global tornado_tracer - from opentracing.scope_managers.tornado import TornadoScopeManager - - tornado_tracer = InstanaTracer( - scope_manager=TornadoScopeManager(), recorder=span_recorder - ) +provider = InstanaTracerProvider(span_processor=span_recorder, exporter=agent) +# Sets the global default tracer provider +trace.set_tracer_provider(provider) -# Set ourselves as the tracer. -opentracing.tracer = tracer +# Creates a tracer from the global tracer provider +tracer = trace.get_tracer("instana.tracer") def get_tracer(): diff --git a/src/instana/span.py b/src/instana/span.py index c9896453..ca268748 100644 --- a/src/instana/span.py +++ b/src/instana/span.py @@ -4,7 +4,7 @@ """ This module contains the classes that represents spans. -InstanaSpan - the OpenTracing based span used during tracing +InstanaSpan - the OpenTelemetry based span used during tracing When an InstanaSpan is finished, it is converted into either an SDKSpan or RegisteredSpan depending on type. @@ -14,102 +14,277 @@ - RegisteredSpan: Class that represents a Registered type span """ import six +from typing import Dict, Optional, Union, Sequence, Tuple +from threading import Lock +from time import time_ns -from basictracer.span import BasicSpan -import opentracing.ext.tags as ot_tags +from opentelemetry.trace import Span # , SpanContext +from opentelemetry.util import types +from opentelemetry.trace.status import Status, StatusCode +from .span_context import SpanContext from .log import logger from .util import DictionaryOfStan -class InstanaSpan(BasicSpan): +class Event: + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + self._name = name + self._attributes = attributes + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class InstanaSpan(Span): stack = None synthetic = False - def mark_as_errored(self, tags=None): + def __init__( + self, + name: str, + context: SpanContext, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + self._name = name + self._context = context + self._lock = Lock() + self._start_time = start_time or time_ns() + self._end_time = end_time + self._duration = 0 + self._attributes = attributes + self._events = events + self._parent_id = parent_id + self._status = status + + if context.synthetic: + self.synthetic = True + + + @property + def name(self) -> str: + return self._name + + def get_span_context(self) -> SpanContext: + return self._context + + @property + def context(self) -> SpanContext: + return self._context + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def duration(self) -> int: + return self._duration + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: + if not self._attributes: + self._attributes = {} + + with self._lock: + for key, value in attributes.items(): + self._attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) + + @property + def events(self) -> Sequence[Event]: + return self._events + + @property + def status(self) -> Status: + return self._status + + @property + def parent_id(self) -> int: + return self._parent_id + + def update_name(self, name: str) -> None: + with self._lock: + self._name = name + + def is_recording(self) -> bool: + return self._end_time is None + + def set_status( + self, + status: Union[Status, StatusCode], + description: Optional[str] = None, + ) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + + event = Event( + name=name, + attributes=attributes, + timestamp=timestamp, + ) + + self._events.append(event) + + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + escaped: bool = False, + ) -> None: + """ + Records an exception as a span event. This will record pertinent info from the exception and + assure that this span is marked as errored. + """ + try: + message = "" + self.mark_as_errored() + if hasattr(exception, "__str__") and len(str(exception)) > 0: + message = str(exception) + elif hasattr(exception, "message") and exception.message is not None: + message = exception.message + else: + message = repr(exception) + + if self.name in ["rpc-server", "rpc-client"]: + self.set_attribute("rpc.error", message) + elif self.name == "mysql": + self.set_attribute("mysql.error", message) + elif self.name == "postgres": + self.set_attribute("pg.error", message) + elif self.name in RegisteredSpan.HTTP_SPANS: + self.set_attribute("http.error", message) + elif self.name in ["celery-client", "celery-worker"]: + self.set_attribute("error", message) + elif self.name == "sqlalchemy": + self.set_attribute("sqlalchemy.err", message) + elif self.name == "aws.lambda.entry": + self.set_attribute("lambda.error", message) + else: + _attributes = {"message": message} + if attributes: + _attributes.update(attributes) + self.add_event( + name="exception", attributes=_attributes, timestamp=timestamp + ) + except Exception: + logger.debug("span.record_exception", exc_info=True) + raise + + def end(self, end_time: Optional[int] = None) -> None: + with self._lock: + self._end_time = end_time if end_time is not None else time_ns() + self._duration = self._end_time - self._start_time + + def mark_as_errored(self, attributes: types.Attributes = None) -> None: """ Mark this span as errored. - @param tags: optional tags to add to the span + @param attributes: optional attributes to add to the span """ try: - ec = self.tags.get('ec', 0) - self.set_tag('ec', ec + 1) + ec = self.attributes.get("ec", 0) + self.set_attribute("ec", ec + 1) - if tags is not None and isinstance(tags, dict): - for key in tags: - self.set_tag(key, tags[key]) + if attributes is not None and isinstance(attributes, dict): + for key in attributes: + self.set_attribute(key, attributes[key]) except Exception: - logger.debug('span.mark_as_errored', exc_info=True) + logger.debug("span.mark_as_errored", exc_info=True) - def assure_errored(self): + def assure_errored(self) -> None: """ Make sure that this span is marked as errored. @return: None """ try: - ec = self.tags.get('ec', None) + ec = self.attributes.get("ec", None) if ec is None or ec == 0: - self.set_tag('ec', 1) + self.set_attribute("ec", 1) except Exception: - logger.debug('span.assure_errored', exc_info=True) - - def log_exception(self, exc): - """ - Log an exception onto this span. This will log pertinent info from the exception and - assure that this span is marked as errored. - - @param e: the exception to log - """ - try: - message = "" - self.mark_as_errored() - if hasattr(exc, '__str__') and len(str(exc)) > 0: - message = str(exc) - elif hasattr(exc, 'message') and exc.message is not None: - message = exc.message - else: - message = repr(exc) - - if self.operation_name in ['rpc-server', 'rpc-client']: - self.set_tag('rpc.error', message) - elif self.operation_name == "mysql": - self.set_tag('mysql.error', message) - elif self.operation_name == "postgres": - self.set_tag('pg.error', message) - elif self.operation_name in RegisteredSpan.HTTP_SPANS: - self.set_tag('http.error', message) - elif self.operation_name in ["celery-client", "celery-worker"]: - self.set_tag('error', message) - elif self.operation_name == "sqlalchemy": - self.set_tag('sqlalchemy.err', message) - elif self.operation_name == "aws.lambda.entry": - self.set_tag('lambda.error', message) - else: - self.log_kv({'message': message}) - except Exception: - logger.debug("span.log_exception", exc_info=True) - raise + logger.debug("span.assure_errored", exc_info=True) class BaseSpan(object): sy = None - def __str__(self): + def __str__(self) -> str: return "BaseSpan(%s)" % self.__dict__.__str__() - def __repr__(self): + def __repr__(self) -> str: return self.__dict__.__str__() - def __init__(self, span, source, service_name, **kwargs): + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name self.t = span.context.trace_id self.p = span.parent_id + # self.p = span.context.span_id if span.context.is_remote else None self.s = span.context.span_id - self.ts = int(round(span.start_time * 1000)) - self.d = int(round(span.duration * 1000)) + self.l = span.context.level + self.ts = round(span.start_time / 10**6) + self.d = round(span.duration / 10**6) self.f = source - self.ec = span.tags.pop('ec', None) + self.ec = span.attributes.pop("ec", None) self.data = DictionaryOfStan() self.stack = span.stack @@ -118,7 +293,7 @@ def __init__(self, span, source, service_name, **kwargs): self.__dict__.update(kwargs) - def _populate_extra_span_attributes(self, span): + def _populate_extra_span_attributes(self, span) -> None: if span.context.trace_parent: self.tp = span.context.trace_parent if span.context.instana_ancestor: @@ -130,60 +305,70 @@ def _populate_extra_span_attributes(self, span): if span.context.correlation_id: self.crid = span.context.correlation_id - def _validate_tags(self, tags): + def _validate_attributes(self, attributes): """ - This method will loop through a set of tags to validate each key and value. + This method will loop through a set of attributes to validate each key and value. - :param tags: dict of tags - :return: dict - a filtered set of tags + :param attributes: dict of attributes + :return: dict - a filtered set of attributes """ - filtered_tags = DictionaryOfStan() - for key in tags.keys(): - validated_key, validated_value = self._validate_tag(key, tags[key]) + filtered_attributes = DictionaryOfStan() + for key in attributes.keys(): + validated_key, validated_value = self._validate_attribute( + key, attributes[key] + ) if validated_key is not None and validated_value is not None: - filtered_tags[validated_key] = validated_value - return filtered_tags + filtered_attributes[validated_key] = validated_value + return filtered_attributes - def _validate_tag(self, key, value): + def _validate_attribute(self, key, value): """ - This method will assure that and are valid to set as a tag. + This method will assure that and are valid to set as a attribute. If fails the check, an attempt will be made to convert it into something useful. - On check failure, this method will return None values indicating that the tag is + On check failure, this method will return None values indicating that the attribute is not valid and could not be converted into something useful - :param key: The tag key - :param value: The tag value + :param key: The attribute key + :param value: The attribute value :return: Tuple (key, value) """ validated_key = None validated_value = None try: - # Tag keys must be some type of text or string type + # Attribute keys must be some type of text or string type if isinstance(key, (six.text_type, six.string_types)): validated_key = key[0:1024] # Max key length of 1024 characters - if isinstance(value, (bool, float, int, list, dict, six.text_type, six.string_types)): + if isinstance( + value, + (bool, float, int, list, dict, six.text_type, six.string_types), + ): validated_value = value else: - validated_value = self._convert_tag_value(value) + validated_value = self._convert_attribute_value(value) else: - logger.debug("(non-fatal) tag names must be strings. tag discarded for %s", type(key)) + logger.debug( + "(non-fatal) attribute names must be strings. attribute discarded for %s", + type(key), + ) except Exception: - logger.debug("instana.span._validate_tag: ", exc_info=True) + logger.debug("instana.span._validate_attribute: ", exc_info=True) return (validated_key, validated_value) - def _convert_tag_value(self, value): + def _convert_attribute_value(self, value): final_value = None try: final_value = repr(value) except Exception: - final_value = "(non-fatal) span.set_tag: values must be one of these types: bool, float, int, list, " \ - "set, str or alternatively support 'repr'. tag discarded" + final_value = ( + "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " + "set, str or alternatively support 'repr'. attribute discarded" + ) logger.debug(final_value, exc_info=True) return None return final_value @@ -193,7 +378,7 @@ class SDKSpan(BaseSpan): ENTRY_KIND = ["entry", "server", "consumer"] EXIT_KIND = ["exit", "client", "producer"] - def __init__(self, span, source, service_name, **kwargs): + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(SDKSpan, self).__init__(span, source, service_name, **kwargs) @@ -205,306 +390,417 @@ def __init__(self, span, source, service_name, **kwargs): if service_name is not None: self.data["service"] = service_name - self.data["sdk"]["name"] = span.operation_name + self.data["sdk"]["name"] = span.name self.data["sdk"]["type"] = span_kind[0] - self.data["sdk"]["custom"]["tags"] = self._validate_tags(span.tags) + self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) - if span.logs is not None and len(span.logs) > 0: - logs = DictionaryOfStan() - for log in span.logs: - filtered_key_values = self._validate_tags(log.key_values) - if len(filtered_key_values.keys()) > 0: - logs[repr(log.timestamp)] = filtered_key_values - self.data["sdk"]["custom"]["logs"] = logs + if span.events is not None and len(span.events) > 0: + events = DictionaryOfStan() + for event in span.events: + filtered_attributes = self._validate_attributes(event.attributes) + if len(filtered_attributes.keys()) > 0: + events[repr(event.timestamp)] = filtered_attributes + self.data["sdk"]["custom"]["events"] = events - if "arguments" in span.tags: - self.data['sdk']['arguments'] = span.tags["arguments"] + if "arguments" in span.attributes: + self.data["sdk"]["arguments"] = span.attributes["arguments"] - if "return" in span.tags: - self.data['sdk']['return'] = span.tags["return"] + if "return" in span.attributes: + self.data["sdk"]["return"] = span.attributes["return"] - if len(span.context.baggage) > 0: - self.data["baggage"] = span.context.baggage + # if len(span.context.baggage) > 0: + # self.data["baggage"] = span.context.baggage - def get_span_kind(self, span): + def get_span_kind(self, span) -> Tuple[str, int]: """ - Will retrieve the `span.kind` tag and return a tuple containing the appropriate string and integer + Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer values for the Instana backend - :param span: The span to search for the `span.kind` tag + :param span: The span to search for the `span.kind` attribute :return: Tuple (String, Int) """ kind = ("intermediate", 3) - if "span.kind" in span.tags: - if span.tags["span.kind"] in self.ENTRY_KIND: + if "span.kind" in span.attributes: + if span.attributes["span.kind"] in self.ENTRY_KIND: kind = ("entry", 1) - elif span.tags["span.kind"] in self.EXIT_KIND: + elif span.attributes["span.kind"] in self.EXIT_KIND: kind = ("exit", 2) return kind class RegisteredSpan(BaseSpan): - HTTP_SPANS = ("aiohttp-client", "aiohttp-server", "django", "http", "tornado-client", - "tornado-server", "urllib3", "wsgi", "asgi") - - EXIT_SPANS = ("aiohttp-client", "boto3", "cassandra", "celery-client", "couchbase", "log", "memcache", - "mongo", "mysql", "postgres", "rabbitmq", "redis", "rpc-client", "sqlalchemy", - "tornado-client", "urllib3", "pymongo", "gcs", "gcps-producer") - - ENTRY_SPANS = ("aiohttp-server", "aws.lambda.entry", "celery-worker", "django", "wsgi", "rabbitmq", - "rpc-server", "tornado-server", "gcps-consumer", "asgi") - - LOCAL_SPANS = ("render") - - def __init__(self, span, source, service_name, **kwargs): + HTTP_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "django", + "http", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", + ) + + EXIT_SPANS = ( + "aiohttp-client", + "boto3", + "cassandra", + "celery-client", + "couchbase", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "rabbitmq", + "redis", + "rpc-client", + "sqlalchemy", + "tornado-client", + "urllib3", + "pymongo", + "gcs", + "gcps-producer", + ) + + ENTRY_SPANS = ( + "aiohttp-server", + "aws.lambda.entry", + "celery-worker", + "django", + "wsgi", + "rabbitmq", + "rpc-server", + "tornado-server", + "gcps-consumer", + "asgi", + ) + + LOCAL_SPANS = "render" + + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) - self.n = span.operation_name + self.n = span.name self.k = 1 self.data["service"] = service_name - if span.operation_name in self.ENTRY_SPANS: + if span.name in self.ENTRY_SPANS: # entry self._populate_entry_span_data(span) self._populate_extra_span_attributes(span) - elif span.operation_name in self.EXIT_SPANS: + elif span.name in self.EXIT_SPANS: self.k = 2 # exit self._populate_exit_span_data(span) - elif span.operation_name in self.LOCAL_SPANS: + elif span.name in self.LOCAL_SPANS: self.k = 3 # intermediate span self._populate_local_span_data(span) if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": self.k = 2 # exit - # unify the span operation_name for gcps-producer and gcps-consumer - if "gcps" in span.operation_name: - self.n = 'gcps' + # unify the span name for gcps-producer and gcps-consumer + if "gcps" in span.name: + self.n = "gcps" - # Store any leftover tags in the custom section - if len(span.tags) > 0: - self.data["custom"]["tags"] = self._validate_tags(span.tags) + # Store any leftover attributes in the custom section + if len(span.attributes) > 0: + self.data["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) - def _populate_entry_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) + def _populate_entry_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) - elif span.operation_name == "aws.lambda.entry": - self.data["lambda"]["arn"] = span.tags.pop('lambda.arn', "Unknown") + elif span.name == "aws.lambda.entry": + self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") self.data["lambda"]["alias"] = None self.data["lambda"]["runtime"] = "python" - self.data["lambda"]["functionName"] = span.tags.pop('lambda.name', "Unknown") - self.data["lambda"]["functionVersion"] = span.tags.pop('lambda.version', "Unknown") - self.data["lambda"]["trigger"] = span.tags.pop('lambda.trigger', None) - self.data["lambda"]["error"] = span.tags.pop('lambda.error', None) + self.data["lambda"]["functionName"] = span.attributes.pop( + "lambda.name", "Unknown" + ) + self.data["lambda"]["functionVersion"] = span.attributes.pop( + "lambda.version", "Unknown" + ) + self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) + self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) trigger_type = self.data["lambda"]["trigger"] if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: - self._collect_http_tags(span) - elif trigger_type == 'aws:cloudwatch.events': - self.data["lambda"]["cw"]["events"]["id"] = span.tags.pop('data.lambda.cw.events.id', None) - self.data["lambda"]["cw"]["events"]["more"] = span.tags.pop('lambda.cw.events.more', False) - self.data["lambda"]["cw"]["events"]["resources"] = span.tags.pop('lambda.cw.events.resources', None) - - elif trigger_type == 'aws:cloudwatch.logs': - self.data["lambda"]["cw"]["logs"]["group"] = span.tags.pop('lambda.cw.logs.group', None) - self.data["lambda"]["cw"]["logs"]["stream"] = span.tags.pop('lambda.cw.logs.stream', None) - self.data["lambda"]["cw"]["logs"]["more"] = span.tags.pop('lambda.cw.logs.more', None) - self.data["lambda"]["cw"]["logs"]["events"] = span.tags.pop('lambda.cw.logs.events', None) - - elif trigger_type == 'aws:s3': - self.data["lambda"]["s3"]["events"] = span.tags.pop('lambda.s3.events', None) - elif trigger_type == 'aws:sqs': - self.data["lambda"]["sqs"]["messages"] = span.tags.pop('lambda.sqs.messages', None) - - elif span.operation_name == "celery-worker": - self.data["celery"]["task"] = span.tags.pop('task', None) - self.data["celery"]["task_id"] = span.tags.pop('task_id', None) - self.data["celery"]["scheme"] = span.tags.pop('scheme', None) - self.data["celery"]["host"] = span.tags.pop('host', None) - self.data["celery"]["port"] = span.tags.pop('port', None) - self.data["celery"]["retry-reason"] = span.tags.pop('retry-reason', None) - self.data["celery"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "gcps-consumer": - self.data["gcps"]["op"] = span.tags.pop('gcps.op', None) - self.data["gcps"]["projid"] = span.tags.pop('gcps.projid', None) - self.data["gcps"]["sub"] = span.tags.pop('gcps.sub', None) - - elif span.operation_name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.tags.pop('exchange', None) - self.data["rabbitmq"]["queue"] = span.tags.pop('queue', None) - self.data["rabbitmq"]["sort"] = span.tags.pop('sort', None) - self.data["rabbitmq"]["address"] = span.tags.pop('address', None) - self.data["rabbitmq"]["key"] = span.tags.pop('key', None) - - elif span.operation_name == "rpc-server": - self.data["rpc"]["flavor"] = span.tags.pop('rpc.flavor', None) - self.data["rpc"]["host"] = span.tags.pop('rpc.host', None) - self.data["rpc"]["port"] = span.tags.pop('rpc.port', None) - self.data["rpc"]["call"] = span.tags.pop('rpc.call', None) - self.data["rpc"]["call_type"] = span.tags.pop('rpc.call_type', None) - self.data["rpc"]["params"] = span.tags.pop('rpc.params', None) - self.data["rpc"]["baggage"] = span.tags.pop('rpc.baggage', None) - self.data["rpc"]["error"] = span.tags.pop('rpc.error', None) + self._collect_http_attributes(span) + elif trigger_type == "aws:cloudwatch.events": + self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( + "data.lambda.cw.events.id", None + ) + self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( + "lambda.cw.events.more", False + ) + self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( + "lambda.cw.events.resources", None + ) + + elif trigger_type == "aws:cloudwatch.logs": + self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( + "lambda.cw.logs.group", None + ) + self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( + "lambda.cw.logs.stream", None + ) + self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( + "lambda.cw.logs.more", None + ) + self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( + "lambda.cw.logs.events", None + ) + + elif trigger_type == "aws:s3": + self.data["lambda"]["s3"]["events"] = span.attributes.pop( + "lambda.s3.events", None + ) + elif trigger_type == "aws:sqs": + self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( + "lambda.sqs.messages", None + ) + + elif span.name == "celery-worker": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["retry-reason"] = span.attributes.pop( + "retry-reason", None + ) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcps-consumer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "rpc-server": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) else: - logger.debug("SpanRecorder: Unknown entry span: %s" % span.operation_name) - - def _populate_local_span_data(self, span): - if span.operation_name == "render": - self.data["render"]["name"] = span.tags.pop('name', None) - self.data["render"]["type"] = span.tags.pop('type', None) - self.data["log"]["message"] = span.tags.pop('message', None) - self.data["log"]["parameters"] = span.tags.pop('parameters', None) + logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) + + def _populate_local_span_data(self, span) -> None: + if span.name == "render": + self.data["render"]["name"] = span.attributes.pop("name", None) + self.data["render"]["type"] = span.attributes.pop("type", None) + self.data["event"]["message"] = span.attributes.pop("message", None) + self.data["event"]["parameters"] = span.attributes.pop("parameters", None) else: - logger.debug("SpanRecorder: Unknown local span: %s" % span.operation_name) + logger.debug("SpanRecorder: Unknown local span: %s" % span.name) - def _populate_exit_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) + def _populate_exit_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) - elif span.operation_name == "boto3": - # boto3 also sends http tags - self._collect_http_tags(span) + elif span.name == "boto3": + # boto3 also sends http attributes + self._collect_http_attributes(span) - for tag in ['op', 'ep', 'reg', 'payload', 'error']: - value = span.tags.pop(tag, None) + for attribute in ["op", "ep", "reg", "payload", "error"]: + value = span.attributes.pop(attribute, None) if value is not None: - if tag == 'payload': - self.data["boto3"][tag] = self._validate_tags(value) + if attribute == "payload": + self.data["boto3"][attribute] = self._validate_attributes(value) else: - self.data["boto3"][tag] = value - - elif span.operation_name == "cassandra": - self.data["cassandra"]["cluster"] = span.tags.pop('cassandra.cluster', None) - self.data["cassandra"]["query"] = span.tags.pop('cassandra.query', None) - self.data["cassandra"]["keyspace"] = span.tags.pop('cassandra.keyspace', None) - self.data["cassandra"]["fetchSize"] = span.tags.pop('cassandra.fetchSize', None) - self.data["cassandra"]["achievedConsistency"] = span.tags.pop('cassandra.achievedConsistency', None) - self.data["cassandra"]["triedHosts"] = span.tags.pop('cassandra.triedHosts', None) - self.data["cassandra"]["fullyFetched"] = span.tags.pop('cassandra.fullyFetched', None) - self.data["cassandra"]["error"] = span.tags.pop('cassandra.error', None) - - elif span.operation_name == "celery-client": - self.data["celery"]["task"] = span.tags.pop('task', None) - self.data["celery"]["task_id"] = span.tags.pop('task_id', None) - self.data["celery"]["scheme"] = span.tags.pop('scheme', None) - self.data["celery"]["host"] = span.tags.pop('host', None) - self.data["celery"]["port"] = span.tags.pop('port', None) - self.data["celery"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "couchbase": - self.data["couchbase"]["hostname"] = span.tags.pop('couchbase.hostname', None) - self.data["couchbase"]["bucket"] = span.tags.pop('couchbase.bucket', None) - self.data["couchbase"]["type"] = span.tags.pop('couchbase.type', None) - self.data["couchbase"]["error"] = span.tags.pop('couchbase.error', None) - self.data["couchbase"]["error_type"] = span.tags.pop('couchbase.error_type', None) - self.data["couchbase"]["sql"] = span.tags.pop('couchbase.sql', None) - - elif span.operation_name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.tags.pop('exchange', None) - self.data["rabbitmq"]["queue"] = span.tags.pop('queue', None) - self.data["rabbitmq"]["sort"] = span.tags.pop('sort', None) - self.data["rabbitmq"]["address"] = span.tags.pop('address', None) - self.data["rabbitmq"]["key"] = span.tags.pop('key', None) - - elif span.operation_name == "redis": - self.data["redis"]["connection"] = span.tags.pop('connection', None) - self.data["redis"]["driver"] = span.tags.pop('driver', None) - self.data["redis"]["command"] = span.tags.pop('command', None) - self.data["redis"]["error"] = span.tags.pop('redis.error', None) - self.data["redis"]["subCommands"] = span.tags.pop('subCommands', None) - - elif span.operation_name == "rpc-client": - self.data["rpc"]["flavor"] = span.tags.pop('rpc.flavor', None) - self.data["rpc"]["host"] = span.tags.pop('rpc.host', None) - self.data["rpc"]["port"] = span.tags.pop('rpc.port', None) - self.data["rpc"]["call"] = span.tags.pop('rpc.call', None) - self.data["rpc"]["call_type"] = span.tags.pop('rpc.call_type', None) - self.data["rpc"]["params"] = span.tags.pop('rpc.params', None) - self.data["rpc"]["baggage"] = span.tags.pop('rpc.baggage', None) - self.data["rpc"]["error"] = span.tags.pop('rpc.error', None) - - elif span.operation_name == "sqlalchemy": - self.data["sqlalchemy"]["sql"] = span.tags.pop('sqlalchemy.sql', None) - self.data["sqlalchemy"]["eng"] = span.tags.pop('sqlalchemy.eng', None) - self.data["sqlalchemy"]["url"] = span.tags.pop('sqlalchemy.url', None) - self.data["sqlalchemy"]["err"] = span.tags.pop('sqlalchemy.err', None) - - elif span.operation_name == "mysql": - self.data["mysql"]["host"] = span.tags.pop('host', None) - self.data["mysql"]["port"] = span.tags.pop('port', None) - self.data["mysql"]["db"] = span.tags.pop(ot_tags.DATABASE_INSTANCE, None) - self.data["mysql"]["user"] = span.tags.pop(ot_tags.DATABASE_USER, None) - self.data["mysql"]["stmt"] = span.tags.pop(ot_tags.DATABASE_STATEMENT, None) - self.data["mysql"]["error"] = span.tags.pop('mysql.error', None) - - elif span.operation_name == "postgres": - self.data["pg"]["host"] = span.tags.pop('host', None) - self.data["pg"]["port"] = span.tags.pop('port', None) - self.data["pg"]["db"] = span.tags.pop(ot_tags.DATABASE_INSTANCE, None) - self.data["pg"]["user"] = span.tags.pop(ot_tags.DATABASE_USER, None) - self.data["pg"]["stmt"] = span.tags.pop(ot_tags.DATABASE_STATEMENT, None) - self.data["pg"]["error"] = span.tags.pop('pg.error', None) - - elif span.operation_name == "mongo": - service = "%s:%s" % (span.tags.pop('host', None), span.tags.pop('port', None)) - namespace = "%s.%s" % (span.tags.pop('db', "?"), span.tags.pop('collection', "?")) + self.data["boto3"][attribute] = value + + elif span.name == "cassandra": + self.data["cassandra"]["cluster"] = span.attributes.pop( + "cassandra.cluster", None + ) + self.data["cassandra"]["query"] = span.attributes.pop( + "cassandra.query", None + ) + self.data["cassandra"]["keyspace"] = span.attributes.pop( + "cassandra.keyspace", None + ) + self.data["cassandra"]["fetchSize"] = span.attributes.pop( + "cassandra.fetchSize", None + ) + self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( + "cassandra.achievedConsistency", None + ) + self.data["cassandra"]["triedHosts"] = span.attributes.pop( + "cassandra.triedHosts", None + ) + self.data["cassandra"]["fullyFetched"] = span.attributes.pop( + "cassandra.fullyFetched", None + ) + self.data["cassandra"]["error"] = span.attributes.pop( + "cassandra.error", None + ) + + elif span.name == "celery-client": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "couchbase": + self.data["couchbase"]["hostname"] = span.attributes.pop( + "couchbase.hostname", None + ) + self.data["couchbase"]["bucket"] = span.attributes.pop( + "couchbase.bucket", None + ) + self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) + self.data["couchbase"]["error"] = span.attributes.pop( + "couchbase.error", None + ) + self.data["couchbase"]["error_type"] = span.attributes.pop( + "couchbase.error_type", None + ) + self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "redis": + self.data["redis"]["connection"] = span.attributes.pop("connection", None) + self.data["redis"]["driver"] = span.attributes.pop("driver", None) + self.data["redis"]["command"] = span.attributes.pop("command", None) + self.data["redis"]["error"] = span.attributes.pop("redis.error", None) + self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) + + elif span.name == "rpc-client": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + + elif span.name == "sqlalchemy": + self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) + self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) + self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) + self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) + + elif span.name == "mysql": + self.data["mysql"]["host"] = span.attributes.pop("host", None) + self.data["mysql"]["port"] = span.attributes.pop("port", None) + self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) + self.data["mysql"]["user"] = span.attributes.pop("db.user", None) + self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) + + elif span.name == "postgres": + self.data["pg"]["host"] = span.attributes.pop("host", None) + self.data["pg"]["port"] = span.attributes.pop("port", None) + self.data["pg"]["db"] = span.attributes.pop("db.instance", None) + self.data["pg"]["user"] = span.attributes.pop("db.user", None) + self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["pg"]["error"] = span.attributes.pop("pg.error", None) + + elif span.name == "mongo": + service = "%s:%s" % ( + span.attributes.pop("host", None), + span.attributes.pop("port", None), + ) + namespace = "%s.%s" % ( + span.attributes.pop("db", "?"), + span.attributes.pop("collection", "?"), + ) self.data["mongo"]["service"] = service self.data["mongo"]["namespace"] = namespace - self.data["mongo"]["command"] = span.tags.pop('command', None) - self.data["mongo"]["filter"] = span.tags.pop('filter', None) - self.data["mongo"]["json"] = span.tags.pop('json', None) - self.data["mongo"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "gcs": - self.data["gcs"]["op"] = span.tags.pop('gcs.op') - self.data["gcs"]["bucket"] = span.tags.pop('gcs.bucket', None) - self.data["gcs"]["object"] = span.tags.pop('gcs.object', None) - self.data["gcs"]["entity"] = span.tags.pop('gcs.entity', None) - self.data["gcs"]["range"] = span.tags.pop('gcs.range', None) - self.data["gcs"]["sourceBucket"] = span.tags.pop('gcs.sourceBucket', None) - self.data["gcs"]["sourceObject"] = span.tags.pop('gcs.sourceObject', None) - self.data["gcs"]["sourceObjects"] = span.tags.pop('gcs.sourceObjects', None) - self.data["gcs"]["destinationBucket"] = span.tags.pop('gcs.destinationBucket', None) - self.data["gcs"]["destinationObject"] = span.tags.pop('gcs.destinationObject', None) - self.data["gcs"]["numberOfOperations"] = span.tags.pop('gcs.numberOfOperations', None) - self.data["gcs"]["projectId"] = span.tags.pop('gcs.projectId', None) - self.data["gcs"]["accessId"] = span.tags.pop('gcs.accessId', None) - - elif span.operation_name == "gcps-producer": - self.data["gcps"]["op"] = span.tags.pop('gcps.op', None) - self.data["gcps"]["projid"] = span.tags.pop('gcps.projid', None) - self.data["gcps"]["top"] = span.tags.pop('gcps.top', None) - - elif span.operation_name == "log": + self.data["mongo"]["command"] = span.attributes.pop("command", None) + self.data["mongo"]["filter"] = span.attributes.pop("filter", None) + self.data["mongo"]["json"] = span.attributes.pop("json", None) + self.data["mongo"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcs": + self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) + self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) + self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) + self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) + self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) + self.data["gcs"]["sourceBucket"] = span.attributes.pop( + "gcs.sourceBucket", None + ) + self.data["gcs"]["sourceObject"] = span.attributes.pop( + "gcs.sourceObject", None + ) + self.data["gcs"]["sourceObjects"] = span.attributes.pop( + "gcs.sourceObjects", None + ) + self.data["gcs"]["destinationBucket"] = span.attributes.pop( + "gcs.destinationBucket", None + ) + self.data["gcs"]["destinationObject"] = span.attributes.pop( + "gcs.destinationObject", None + ) + self.data["gcs"]["numberOfOperations"] = span.attributes.pop( + "gcs.numberOfOperations", None + ) + self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) + self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) + + elif span.name == "gcps-producer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) + + elif span.name == "log": # use last special key values - for l in span.logs: - if "message" in l.key_values: - self.data["log"]["message"] = l.key_values.pop("message", None) - if "parameters" in l.key_values: - self.data["log"]["parameters"] = l.key_values.pop("parameters", None) + for event in span.events: + if "message" in event.attributes: + self.data["event"]["message"] = event.attributes.pop( + "message", None + ) + if "parameters" in event.attributes: + self.data["event"]["parameters"] = event.attributes.pop( + "parameters", None + ) else: - logger.debug("SpanRecorder: Unknown exit span: %s" % span.operation_name) - - def _collect_http_tags(self, span): - self.data["http"]["host"] = span.tags.pop("http.host", None) - self.data["http"]["url"] = span.tags.pop(ot_tags.HTTP_URL, None) - self.data["http"]["path"] = span.tags.pop("http.path", None) - self.data["http"]["params"] = span.tags.pop('http.params', None) - self.data["http"]["method"] = span.tags.pop(ot_tags.HTTP_METHOD, None) - self.data["http"]["status"] = span.tags.pop(ot_tags.HTTP_STATUS_CODE, None) - self.data["http"]["path_tpl"] = span.tags.pop("http.path_tpl", None) - self.data["http"]["error"] = span.tags.pop('http.error', None) - - if len(span.tags) > 0: + logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) + + def _collect_http_attributes(self, span) -> None: + self.data["http"]["host"] = span.attributes.pop("http.host", None) + self.data["http"]["url"] = span.attributes.pop("http.url", None) + self.data["http"]["path"] = span.attributes.pop("http.path", None) + self.data["http"]["params"] = span.attributes.pop("http.params", None) + self.data["http"]["method"] = span.attributes.pop("http.method", None) + self.data["http"]["status"] = span.attributes.pop("http.status_code", None) + self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) + self.data["http"]["error"] = span.attributes.pop("http.error", None) + + if len(span.attributes) > 0: custom_headers = [] - for key in span.tags: + for key in span.attributes: if key[0:12] == "http.header.": custom_headers.append(key) for key in custom_headers: trimmed_key = key[12:] - self.data["http"]["header"][trimmed_key] = span.tags.pop(key) + self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/src/instana/instrumentation/pyramid/__init__.py b/src/instana/span/__init__.py similarity index 100% rename from src/instana/instrumentation/pyramid/__init__.py rename to src/instana/span/__init__.py diff --git a/src/instana/span/base_span.py b/src/instana/span/base_span.py new file mode 100644 index 00000000..1ff47b4d --- /dev/null +++ b/src/instana/span/base_span.py @@ -0,0 +1,119 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import TYPE_CHECKING, Type +import six + +from instana.log import logger +from instana.util import DictionaryOfStan + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + +class BaseSpan(object): + sy = None + + def __str__(self) -> str: + return "BaseSpan(%s)" % self.__dict__.__str__() + + def __repr__(self) -> str: + return self.__dict__.__str__() + + def __init__(self, span: Type["Span"], source, **kwargs) -> None: + # pylint: disable=invalid-name + self.t = span.context.trace_id + self.p = span.parent_id + # self.p = span.context.span_id if span.context.is_remote else None + self.s = span.context.span_id + self.l = span.context.level + self.ts = round(span.start_time / 10**6) + self.d = round(span.duration / 10**6) if span.duration else None + self.f = source + self.ec = span.attributes.pop("ec", None) + self.data = DictionaryOfStan() + self.stack = span.stack + + if span.synthetic is True: + self.sy = span.synthetic + + self.__dict__.update(kwargs) + + def _populate_extra_span_attributes(self, span) -> None: + if span.context.trace_parent: + self.tp = span.context.trace_parent + if span.context.instana_ancestor: + self.ia = span.context.instana_ancestor + if span.context.long_trace_id: + self.lt = span.context.long_trace_id + if span.context.correlation_type: + self.crtp = span.context.correlation_type + if span.context.correlation_id: + self.crid = span.context.correlation_id + + def _validate_attributes(self, attributes): + """ + This method will loop through a set of attributes to validate each key and value. + + :param attributes: dict of attributes + :return: dict - a filtered set of attributes + """ + filtered_attributes = DictionaryOfStan() + for key in attributes.keys(): + validated_key, validated_value = self._validate_attribute( + key, attributes[key] + ) + if validated_key is not None and validated_value is not None: + filtered_attributes[validated_key] = validated_value + return filtered_attributes + + def _validate_attribute(self, key, value): + """ + This method will assure that and are valid to set as a attribute. + If fails the check, an attempt will be made to convert it into + something useful. + + On check failure, this method will return None values indicating that the attribute is + not valid and could not be converted into something useful + + :param key: The attribute key + :param value: The attribute value + :return: Tuple (key, value) + """ + validated_key = None + validated_value = None + + try: + # Attribute keys must be some type of text or string type + if isinstance(key, (six.text_type, six.string_types)): + validated_key = key[0:1024] # Max key length of 1024 characters + + if isinstance( + value, + (bool, float, int, list, dict, six.text_type, six.string_types), + ): + validated_value = value + else: + validated_value = self._convert_attribute_value(value) + else: + logger.debug( + "(non-fatal) attribute names must be strings. attribute discarded for %s", + type(key), + ) + except Exception: + logger.debug("instana.span._validate_attribute: ", exc_info=True) + + return (validated_key, validated_value) + + def _convert_attribute_value(self, value): + final_value = None + + try: + final_value = repr(value) + except Exception: + final_value = ( + "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " + "set, str or alternatively support 'repr'. attribute discarded" + ) + logger.debug(final_value, exc_info=True) + return None + return final_value diff --git a/src/instana/span/kind.py b/src/instana/span/kind.py new file mode 100644 index 00000000..9fd7b340 --- /dev/null +++ b/src/instana/span/kind.py @@ -0,0 +1,58 @@ +# (c) Copyright IBM Corp. 2024 + +from opentelemetry.trace import SpanKind + +ENTRY_KIND = ("entry", "server", "consumer", SpanKind.SERVER, SpanKind.CONSUMER) + +EXIT_KIND = ("exit", "client", "producer", SpanKind.CLIENT, SpanKind.PRODUCER) + +LOCAL_SPANS = ("asyncio", "render", SpanKind.INTERNAL) + +HTTP_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "django", + "http", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", +) + +ENTRY_SPANS = ( + "aiohttp-server", + "aws.lambda.entry", + "celery-worker", + "django", + "wsgi", + "rabbitmq", + "rpc-server", + "tornado-server", + "gcps-consumer", + "asgi", +) + +EXIT_SPANS = ( + "aiohttp-client", + "boto3", + "cassandra", + "celery-client", + "couchbase", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "rabbitmq", + "redis", + "rpc-client", + "sqlalchemy", + "tornado-client", + "urllib3", + "pymongo", + "gcs", + "gcps-producer", +) + +REGISTERED_SPANS = LOCAL_SPANS + ENTRY_SPANS + EXIT_SPANS diff --git a/src/instana/span/readable_span.py b/src/instana/span/readable_span.py new file mode 100644 index 00000000..3e95ec67 --- /dev/null +++ b/src/instana/span/readable_span.py @@ -0,0 +1,112 @@ +# (c) Copyright IBM Corp. 2024 + +from time import time_ns +from typing import Optional, Sequence, List + +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util import types + +from instana.span_context import SpanContext + + +class Event: + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + self._name = name + self._attributes = attributes + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class ReadableSpan: + """ + Provides read-only access to span attributes. + + Users should NOT be creating these objects directly. + `ReadableSpan`s are created as a direct result from using the tracing pipeline + via the `Tracer`. + """ + + def __init__( + self, + name: str, + context: SpanContext, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + stack: Optional[List] = None, + ) -> None: + self._name = name + self._context = context + self._start_time = start_time or time_ns() + self._end_time = end_time + self._duration = ( + self._end_time - self._start_time + if self._start_time and self._end_time + else None + ) + self._attributes = attributes if attributes else {} + self._events = events + self._parent_id = parent_id + self._status = status + self.stack = stack + self.synthetic = False + if context.synthetic: + self.synthetic = True + + @property + def name(self) -> str: + return self._name + + @property + def context(self) -> SpanContext: + return self._context + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def duration(self) -> Optional[int]: + return self._duration + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + @property + def events(self) -> Sequence[Event]: + return self._events + + @property + def status(self) -> Status: + return self._status + + @property + def parent_id(self) -> int: + return self._parent_id diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py new file mode 100644 index 00000000..6164ca86 --- /dev/null +++ b/src/instana/span/registered_span.py @@ -0,0 +1,327 @@ +# (c) Copyright IBM Corp. 2024 + +from instana.log import logger +from instana.span.base_span import BaseSpan +from instana.span.kind import ENTRY_SPANS, EXIT_SPANS, HTTP_SPANS, LOCAL_SPANS + +from opentelemetry.trace import SpanKind +from opentelemetry.semconv.trace import SpanAttributes + + +class RegisteredSpan(BaseSpan): + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(RegisteredSpan, self).__init__(span, source, **kwargs) + self.n = span.name + self.k = SpanKind.SERVER # entry -> Server span represents a synchronous incoming remote call such as an incoming HTTP request + + self.data["service"] = service_name + if span.name in ENTRY_SPANS: + # entry + self._populate_entry_span_data(span) + self._populate_extra_span_attributes(span) + elif span.name in EXIT_SPANS: + self.k = SpanKind.CLIENT # exit -> Client span represents a synchronous outgoing remote call such as an outgoing HTTP request or database call + self._populate_exit_span_data(span) + elif span.name in LOCAL_SPANS: + self.k = SpanKind.INTERNAL # intermediate -> Internal span represents an internal operation within an application + self._populate_local_span_data(span) + + if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": + self.k = SpanKind.CLIENT # exit + + # unify the span name for gcps-producer and gcps-consumer + if "gcps" in span.name: + self.n = "gcps" + + # Store any leftover attributes in the custom section + if len(span.attributes) > 0: + self.data["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + def _populate_entry_span_data(self, span) -> None: + if span.name in HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "aws.lambda.entry": + self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") + self.data["lambda"]["alias"] = None + self.data["lambda"]["runtime"] = "python" + self.data["lambda"]["functionName"] = span.attributes.pop( + "lambda.name", "Unknown" + ) + self.data["lambda"]["functionVersion"] = span.attributes.pop( + "lambda.version", "Unknown" + ) + self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) + self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) + + trigger_type = self.data["lambda"]["trigger"] + + if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: + self._collect_http_attributes(span) + elif trigger_type == "aws:cloudwatch.events": + self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( + "data.lambda.cw.events.id", None + ) + self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( + "lambda.cw.events.more", False + ) + self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( + "lambda.cw.events.resources", None + ) + + elif trigger_type == "aws:cloudwatch.logs": + self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( + "lambda.cw.logs.group", None + ) + self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( + "lambda.cw.logs.stream", None + ) + self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( + "lambda.cw.logs.more", None + ) + self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( + "lambda.cw.logs.events", None + ) + + elif trigger_type == "aws:s3": + self.data["lambda"]["s3"]["events"] = span.attributes.pop( + "lambda.s3.events", None + ) + elif trigger_type == "aws:sqs": + self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( + "lambda.sqs.messages", None + ) + + elif span.name == "celery-worker": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["retry-reason"] = span.attributes.pop( + "retry-reason", None + ) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcps-consumer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "rpc-server": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + else: + logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) + + def _populate_local_span_data(self, span) -> None: + if span.name == "render": + self.data["render"]["name"] = span.attributes.pop("name", None) + self.data["render"]["type"] = span.attributes.pop("type", None) + self.data["log"]["message"] = span.attributes.pop("message", None) + self.data["log"]["parameters"] = span.attributes.pop("parameters", None) + else: + logger.debug("SpanRecorder: Unknown local span: %s" % span.name) + + def _populate_exit_span_data(self, span) -> None: + if span.name in HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "boto3": + # boto3 also sends http attributes + self._collect_http_attributes(span) + + for attribute in ["op", "ep", "reg", "payload", "error"]: + value = span.attributes.pop(attribute, None) + if value is not None: + if attribute == "payload": + self.data["boto3"][attribute] = self._validate_attributes(value) + else: + self.data["boto3"][attribute] = value + + elif span.name == "cassandra": + self.data["cassandra"]["cluster"] = span.attributes.pop( + "cassandra.cluster", None + ) + self.data["cassandra"]["query"] = span.attributes.pop( + "cassandra.query", None + ) + self.data["cassandra"]["keyspace"] = span.attributes.pop( + "cassandra.keyspace", None + ) + self.data["cassandra"]["fetchSize"] = span.attributes.pop( + "cassandra.fetchSize", None + ) + self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( + "cassandra.achievedConsistency", None + ) + self.data["cassandra"]["triedHosts"] = span.attributes.pop( + "cassandra.triedHosts", None + ) + self.data["cassandra"]["fullyFetched"] = span.attributes.pop( + "cassandra.fullyFetched", None + ) + self.data["cassandra"]["error"] = span.attributes.pop( + "cassandra.error", None + ) + + elif span.name == "celery-client": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "couchbase": + self.data["couchbase"]["hostname"] = span.attributes.pop( + "couchbase.hostname", None + ) + self.data["couchbase"]["bucket"] = span.attributes.pop( + "couchbase.bucket", None + ) + self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) + self.data["couchbase"]["error"] = span.attributes.pop( + "couchbase.error", None + ) + self.data["couchbase"]["error_type"] = span.attributes.pop( + "couchbase.error_type", None + ) + self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "redis": + self.data["redis"]["connection"] = span.attributes.pop("connection", None) + self.data["redis"]["driver"] = span.attributes.pop("driver", None) + self.data["redis"]["command"] = span.attributes.pop("command", None) + self.data["redis"]["error"] = span.attributes.pop("redis.error", None) + self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) + + elif span.name == "rpc-client": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + + elif span.name == "sqlalchemy": + self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) + self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) + self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) + self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) + + elif span.name == "mysql": + self.data["mysql"]["host"] = span.attributes.pop("host", None) + self.data["mysql"]["port"] = span.attributes.pop("port", None) + self.data["mysql"]["db"] = span.attributes.pop(SpanAttributes.DB_NAME, None) + self.data["mysql"]["user"] = span.attributes.pop(SpanAttributes.DB_USER, None) + self.data["mysql"]["stmt"] = span.attributes.pop(SpanAttributes.DB_STATEMENT, None) + self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) + + elif span.name == "postgres": + self.data["pg"]["host"] = span.attributes.pop("host", None) + self.data["pg"]["port"] = span.attributes.pop("port", None) + self.data["pg"]["db"] = span.attributes.pop("db.name", None) + self.data["pg"]["user"] = span.attributes.pop("db.user", None) + self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["pg"]["error"] = span.attributes.pop("pg.error", None) + + elif span.name == "mongo": + service = f"{span.attributes.pop(SpanAttributes.SERVER_ADDRESS, None)}:{span.attributes.pop(SpanAttributes.SERVER_PORT, None)}" + namespace = f"{span.attributes.pop(SpanAttributes.DB_NAME, '?')}.{span.attributes.pop(SpanAttributes.DB_MONGODB_COLLECTION, '?')}" + + self.data["mongo"]["service"] = service + self.data["mongo"]["namespace"] = namespace + self.data["mongo"]["command"] = span.attributes.pop("command", None) + self.data["mongo"]["filter"] = span.attributes.pop("filter", None) + self.data["mongo"]["json"] = span.attributes.pop("json", None) + self.data["mongo"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcs": + self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) + self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) + self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) + self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) + self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) + self.data["gcs"]["sourceBucket"] = span.attributes.pop( + "gcs.sourceBucket", None + ) + self.data["gcs"]["sourceObject"] = span.attributes.pop( + "gcs.sourceObject", None + ) + self.data["gcs"]["sourceObjects"] = span.attributes.pop( + "gcs.sourceObjects", None + ) + self.data["gcs"]["destinationBucket"] = span.attributes.pop( + "gcs.destinationBucket", None + ) + self.data["gcs"]["destinationObject"] = span.attributes.pop( + "gcs.destinationObject", None + ) + self.data["gcs"]["numberOfOperations"] = span.attributes.pop( + "gcs.numberOfOperations", None + ) + self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) + self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) + + elif span.name == "gcps-producer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) + + elif span.name == "log": + # use last special key values + for event in span.events: + if "message" in event.attributes: + self.data["log"]["message"] = event.attributes.pop("message", None) + if "parameters" in event.attributes: + self.data["log"]["parameters"] = event.attributes.pop( + "parameters", None + ) + else: + logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) + + def _collect_http_attributes(self, span) -> None: + self.data["http"]["host"] = span.attributes.pop("http.host", None) + self.data["http"]["url"] = span.attributes.pop("http.url", None) + self.data["http"]["path"] = span.attributes.pop("http.path", None) + self.data["http"]["params"] = span.attributes.pop("http.params", None) + self.data["http"]["method"] = span.attributes.pop("http.method", None) + self.data["http"]["status"] = span.attributes.pop("http.status_code", None) + self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) + self.data["http"]["error"] = span.attributes.pop("http.error", None) + + if len(span.attributes) > 0: + custom_headers = [] + for key in span.attributes: + if key[0:12] == "http.header.": + custom_headers.append(key) + + for key in custom_headers: + trimmed_key = key[12:] + self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/src/instana/span/sdk_span.py b/src/instana/span/sdk_span.py new file mode 100644 index 00000000..89485144 --- /dev/null +++ b/src/instana/span/sdk_span.py @@ -0,0 +1,62 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Tuple + +from instana.span.base_span import BaseSpan +from instana.span.kind import ENTRY_KIND, EXIT_KIND +from instana.util import DictionaryOfStan + + +class SDKSpan(BaseSpan): + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(SDKSpan, self).__init__(span, source, **kwargs) + + span_kind = self.get_span_kind(span) + + self.n = "sdk" + self.k = span_kind[1] + + if service_name is not None: + self.data["service"] = service_name + + self.data["sdk"]["name"] = span.name + self.data["sdk"]["type"] = span_kind[0] + self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + if span.events is not None and len(span.events) > 0: + events = DictionaryOfStan() + for event in span.events: + filtered_attributes = self._validate_attributes(event.attributes) + if len(filtered_attributes.keys()) > 0: + events[repr(event.timestamp)] = filtered_attributes + self.data["sdk"]["custom"]["events"] = events + + if "arguments" in span.attributes: + self.data["sdk"]["arguments"] = span.attributes["arguments"] + + if "return" in span.attributes: + self.data["sdk"]["return"] = span.attributes["return"] + + # if len(span.context.baggage) > 0: + # self.data["baggage"] = span.context.baggage + + def get_span_kind(self, span) -> Tuple[str, int]: + """ + Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer + values for the Instana backend + + :param span: The span to search for the `span.kind` attribute + :return: Tuple (String, Int) + """ + kind = ("intermediate", 3) + if "span.kind" in span.attributes: + if span.attributes["span.kind"] in ENTRY_KIND: + kind = ("entry", 1) + elif span.attributes["span.kind"] in EXIT_KIND: + kind = ("exit", 2) + return kind + + diff --git a/src/instana/span/span.py b/src/instana/span/span.py new file mode 100644 index 00000000..61b99e55 --- /dev/null +++ b/src/instana/span/span.py @@ -0,0 +1,252 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2017 + +""" +This module contains the classes that represents spans. + +InstanaSpan - the OpenTelemetry based span used during tracing + +When an InstanaSpan is finished, it is converted into either an SDKSpan +or RegisteredSpan depending on type. + +BaseSpan: Base class containing the commonalities for the two descendants + - SDKSpan: Class that represents an SDK type span + - RegisteredSpan: Class that represents a Registered type span +""" + +from threading import Lock +from time import time_ns +from typing import Dict, Optional, Sequence, Union + +from opentelemetry.context import get_value +from opentelemetry.context.context import Context +from opentelemetry.trace import ( + _SPAN_KEY, + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN_ID, + INVALID_TRACE_ID, + Span, +) +from opentelemetry.trace.span import NonRecordingSpan +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util import types + +from instana.log import logger +from instana.recorder import StanRecorder +from instana.span.kind import HTTP_SPANS +from instana.span.readable_span import Event, ReadableSpan +from instana.span_context import SpanContext + + +class InstanaSpan(Span, ReadableSpan): + def __init__( + self, + name: str, + context: SpanContext, + span_processor: StanRecorder, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + super().__init__( + name=name, + context=context, + parent_id=parent_id, + start_time=start_time, + end_time=end_time, + attributes=attributes, + events=events, + status=status, + # kind=kind, + ) + self._span_processor = span_processor + self._lock = Lock() + + def get_span_context(self) -> SpanContext: + return self._context + + def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: + if not self._attributes: + self._attributes = {} + + with self._lock: + for key, value in attributes.items(): + self._attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) + + def update_name(self, name: str) -> None: + with self._lock: + self._name = name + + def is_recording(self) -> bool: + return self._end_time is None + + def set_status( + self, + status: Union[Status, StatusCode], + description: Optional[str] = None, + ) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + event = Event( + name=name, + attributes=attributes, + timestamp=timestamp, + ) + + self._events.append(event) + + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + escaped: bool = False, + ) -> None: + """ + Records an exception as a span event. This will record pertinent info from the exception and + assure that this span is marked as errored. + """ + try: + message = "" + self.mark_as_errored() + if hasattr(exception, "__str__") and len(str(exception)) > 0: + message = str(exception) + elif hasattr(exception, "message") and exception.message is not None: + message = exception.message + else: + message = repr(exception) + + if self.name in ["rpc-server", "rpc-client"]: + self.set_attribute("rpc.error", message) + elif self.name == "mysql": + self.set_attribute("mysql.error", message) + elif self.name == "postgres": + self.set_attribute("pg.error", message) + elif self.name in HTTP_SPANS: + self.set_attribute("http.error", message) + elif self.name in ["celery-client", "celery-worker"]: + self.set_attribute("error", message) + elif self.name == "sqlalchemy": + self.set_attribute("sqlalchemy.err", message) + elif self.name == "aws.lambda.entry": + self.set_attribute("lambda.error", message) + else: + _attributes = {"message": message} + if attributes: + _attributes.update(attributes) + self.add_event( + name="exception", attributes=_attributes, timestamp=timestamp + ) + except Exception: + logger.debug("span.record_exception", exc_info=True) + raise + + def _readable_span(self) -> ReadableSpan: + return ReadableSpan( + name=self.name, + context=self.context, + parent_id=self.parent_id, + start_time=self.start_time, + end_time=self.end_time, + attributes=self.attributes, + events=self.events, + status=self.status, + stack=self.stack, + # kind=self.kind, + ) + + def end(self, end_time: Optional[int] = None) -> None: + with self._lock: + self._end_time = end_time if end_time else time_ns() + self._duration = self._end_time - self._start_time + + self._span_processor.record_span(self._readable_span()) + + def mark_as_errored(self, attributes: types.Attributes = None) -> None: + """ + Mark this span as errored. + + @param attributes: optional attributes to add to the span + """ + try: + ec = self.attributes.get("ec", 0) + self.set_attribute("ec", ec + 1) + + if attributes is not None and isinstance(attributes, dict): + for key in attributes: + self.set_attribute(key, attributes[key]) + except Exception: + logger.debug("span.mark_as_errored", exc_info=True) + + def assure_errored(self) -> None: + """ + Make sure that this span is marked as errored. + @return: None + """ + try: + ec = self.attributes.get("ec", None) + if ec is None or ec == 0: + self.set_attribute("ec", 1) + except Exception: + logger.debug("span.assure_errored", exc_info=True) + + +INVALID_SPAN_CONTEXT = SpanContext( + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +) +INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) + + +def get_current_span(context: Optional[Context] = None) -> InstanaSpan: + """Retrieve the current span. + + Args: + context: A Context object. If one is not passed, the + default current context is used instead. + + Returns: + The Span set in the context if it exists. INVALID_SPAN otherwise. + """ + span = get_value(_SPAN_KEY, context=context) + if span is None or not isinstance(span, InstanaSpan): + return INVALID_SPAN + return span diff --git a/src/instana/span_context.py b/src/instana/span_context.py index 1c874a35..d961fbf1 100644 --- a/src/instana/span_context.py +++ b/src/instana/span_context.py @@ -1,103 +1,133 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 - -class SpanContext(): - def __init__( - self, - trace_id=None, - span_id=None, - baggage=None, - sampled=True, - level=1, - synthetic=False - ): - - self.level = level - self.trace_id = trace_id - self.span_id = span_id - self.sampled = sampled - self.synthetic = synthetic - self._baggage = baggage or {} - - self.trace_parent = None # true/false flag - self.instana_ancestor = None - self.long_trace_id = None - self.correlation_type = None - self.correlation_id = None - self.traceparent = None # temporary storage of the validated traceparent header of the incoming request - self.tracestate = None # temporary storage of the tracestate header +from opentelemetry.trace import SpanContext as OtelSpanContext + +import typing + +from opentelemetry.trace import SpanContext as OtelSpanContext +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + TraceFlags, + TraceState, + format_span_id, +) + + +class SpanContext(OtelSpanContext): + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Required Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + is_remote: True if propagated from a remote parent. + """ + + def __new__( + cls, + trace_id: int, + span_id: int, + is_remote: bool, + trace_flags: typing.Optional[TraceFlags] = DEFAULT_TRACE_OPTIONS, + trace_state: typing.Optional[TraceState] = DEFAULT_TRACE_STATE, + level=1, + synthetic=False, + trace_parent=None, # true/false flag, + instana_ancestor=None, + long_trace_id=None, + correlation_type=None, + correlation_id=None, + traceparent=None, # temporary storage of the validated traceparent header of the incoming request + tracestate=None, # temporary storage of the tracestate header + **kwargs, + ) -> "SpanContext": + instance = super().__new__( + cls, trace_id, span_id, is_remote, trace_flags, trace_state + ) + return tuple.__new__( + cls, + ( + instance.trace_id, + instance.span_id, + instance.is_remote, + instance.trace_flags, + instance.trace_state, + instance.is_valid, + level, + synthetic, + trace_parent, # true/false flag, + instana_ancestor, + long_trace_id, + correlation_type, + correlation_id, + traceparent, # temporary storage of the validated traceparent header of the incoming request + tracestate, # temporary storage of the tracestate header + ), + ) + + def __getnewargs__( + self, + ): # -> typing.Tuple[int, int, bool, "TraceFlags", "TraceState", int, bool, bool]: + return ( + self.trace_id, + self.span_id, + self.is_remote, + self.trace_flags, + self.trace_state, + self.level, + self.synthetic, + self.trace_parent, + self.instana_ancestor, + self.long_trace_id, + self.correlation_type, + self.correlation_id, + self.traceparent, + self.tracestate, + ) @property - def traceparent(self): - return self._traceparent - - @traceparent.setter - def traceparent(self, value): - self._traceparent = value + def level(self) -> int: + return self[6] @property - def tracestate(self): - return self._tracestate - - @tracestate.setter - def tracestate(self, value): - self._tracestate = value + def synthetic(self) -> bool: + return self[7] @property - def trace_parent(self): - return self._trace_parent - - @trace_parent.setter - def trace_parent(self, value): - self._trace_parent = value + def trace_parent(self) -> bool: + return self[8] @property def instana_ancestor(self): - return self._instana_ancestor - - @instana_ancestor.setter - def instana_ancestor(self, value): - self._instana_ancestor = value + return self[9] @property def long_trace_id(self): - return self._long_trace_id - - @long_trace_id.setter - def long_trace_id(self, value): - self._long_trace_id = value + return self[10] @property def correlation_type(self): - return self._correlation_type - - @correlation_type.setter - def correlation_type(self, value): - self._correlation_type = value + return self[11] @property def correlation_id(self): - return self._correlation_id + return self[12] - @correlation_id.setter - def correlation_id(self, value): - self._correlation_id = value + @property + def traceparent(self): + return self[13] @property - def baggage(self): - return self._baggage + def tracestate(self): + return self[14] @property - def suppression(self): + def suppression(self) -> bool: return self.level == 0 - def with_baggage_item(self, key, value): - new_baggage = self._baggage.copy() - new_baggage[key] = value - return SpanContext( - trace_id=self.trace_id, - span_id=self.span_id, - sampled=self.sampled, - level=self.level, - baggage=new_baggage) + def __repr__(self) -> str: + return f"{type(self).__name__}(trace_id=0x{format_span_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote}, synthetic={self.synthetic})" diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 2d1d2def..a5bdf895 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -6,135 +6,178 @@ import re import time import traceback +from contextlib import contextmanager +from typing import TYPE_CHECKING, Iterator, Mapping, Optional, Type, Union + +from opentelemetry.context.context import Context +from opentelemetry.trace import ( + SpanKind, + TraceFlags, + Tracer, + TracerProvider, + _Links, + use_span, +) +from opentelemetry.util import types + +from instana.agent.host import HostAgent +from instana.log import logger +from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.exceptions import UnsupportedFormatException +from instana.propagators.format import Format +from instana.propagators.http_propagator import HTTPPropagator +from instana.propagators.text_propagator import TextPropagator +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler, Sampler +from instana.span.kind import EXIT_SPANS +from instana.span.span import InstanaSpan, get_current_span +from instana.span_context import SpanContext +from instana.util.ids import generate_id + +if TYPE_CHECKING: + from instana.agent.base import BaseAgent + from instana.propagators.base_propagator import BasePropagator, CarrierT + + +class InstanaTracerProvider(TracerProvider): + def __init__( + self, + sampler: Optional[Sampler] = None, + span_processor: Optional[StanRecorder] = None, + exporter: Optional[Type["BaseAgent"]] = None, + ) -> None: + self.sampler = sampler or InstanaSampler() + self._span_processor = span_processor or StanRecorder() + self._exporter = exporter or HostAgent() + self._propagators = {} + self._propagators[Format.HTTP_HEADERS] = HTTPPropagator() + self._propagators[Format.TEXT_MAP] = TextPropagator() + self._propagators[Format.BINARY] = BinaryPropagator() + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: Optional[str] = None, + schema_url: Optional[str] = None, + attributes: Optional[types.Attributes] = None, + ) -> Tracer: + if not instrumenting_module_name: # Reject empty strings too. + instrumenting_module_name = "" + logger.error("get_tracer called with missing module name.") + + return InstanaTracer( + self.sampler, + self._span_processor, + self._exporter, + self._propagators, + ) -import opentracing as ot -from basictracer import BasicTracer - -from .util.ids import generate_id -from .span_context import SpanContext -from .span import InstanaSpan, RegisteredSpan -from .recorder import StanRecorder, InstanaSampler -from .propagators.http_propagator import HTTPPropagator -from .propagators.text_propagator import TextPropagator -from .propagators.binary_propagator import BinaryPropagator - - -class InstanaTracer(BasicTracer): - def __init__(self, scope_manager=None, recorder=None): - - if recorder is None: - recorder = StanRecorder() + def add_span_processor( + self, + span_processor: StanRecorder, + ) -> None: + """Registers a new SpanProcessor for the TracerProvider.""" + self._span_processor = span_processor + + +class InstanaTracer(Tracer): + """Handles :class:`InstanaSpan` creation and in-process context propagation. + + This class provides methods for manipulating the context, creating spans, + and controlling spans' lifecycles. + """ + + def __init__( + self, + sampler: Sampler, + span_processor: StanRecorder, + exporter: Type["BaseAgent"], + propagators: Mapping[str, Type["BasePropagator"]], + ) -> None: + self._sampler = sampler + self._span_processor = span_processor + self._exporter = exporter + self._propagators = propagators + + @property + def span_processor(self) -> Optional[StanRecorder]: + return self._span_processor + + @property + def exporter(self) -> Optional[Type["BaseAgent"]]: + return self._exporter + + def start_span( + self, + name: str, + span_context: Optional[SpanContext] = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: _Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + ) -> InstanaSpan: + parent_context = span_context if span_context else get_current_span().get_span_context() + + if parent_context and not isinstance(parent_context, SpanContext): + raise TypeError("parent_context must be an Instana SpanContext or None.") + + if parent_context and not parent_context.is_valid and not parent_context.suppression: + # We probably have an INVALID_SPAN_CONTEXT. + parent_context = None + + span_context = self._create_span_context(parent_context) + span = InstanaSpan( + name, + span_context, + self._span_processor, + parent_id=(None if parent_context is None else parent_context.span_id), + start_time=(time.time_ns() if start_time is None else start_time), + attributes=attributes, + # events: Sequence[Event] = None, + ) - super(InstanaTracer, self).__init__( - recorder, InstanaSampler(), scope_manager) + if parent_context is not None: + span.synthetic = parent_context.synthetic - self._propagators[ot.Format.HTTP_HEADERS] = HTTPPropagator() - self._propagators[ot.Format.TEXT_MAP] = TextPropagator() - self._propagators[ot.Format.BINARY] = BinaryPropagator() + if name in EXIT_SPANS: + self._add_stack(span) - def start_active_span(self, - operation_name, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - finish_on_close=True): + return span - # create a new Span + @contextmanager + def start_as_current_span( + self, + name: str, + span_context: Optional[SpanContext] = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: _Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + end_on_exit: bool = True, + ) -> Iterator[InstanaSpan]: span = self.start_span( - operation_name=operation_name, - child_of=child_of, - references=references, - tags=tags, + name=name, + span_context=span_context, + kind=kind, + attributes=attributes, + links=links, start_time=start_time, - ignore_active_span=ignore_active_span, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, ) - - return self.scope_manager.activate(span, finish_on_close) - - def start_span(self, - operation_name=None, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False): - "Taken from BasicTracer so we can override generate_id calls to ours" - - start_time = time.time() if start_time is None else start_time - - # See if we have a parent_ctx in `references` - parent_ctx = None - if child_of is not None: - parent_ctx = ( - child_of if isinstance(child_of, SpanContext) - else child_of.context) - elif references is not None and len(references) > 0: - # TODO only the first reference is currently used - parent_ctx = references[0].referenced_context - - # retrieve the active SpanContext - if not ignore_active_span and parent_ctx is None: - scope = self.scope_manager.active - if scope is not None: - parent_ctx = scope.span.context - - # Assemble the child ctx - gid = generate_id() - ctx = SpanContext(span_id=gid) - if parent_ctx is not None and parent_ctx.trace_id is not None: - if hasattr(parent_ctx, '_baggage') and parent_ctx._baggage is not None: - ctx._baggage = parent_ctx._baggage.copy() - ctx.trace_id = parent_ctx.trace_id - ctx.sampled = parent_ctx.sampled - ctx.long_trace_id = parent_ctx.long_trace_id - ctx.trace_parent = parent_ctx.trace_parent - ctx.instana_ancestor = parent_ctx.instana_ancestor - ctx.level = parent_ctx.level - ctx.correlation_type = parent_ctx.correlation_type - ctx.correlation_id = parent_ctx.correlation_id - ctx.traceparent = parent_ctx.traceparent - ctx.tracestate = parent_ctx.tracestate - else: - ctx.trace_id = gid - ctx.sampled = self.sampler.sampled(ctx.trace_id) - if parent_ctx is not None: - ctx.level = parent_ctx.level - ctx.correlation_type = parent_ctx.correlation_type - ctx.correlation_id = parent_ctx.correlation_id - ctx.traceparent = parent_ctx.traceparent - ctx.tracestate = parent_ctx.tracestate - - # Tie it all together - span = InstanaSpan(self, - operation_name=operation_name, - context=ctx, - parent_id=(None if parent_ctx is None else parent_ctx.span_id), - tags=tags, - start_time=start_time) - - if parent_ctx is not None: - span.synthetic = parent_ctx.synthetic - - if operation_name in RegisteredSpan.EXIT_SPANS: - self.__add_stack(span) - - return span - - def inject(self, span_context, format, carrier, disable_w3c_trace_context=False): - if format in self._propagators: - return self._propagators[format].inject(span_context, carrier, disable_w3c_trace_context) - - raise ot.UnsupportedFormatException() - - def extract(self, format, carrier, disable_w3c_trace_context=False): - if format in self._propagators: - return self._propagators[format].extract(carrier, disable_w3c_trace_context) - - raise ot.UnsupportedFormatException() - - def __add_stack(self, span, limit=30): + with use_span( + span, + end_on_exit=end_on_exit, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) as span: + yield span + + def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: """ Adds a backtrace to . The default length limit for stack traces is 30 frames. A hard limit of 40 frames is enforced. @@ -155,23 +198,77 @@ def __add_stack(self, span, limit=30): if re_with_stan_frame.search(frame[2]) is not None: continue - sanitized_stack.append({ - "c": frame[0], - "n": frame[1], - "m": frame[2] - }) + sanitized_stack.append({"c": frame[0], "n": frame[1], "m": frame[2]}) if len(sanitized_stack) > limit: # (limit * -1) gives us negative form of used for # slicing from the end of the list. e.g. stack[-30:] - span.stack = sanitized_stack[(limit*-1):] + span.stack = sanitized_stack[(limit * -1) :] else: span.stack = sanitized_stack except Exception: # No fail pass + def _create_span_context(self, parent_context: SpanContext) -> SpanContext: + """Creates a new SpanContext based on the given parent context.""" + + if parent_context and parent_context.is_valid: + trace_id = parent_context.trace_id + span_id = generate_id() + trace_flags = parent_context.trace_flags + is_remote = parent_context.is_remote + else: + trace_id = span_id = generate_id() + trace_flags = TraceFlags(self._sampler.sampled()) + is_remote = False + + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + trace_flags=trace_flags, + is_remote=is_remote, + level=(parent_context.level if parent_context else 1), + synthetic=(parent_context.synthetic if parent_context else False), + ) + + if parent_context is not None: + span_context.long_trace_id = parent_context.long_trace_id + span_context.trace_parent = parent_context.trace_parent + span_context.instana_ancestor = parent_context.instana_ancestor + span_context.correlation_type = parent_context.correlation_type + span_context.correlation_id = parent_context.correlation_id + span_context.traceparent = parent_context.traceparent + span_context.tracestate = parent_context.tracestate + + return span_context + + def inject( + self, + span_context: SpanContext, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: "CarrierT", + disable_w3c_trace_context: bool = False, + ) -> Optional["CarrierT"]: + if format in self._propagators: + return self._propagators[format].inject( + span_context, carrier, disable_w3c_trace_context + ) + + raise UnsupportedFormatException() + + def extract( + self, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: "CarrierT", + disable_w3c_trace_context: bool = False, + ) -> Optional[Context]: + if format in self._propagators: + return self._propagators[format].extract(carrier, disable_w3c_trace_context) + + raise UnsupportedFormatException() + # Used by __add_stack re_tracer_frame = re.compile(r"/instana/.*\.py$") -re_with_stan_frame = re.compile('with_instana') +re_with_stan_frame = re.compile("with_instana") diff --git a/src/instana/util/__init__.py b/src/instana/util/__init__.py index 97de4f3f..bd991126 100644 --- a/src/instana/util/__init__.py +++ b/src/instana/util/__init__.py @@ -10,9 +10,11 @@ from ..log import logger + def nested_dictionary(): return defaultdict(DictionaryOfStan) + # Simple implementation of a nested dictionary. DictionaryOfStan = nested_dictionary @@ -26,17 +28,21 @@ def to_json(obj): :return: json string """ try: + def extractor(o): - if not hasattr(o, '__dict__'): + if not hasattr(o, "__dict__"): logger.debug("Couldn't serialize non dict type: %s", type(o)) return {} else: return {k.lower(): v for k, v in o.__dict__.items() if v is not None} - return json.dumps(obj, default=extractor, sort_keys=False, separators=(',', ':')).encode() + return json.dumps( + obj, default=extractor, sort_keys=False, separators=(",", ":") + ).encode() except Exception: logger.debug("to_json non-fatal encoding issue: ", exc_info=True) + def to_pretty_json(obj): """ Convert obj to pretty json. Used mostly in logging/debugging. @@ -45,14 +51,17 @@ def to_pretty_json(obj): :return: json string """ try: + def extractor(o): - if not hasattr(o, '__dict__'): + if not hasattr(o, "__dict__"): logger.debug("Couldn't serialize non dict type: %s", type(o)) return {} else: return {k.lower(): v for k, v in o.__dict__.items() if v is not None} - return json.dumps(obj, default=extractor, sort_keys=True, indent=4, separators=(',', ':')) + return json.dumps( + obj, default=extractor, sort_keys=True, indent=4, separators=(",", ":") + ) except Exception: logger.debug("to_pretty_json non-fatal encoding issue: ", exc_info=True) @@ -65,9 +74,9 @@ def package_version(): """ version = "" try: - version = importlib.metadata.version('instana') + version = importlib.metadata.version("instana") except importlib.metadata.PackageNotFoundError: - version = 'unknown' + version = "unknown" return version @@ -85,13 +94,18 @@ def get_default_gateway(): # The Gateway IP is encoded backwards in hex. with open("/proc/self/net/route") as routes: for line in routes: - parts = line.split('\t') - if parts[1] == '00000000': + parts = line.split("\t") + if parts[1] == "00000000": hip = parts[2] if hip is not None and len(hip) == 8: # Reverse order, convert hex to int - return "%i.%i.%i.%i" % (int(hip[6:8], 16), int(hip[4:6], 16), int(hip[2:4], 16), int(hip[0:2], 16)) + return "%i.%i.%i.%i" % ( + int(hip[6:8], 16), + int(hip[4:6], 16), + int(hip[2:4], 16), + int(hip[0:2], 16), + ) except Exception: logger.warning("get_default_gateway: ", exc_info=True) @@ -113,7 +127,9 @@ def every(delay, task, name): if task() is False: break except Exception: - logger.debug("Problem while executing repetitive task: %s", name, exc_info=True) + logger.debug( + "Problem while executing repetitive task: %s", name, exc_info=True + ) # skip tasks if we are behind schedule: next_time += (time.time() - next_time) // delay * delay + delay diff --git a/src/instana/util/ids.py b/src/instana/util/ids.py index 3d6e8d01..81792027 100644 --- a/src/instana/util/ids.py +++ b/src/instana/util/ids.py @@ -4,30 +4,32 @@ import os import time import random +from typing import Union + +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID _rnd = random.Random() _current_pid = 0 -BAD_ID = "BADCAFFE" # Bad Caffe +def generate_id() -> int: + """Get a new ID. -def generate_id(): - """ Generate a 64bit base 16 ID for use as a Span or Trace ID """ + Returns: + A 64-bit int for use as a Span or Trace ID. + """ global _current_pid pid = os.getpid() if _current_pid != pid: _current_pid = pid _rnd.seed(int(1000000 * time.time()) ^ pid) - new_id = format(_rnd.randint(0, 18446744073709551615), '02x') - - if len(new_id) < 16: - new_id = new_id.zfill(16) + new_id = _rnd.randint(0, _SPAN_ID_MAX_VALUE) return new_id -def header_to_long_id(header): +def header_to_long_id(header: Union[bytes, str]) -> int: """ We can receive headers in the following formats: 1. unsigned base 16 hex string (or bytes) of variable length @@ -40,23 +42,19 @@ def header_to_long_id(header): header = header.decode('utf-8') if not isinstance(header, str): - return BAD_ID + return INVALID_SPAN_ID try: - # Test that header is truly a hexadecimal value before we try to convert - int(header, 16) - - length = len(header) - if length < 16: + if len(header) < 16: # Left pad ID with zeros header = header.zfill(16) - return header + return int(header, 16) except ValueError: - return BAD_ID + return INVALID_SPAN_ID -def header_to_id(header): +def header_to_id(header: Union[bytes, str]) -> int: """ We can receive headers in the following formats: 1. unsigned base 16 hex string (or bytes) of variable length @@ -69,12 +67,9 @@ def header_to_id(header): header = header.decode('utf-8') if not isinstance(header, str): - return BAD_ID + return INVALID_SPAN_ID try: - # Test that header is truly a hexadecimal value before we try to convert - int(header, 16) - length = len(header) if length < 16: # Left pad ID with zeros @@ -82,6 +77,7 @@ def header_to_id(header): elif length > 16: # Phase 0: Discard everything but the last 16byte header = header[-16:] - return header + + return int(header, 16) except ValueError: - return BAD_ID + return INVALID_SPAN_ID diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index a5b33304..06b821ca 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -1,45 +1,51 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -from ..singletons import agent, tracer, async_tracer, tornado_tracer -from ..log import logger +from typing import Optional, Tuple +from instana.log import logger +from instana.singletons import agent, tracer +from instana.span.span import InstanaSpan, get_current_span +from instana.tracer import InstanaTracer -def extract_custom_headers(tracing_span, headers): + +def extract_custom_headers(tracing_span, headers) -> None: 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_span.set_tag("http.header.%s" % custom_header, value) + tracing_span.set_attribute(f"http.header.{custom_header}", value) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) -def get_active_tracer(): +def get_active_tracer() -> Optional[InstanaTracer]: try: - if tracer.active_span: - return tracer - elif async_tracer.active_span: - return async_tracer - elif tornado_tracer.active_span: - return tornado_tracer - else: + current_span = get_current_span() + if current_span: + # asyncio Spans are used as NonRecording Spans solely for context propagation + if current_span.is_recording() or current_span.name == "asyncio": + return tracer return None + return None except Exception: # Do not try to log this with instana, as there is no active tracer and there will be an infinite loop at least # for PY2 return None -def get_tracer_tuple(): +def get_tracer_tuple() -> ( + Tuple[Optional[InstanaTracer], Optional[InstanaSpan], Optional[str]] +): active_tracer = get_active_tracer() + current_span = get_current_span() if active_tracer: - return (active_tracer, active_tracer.active_span, active_tracer.active_span.operation_name) + return (active_tracer, current_span, current_span.name) elif agent.options.allow_exit_as_root: return (tracer, None, None) return (None, None, None) -def tracing_is_off(): +def tracing_is_off() -> bool: return not (bool(get_active_tracer()) or agent.options.allow_exit_as_root) diff --git a/src/instana/version.py b/src/instana/version.py index 36a74f5b..ee874de0 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "2.5.2" +VERSION = "3.0.0.dev0" diff --git a/src/instana/w3c_trace_context/traceparent.py b/src/instana/w3c_trace_context/traceparent.py index 3175c6cf..bc39fd3a 100644 --- a/src/instana/w3c_trace_context/traceparent.py +++ b/src/instana/w3c_trace_context/traceparent.py @@ -3,6 +3,7 @@ from ..log import logger import re +from typing import Optional # See https://www.w3.org/TR/trace-context-2/#trace-flags for details on the bitmasks. SAMPLED_BITMASK = 0b1; @@ -45,7 +46,13 @@ def get_traceparent_fields(traceparent): logger.debug("Parsing the traceparent failed: {}".format(err)) return None, None, None, None - def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): + def update_traceparent( + self, + traceparent: Optional[str], + in_trace_id: int, + in_span_id: int, + level: int, + ) -> str: """ This method updates the traceparent header or generates one if there was no traceparent incoming header or it was invalid @@ -56,7 +63,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): :return: the updated traceparent header """ if traceparent is None: # modify the trace_id part only when it was not present at all - trace_id = in_trace_id.zfill(32) + trace_id = ( + in_trace_id.zfill(32) + if not isinstance(in_trace_id, int) + else in_trace_id + ) else: # - We do not need the incoming upstream parent span ID for the header we sent downstream. # - We also do not care about the incoming version: The version field we sent downstream needs to match the @@ -67,12 +78,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): # downstream. _, trace_id, _, _ = self.get_traceparent_fields(traceparent) - parent_id = in_span_id.zfill(16) + parent_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) flags = level & SAMPLED_BITMASK flags = format(flags, '0>2x') - traceparent = "{version}-{traceid}-{parentid}-{flags}".format(version=self.SPECIFICATION_VERSION, - traceid=trace_id, - parentid=parent_id, - flags=flags) + traceparent = f"{self.SPECIFICATION_VERSION}-{trace_id}-{parent_id}-{flags}" return traceparent diff --git a/src/instana/w3c_trace_context/tracestate.py b/src/instana/w3c_trace_context/tracestate.py index b1d066ea..f6eb32cb 100644 --- a/src/instana/w3c_trace_context/tracestate.py +++ b/src/instana/w3c_trace_context/tracestate.py @@ -42,8 +42,10 @@ def update_tracestate(self, tracestate, in_trace_id, in_span_id): :return: tracestate updated """ try: - span_id = in_span_id.zfill(16) # if span_id is shorter than 16 characters we prepend zeros - instana_tracestate = "in={};{}".format(in_trace_id, span_id) + span_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) + instana_tracestate = f"in={in_trace_id};{span_id}" if tracestate is None or tracestate == "": tracestate = instana_tracestate else: diff --git a/tests/__init__.py b/tests/__init__.py index 81660d27..39799ddb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,8 +3,6 @@ import os -os.environ["INSTANA_TEST"] = "true" - if os.environ.get('GEVENT_STARLETTE_TEST'): from gevent import monkey monkey.patch_all() diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py new file mode 100644 index 00000000..b345ce28 --- /dev/null +++ b/tests/agent/test_host.py @@ -0,0 +1,310 @@ +import datetime +import json +import logging +import os + +from unittest.mock import Mock, patch + +import pytest +import requests +from instana.agent.host import AnnounceData, HostAgent +from instana.collector.host import HostCollector +from instana.fsm import TheMachine +from instana.options import StandardOptions +from instana.recorder import StanRecorder +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from pytest import LogCaptureFixture + + +def test_init(): + with patch( + "instana.agent.base.BaseAgent.update_log_level" + ) as mock_update, patch.object(os, "getpid", return_value=12345): + agent = HostAgent() + assert not agent.announce_data + assert not agent.last_seen + assert not agent.last_fork_check + assert agent._boot_pid == 12345 + + mock_update.assert_called_once() + + assert isinstance(agent.options, StandardOptions) + assert isinstance(agent.collector, HostCollector) + assert isinstance(agent.machine, TheMachine) + + +def test_start(): + with patch("instana.collector.host.HostCollector.start") as mock_start: + agent = HostAgent() + agent.start() + mock_start.assert_called_once() + + +def test_handle_fork(): + with patch.object(HostAgent, "reset") as mock_reset: + agent = HostAgent() + agent.handle_fork() + mock_reset.assert_called_once() + + +def test_reset(): + with patch("instana.collector.host.HostCollector.shutdown") as mock_shutdown, patch( + "instana.fsm.TheMachine.reset" + ) as mock_reset: + agent = HostAgent() + agent.reset() + + assert not agent.last_seen + assert not agent.announce_data + + mock_shutdown.assert_called_once_with(report_final=False) + mock_reset.assert_called_once() + + +def test_is_timed_out(): + agent = HostAgent() + assert not agent.is_timed_out() + + agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) + agent.can_send = True + assert agent.is_timed_out() + + +@pytest.mark.original +def test_can_send(): + agent = HostAgent() + agent._boot_pid = 12345 + with patch.object(os, "getpid", return_value=12344), patch( + "instana.agent.host.HostAgent.handle_fork" + ) as mock_handle, patch.dict("os.environ", {}, clear=True): + agent.can_send() + assert agent._boot_pid == 12344 + mock_handle.assert_called_once() + + with patch.object(agent.machine.fsm, "current", "wait4init"): + assert agent.can_send() is True + + +@pytest.mark.original +def test_can_send_default(): + agent = HostAgent() + with patch.dict("os.environ", {}, clear=True): + assert not agent.can_send() + + +def test_set_from(): + agent = HostAgent() + sample_res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + "extraHeaders": ["value-3"], + "agentUuid": "value-4", + "pid": 1234, + } + agent.options.extra_http_headers = None + + agent.set_from(sample_res_data) + assert agent.options.secrets_matcher == "value-1" + assert agent.options.secrets_list == ["value-2"] + assert agent.options.extra_http_headers == ["value-3"] + + agent.options.extra_http_headers = ["value"] + agent.set_from(sample_res_data) + assert "value" in agent.options.extra_http_headers + + assert agent.announce_data.agentUuid == "value-4" + assert agent.announce_data.pid == 1234 + + +@pytest.mark.original +def test_get_from_structure(): + agent = HostAgent() + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + assert agent.get_from_structure() == {"e": 1234, "h": "value"} + + +def test_is_agent_listening( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + with patch.object(requests.Session, "get", return_value=mock_response): + assert agent.is_agent_listening("sample", 1234) + + mock_response.status_code = 404 + with patch.object(requests.Session, "get", return_value=mock_response, clear=True): + assert not agent.is_agent_listening("sample", 1234) + + host = "localhost" + port = 123 + with patch.object(requests.Session, "get", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_listening(host, port) + assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages + + +def test_announce( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + response = json.loads(mock_response.content) + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = mock_response.content.encode("UTF-8") + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + + with patch.object(requests.Session, "put", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert f"announce: connection error ({type(Exception())})" in caplog.messages + + mock_response.content = json.dumps("key") + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert "announce: response payload has no fields: (key)" in caplog.messages + + mock_response.content = json.dumps({"key": "value"}) + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no pid: ({'key': 'value'})" + in caplog.messages + ) + + mock_response.content = json.dumps({"pid": "value"}) + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no agentUuid: ({'pid': 'value'})" + in caplog.messages + ) + + mock_response.status_code = 404 + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + assert not agent.announce("sample-data") + assert "announce: response status code (404) is NOT 200" in caplog.messages + + +def test_log_message_to_host_agent( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "sample" + mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.datetime" + ) as mock_date: + mock_date.now.return_value = mock_datetime + mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + agent.log_message_to_host_agent("sample") + assert agent.last_seen == mock_datetime + + with patch.object(requests.Session, "post", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.log_message_to_host_agent("sample") + assert ( + f"agent logging: connection error ({type(Exception())})" + in caplog.messages + ) + + +def test_is_agent_ready(caplog: LogCaptureFixture): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = {"key": "value"} + agent.AGENT_DATA_PATH = "sample_path" + agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") + with patch.object(requests.Session, "head", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" + ): + assert agent.is_agent_ready() + with patch.object(requests.Session, "head", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_ready() + assert ( + f"is_agent_ready: connection error ({type(Exception())})" + in caplog.messages + ) + + +def test_report_data_payload( + span_context: SpanContext, + span_processor: StanRecorder, +): + agent = HostAgent() + span_name = "test-span" + span_1 = InstanaSpan(span_name, span_context, span_processor) + span_2 = InstanaSpan(span_name, span_context, span_processor) + payload = { + "spans": [span_1, span_2], + "profiles": ["profile-1", "profile-2"], + "metrics": { + "plugins": [ + {"data": "sample data"}, + ] + }, + } + sample_response = {"key": "value"} + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = sample_response + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__traces_url", return_value="localhost" + ), patch( + "instana.agent.host.HostAgent._HostAgent__profiles_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" + ): + test_response = agent.report_data_payload(payload) + assert isinstance(agent.last_seen, datetime.datetime) + assert test_response.content == sample_response + + +def test_diagnostics(caplog: LogCaptureFixture): + caplog.set_level(logging.WARNING, logger="instana") + + agent = HostAgent() + agent.diagnostics() + assert "====> Instana Python Language Agent Diagnostics <====" in caplog.messages + assert "----> Agent <----" in caplog.messages + assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages + assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages + assert "last_seen: None" in caplog.messages + + sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) + agent.last_seen = sample_date + agent.diagnostics() + assert "last_seen: 2022-07-25 14:30:00" in caplog.messages + assert "announce_data: None" in caplog.messages + + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + agent.diagnostics() + assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages + assert f"Options: {agent.options.__dict__}" in caplog.messages + assert "----> StateMachine <----" in caplog.messages + assert f"State: {agent.machine.fsm.current}" in caplog.messages + assert "----> Collector <----" in caplog.messages + assert f"Collector: {agent.collector}" in caplog.messages + assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages + assert "reporting_thread: None" in caplog.messages + assert f"report_interval: {agent.collector.report_interval}" in caplog.messages + assert "should_send_snapshot_data: True" in caplog.messages diff --git a/tests/platforms/test_eksfargate.py b/tests/agents/test_aws_eks_fargate.py similarity index 100% rename from tests/platforms/test_eksfargate.py rename to tests/agents/test_aws_eks_fargate.py diff --git a/tests/platforms/test_fargate.py b/tests/agents/test_aws_fargate.py similarity index 100% rename from tests/platforms/test_fargate.py rename to tests/agents/test_aws_fargate.py diff --git a/tests/platforms/test_lambda.py b/tests/agents/test_aws_lambda.py similarity index 100% rename from tests/platforms/test_lambda.py rename to tests/agents/test_aws_lambda.py diff --git a/tests/platforms/test_google_cloud_run.py b/tests/agents/test_google_cloud_run.py similarity index 100% rename from tests/platforms/test_google_cloud_run.py rename to tests/agents/test_google_cloud_run.py diff --git a/tests/agents/test_host.py b/tests/agents/test_host.py new file mode 100644 index 00000000..e8b8736c --- /dev/null +++ b/tests/agents/test_host.py @@ -0,0 +1,595 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import datetime +import json +import os +import logging +from unittest.mock import Mock + +from mock import MagicMock, patch +import pytest +import requests + +from instana.agent.host import AnnounceData, HostAgent +from instana.agent.test import TestAgent +from instana.collector.host import HostCollector +from instana.fsm import Discovery, TheMachine +from instana.log import logger +from instana.options import StandardOptions +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from instana.tracer import InstanaTracer + + +class TestHostAgent: + @pytest.fixture(autouse=True) + def _resource(self, caplog): + self.agent = None + self.span_recorder = None + self.tracer = None + self.original_agent = get_agent() + self.original_tracer = get_tracer() + yield + caplog.clear() + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_LOG_LEVEL", + "INSTANA_SERVICE_NAME", + "INSTANA_SECRETS", + "INSTANA_TAGS", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer( + sampler=InstanaSampler(), + span_processor=self.span_recorder, + exporter=TestAgent(), + propagators={}, + ) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_secrets(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "contains-ignore-case" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["key", "pass", "secret"] + + def test_options_have_extra_http_headers(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_has_options(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, StandardOptions) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_instana_debug(self): + os.environ["INSTANA_DEBUG"] = "asdf" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.DEBUG + + def test_agent_instana_service_name(self): + os.environ["INSTANA_SERVICE_NAME"] = "greycake" + self.create_agent_and_setup_tracer() + assert self.agent.options.service_name == "greycake" + + @patch.object(requests.Session, "put") + def test_announce_is_successful(self, mock_requests_session_put): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = ( + "{" f' "pid": {test_pid}, ' f' "agentUuid": "{test_agent_uuid}"' "}" + ) + + # This mocks the call to self.agent.client.put + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + + assert "pid" in payload + assert test_pid == payload["pid"] + + assert "agentUuid" in payload + assert test_agent_uuid == payload["agentUuid"] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_200(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 404 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response status code" in caplog.messages[0] + assert "is NOT 200" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_json(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response is not JSON" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_empty_list_json( + self, mock_requests_session_put, caplog + ): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "[]" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "payload has no fields" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_pid(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "agentUuid": "{test_agent_uuid}"' "}" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no pid" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_uuid(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "pid": {test_pid} ' "}" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no agentUuid" in caplog.messages[0] + + @patch.object(requests.Session, "get") + def test_agent_connection_attempt(self, mock_requests_session_get, caplog): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_requests_session_get.return_value = mock_response + + self.create_agent_and_setup_tracer() + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = f"Instana host agent found on {host}:{port}" + + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + result = self.agent.is_agent_listening(host, port) + + assert result + assert msg in caplog.messages[0] + + @patch.object(requests.Session, "get") + def test_agent_connection_attempt_fails_with_404( + self, mock_requests_session_get, caplog + ): + mock_response = MagicMock() + mock_response.status_code = 404 + mock_requests_session_get.return_value = mock_response + + self.create_agent_and_setup_tracer() + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = ( + "The attempt to connect to the Instana host agent on " + f"{host}:{port} has failed with an unexpected status code. " + f"Expected HTTP 200 but received: {mock_response.status_code}" + ) + + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + result = self.agent.is_agent_listening(host, port) + + assert not result + assert msg in caplog.messages[0] + + def test_init(self): + with patch( + "instana.agent.base.BaseAgent.update_log_level" + ) as mock_update, patch.object(os, "getpid", return_value=12345): + agent = HostAgent() + assert not agent.announce_data + assert not agent.last_seen + assert not agent.last_fork_check + assert agent._boot_pid == 12345 + + mock_update.assert_called_once() + + assert isinstance(agent.options, StandardOptions) + assert isinstance(agent.collector, HostCollector) + assert isinstance(agent.machine, TheMachine) + + def test_start( + self, + ): + with patch("instana.collector.host.HostCollector.start") as mock_start: + agent = HostAgent() + agent.start() + mock_start.assert_called_once() + + def test_handle_fork( + self, + ): + with patch.object(HostAgent, "reset") as mock_reset: + agent = HostAgent() + agent.handle_fork() + mock_reset.assert_called_once() + + def test_reset( + self, + ): + with patch( + "instana.collector.host.HostCollector.shutdown" + ) as mock_shutdown, patch("instana.fsm.TheMachine.reset") as mock_reset: + agent = HostAgent() + agent.reset() + + assert not agent.last_seen + assert not agent.announce_data + + mock_shutdown.assert_called_once_with(report_final=False) + mock_reset.assert_called_once() + + def test_is_timed_out( + self, + ): + agent = HostAgent() + assert not agent.is_timed_out() + + agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) + agent.can_send = True + assert agent.is_timed_out() + + def test_can_send_test_env( + self, + ): + agent = HostAgent() + with patch.dict("os.environ", {"INSTANA_TEST": "sample-data"}): + if "INSTANA_TEST" in os.environ: + assert agent.can_send() + + def test_can_send( + self, + ): + agent = HostAgent() + agent._boot_pid = 12345 + with patch.object(os, "getpid", return_value=12344), patch( + "instana.agent.host.HostAgent.handle_fork" + ) as mock_handle, patch.dict("os.environ", {}, clear=True): + agent.can_send() + assert agent._boot_pid == 12344 + mock_handle.assert_called_once() + + with patch.object(agent.machine.fsm, "current", "wait4init"): + assert agent.can_send() is True + + def test_can_send_default( + self, + ): + agent = HostAgent() + with patch.dict("os.environ", {}, clear=True): + assert not agent.can_send() + + def test_set_from( + self, + ): + agent = HostAgent() + sample_res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + "extraHeaders": ["value-3"], + "agentUuid": "value-4", + "pid": 1234, + } + agent.options.extra_http_headers = None + + agent.set_from(sample_res_data) + assert agent.options.secrets_matcher == "value-1" + assert agent.options.secrets_list == ["value-2"] + assert agent.options.extra_http_headers == ["value-3"] + + agent.options.extra_http_headers = ["value"] + agent.set_from(sample_res_data) + assert "value" in agent.options.extra_http_headers + + assert agent.announce_data.agentUuid == "value-4" + assert agent.announce_data.pid == 1234 + + def test_get_from_structure( + self, + ): + agent = HostAgent() + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + assert agent.get_from_structure() == {"e": 1234, "h": "value"} + + def test_is_agent_listening( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + with patch.object(requests.Session, "get", return_value=mock_response): + assert agent.is_agent_listening("sample", 1234) + + mock_response.status_code = 404 + with patch.object( + requests.Session, "get", return_value=mock_response, clear=True + ): + assert not agent.is_agent_listening("sample", 1234) + + host = "localhost" + port = 123 + with patch.object(requests.Session, "get", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_listening(host, port) + assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages + + def test_announce( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + response = json.loads(mock_response.content) + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = mock_response.content.encode("UTF-8") + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + + with patch.object(requests.Session, "put", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + f"announce: connection error ({type(Exception())})" in caplog.messages + ) + + mock_response.content = json.dumps("key") + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert "announce: response payload has no fields: (key)" in caplog.messages + + mock_response.content = json.dumps({"key": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no pid: ({'key': 'value'})" + in caplog.messages + ) + + mock_response.content = json.dumps({"pid": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no agentUuid: ({'pid': 'value'})" + in caplog.messages + ) + + mock_response.status_code = 404 + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + assert not agent.announce("sample-data") + assert "announce: response status code (404) is NOT 200" in caplog.messages + + def test_log_message_to_host_agent( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "sample" + mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.datetime" + ) as mock_date: + mock_date.now.return_value = mock_datetime + mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + agent.log_message_to_host_agent("sample") + assert agent.last_seen == mock_datetime + + with patch.object(requests.Session, "post", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.log_message_to_host_agent("sample") + assert ( + f"agent logging: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_is_agent_ready(self, caplog): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = {"key": "value"} + agent.AGENT_DATA_PATH = "sample_path" + agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") + with patch.object(requests.Session, "head", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + assert agent.is_agent_ready() + with patch.object(requests.Session, "head", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_ready() + assert ( + f"is_agent_ready: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_report_data_payload( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ): + agent = HostAgent() + span_name = "test-span" + span_1 = InstanaSpan(span_name, span_context, span_processor) + span_2 = InstanaSpan(span_name, span_context, span_processor) + payload = { + "spans": [span_1, span_2], + "profiles": ["profile-1", "profile-2"], + "metrics": { + "plugins": [ + {"data": "sample data"}, + ] + }, + } + sample_response = {"key": "value"} + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = sample_response + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__traces_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__profiles_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + test_response = agent.report_data_payload(payload) + assert isinstance(agent.last_seen, datetime.datetime) + assert test_response.content == sample_response + + def test_diagnostics(self, caplog): + caplog.set_level(logging.WARNING, logger="instana") + + agent = HostAgent() + agent.diagnostics() + assert ( + "====> Instana Python Language Agent Diagnostics <====" in caplog.messages + ) + assert "----> Agent <----" in caplog.messages + assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages + assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages + assert "last_seen: None" in caplog.messages + + sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) + agent.last_seen = sample_date + agent.diagnostics() + assert "last_seen: 2022-07-25 14:30:00" in caplog.messages + assert "announce_data: None" in caplog.messages + + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + agent.diagnostics() + assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages + assert f"Options: {agent.options.__dict__}" in caplog.messages + assert "----> StateMachine <----" in caplog.messages + assert f"State: {agent.machine.fsm.current}" in caplog.messages + assert "----> Collector <----" in caplog.messages + assert f"Collector: {agent.collector}" in caplog.messages + assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages + assert "reporting_thread: None" in caplog.messages + assert f"report_interval: {agent.collector.report_interval}" in caplog.messages + assert "should_send_snapshot_data: True" in caplog.messages diff --git a/tests/apps/aiohttp_app2/__init__.py b/tests/apps/aiohttp_app2/__init__.py new file mode 100644 index 00000000..e382343a --- /dev/null +++ b/tests/apps/aiohttp_app2/__init__.py @@ -0,0 +1,13 @@ +# (c) Copyright IBM Corp. 2024 + +import os +import sys +from tests.apps.aiohttp_app2.app import aiohttp_server as server +from tests.apps.utils import launch_background_thread + +APP_THREAD = None + +if not any((os.environ.get('GEVENT_STARLETTE_TEST'), + os.environ.get('CASSANDRA_TEST'), + sys.version_info < (3, 5, 3))): + APP_THREAD = launch_background_thread(server, "AIOHTTP") diff --git a/tests/apps/aiohttp_app2/app.py b/tests/apps/aiohttp_app2/app.py new file mode 100644 index 00000000..82b3d24c --- /dev/null +++ b/tests/apps/aiohttp_app2/app.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright IBM Corp. 2024 + +import asyncio + +from aiohttp import web + +from tests.helpers import testenv + +testenv["aiohttp_port"] = 10810 +testenv["aiohttp_server"] = f"http://127.0.0.1:{testenv['aiohttp_port']}" + + +def say_hello(request): + return web.Response(text="Hello, world") + + +@web.middleware +async def middleware1(request, handler): + print("Middleware 1 called") + response = await handler(request) + print("Middleware 1 finished") + return response + + +def aiohttp_server(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + app = web.Application(middlewares=[middleware1]) + app.add_routes([web.get("/", say_hello)]) + + runner = web.AppRunner(app) + loop.run_until_complete(runner.setup()) + site = web.TCPSite(runner, "127.0.0.1", testenv["aiohttp_port"]) + + loop.run_until_complete(site.start()) + loop.run_forever() diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index b8a3e58b..5e7227ac 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -7,137 +7,147 @@ import os import sys import time -import opentracing -import opentracing.ext.tags as ext + try: - from django.urls import re_path + from django.urls import re_path, include except ImportError: from django.conf.urls import url as re_path from django.http import HttpResponse, Http404 +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind + +from instana.singletons import tracer filepath, extension = os.path.splitext(__file__) -os.environ['DJANGO_SETTINGS_MODULE'] = os.path.basename(filepath) +os.environ["DJANGO_SETTINGS_MODULE"] = os.path.basename(filepath) sys.path.insert(0, os.path.dirname(os.path.abspath(filepath))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SECRET_KEY = '^(myu#*^5v-9o$i-%6vnlwvy^#7&hspj$m3lcq#b$@__@+zd@c' +SECRET_KEY = "^(myu#*^5v-9o$i-%6vnlwvy^#7&hspj$m3lcq#b$@__@+zd@c" DEBUG = True -ALLOWED_HOSTS = ['testserver', 'localhost'] +ALLOWED_HOSTS = ["testserver", "localhost"] INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'app_django' +ROOT_URLCONF = "app_django" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'app_django.wsgi.application' +WSGI_APPLICATION = "app_django.wsgi.application" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True -STATIC_URL = '/static/' +STATIC_URL = "/static/" def index(request): - return HttpResponse('Stan wuz here!') + return HttpResponse("Stan wuz here!") def cause_error(request): - raise Exception('This is a fake error: /cause-error') + raise Exception("This is a fake error: /cause-error") + + +def induce_exception(request): + raise Exception("This is a fake error: /induce-exception") def another(request): - return HttpResponse('Stan wuz here!') + return HttpResponse("Stan wuz here!") def not_found(request): - raise Http404('Nothing here') + raise Http404("Nothing here") def complex(request): - with opentracing.tracer.start_active_span('asteroid') as pscope: - pscope.span.set_tag(ext.COMPONENT, "Python simple example app") - pscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - pscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - pscope.span.set_tag(ext.HTTP_URL, "/python/simple/one") - pscope.span.set_tag(ext.HTTP_METHOD, "GET") - pscope.span.set_tag(ext.HTTP_STATUS_CODE, 200) - pscope.span.log_kv({"foo": "bar"}) - time.sleep(.2) - - with opentracing.tracer.start_active_span('spacedust', child_of=pscope.span) as cscope: - cscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_CLIENT) - cscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - cscope.span.set_tag(ext.HTTP_URL, "/python/simple/two") - cscope.span.set_tag(ext.HTTP_METHOD, "POST") - cscope.span.set_tag(ext.HTTP_STATUS_CODE, 204) - cscope.span.set_baggage_item("someBaggage", "someValue") - time.sleep(.1) - - return HttpResponse('Stan wuz here!') + with tracer.start_as_current_span("asteroid") as pspan: + pspan.set_attribute("component", "Python simple example app") + pspan.set_attribute("span.kind", SpanKind.CLIENT) + pspan.set_attribute("peer.hostname", "localhost") + pspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/one") + pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") + pspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) + pspan.add_event(name="complex_request", attributes={"foo": "bar"}) + time.sleep(0.2) + + with tracer.start_as_current_span("spacedust") as cspan: + cspan.set_attribute("span.kind", SpanKind.CLIENT) + cspan.set_attribute("peer.hostname", "localhost") + cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") + cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") + cspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 204) + time.sleep(0.1) + + return HttpResponse("Stan wuz here!") def response_with_headers(request): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } - return HttpResponse('Stan wuz here with headers!', headers=headers) + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} + return HttpResponse("Stan wuz here with headers!", headers=headers) + +extra_patterns = [ + re_path(r"^induce_exception$", induce_exception, name="induce_exception"), +] urlpatterns = [ - re_path(r'^$', index, name='index'), - re_path(r'^cause_error$', cause_error, name='cause_error'), - re_path(r'^another$', another), - re_path(r'^not_found$', not_found, name='not_found'), - re_path(r'^complex$', complex, name='complex'), - re_path(r'^response_with_headers$', response_with_headers, name='response_with_headers') + re_path(r"^$", index, name="index"), + re_path(r"^cause_error$", cause_error, name="cause_error"), + re_path(r"^another$", another), + re_path(r"^not_found$", not_found, name="not_found"), + re_path( + r"^response_with_headers$", response_with_headers, name="response_with_headers" + ), + re_path(r"^exception$", include(extra_patterns)), + re_path(r"^complex$", complex, name="complex"), ] diff --git a/tests/apps/bottle_app/__init__.py b/tests/apps/bottle_app/__init__.py new file mode 100644 index 00000000..44cdabb1 --- /dev/null +++ b/tests/apps/bottle_app/__init__.py @@ -0,0 +1,10 @@ +# (c) Copyright IBM Corp. 2024 + +import os +from tests.apps.bottle_app.app import bottle_server as server +from tests.apps.utils import launch_background_thread + +app_thread = None + +if not os.environ.get('CASSANDRA_TEST') and app_thread is None: + app_thread = launch_background_thread(server.serve_forever, "Bottle") \ No newline at end of file diff --git a/tests/apps/bottle_app/app.py b/tests/apps/bottle_app/app.py new file mode 100644 index 00000000..cd56c138 --- /dev/null +++ b/tests/apps/bottle_app/app.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright IBM Corp. 2024 + +import logging + +from wsgiref.simple_server import make_server +from bottle import default_app + +from tests.helpers import testenv +from instana.middleware import InstanaWSGIMiddleware + +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +testenv["wsgi_port"] = 10812 +testenv["wsgi_server"] = ("http://127.0.0.1:" + str(testenv["wsgi_port"])) + +app = default_app() + +@app.route("/") +def hello(): + return "

🐍 Hello Stan! 🦄

" + +# Wrap the application with the Instana WSGI Middleware +app = InstanaWSGIMiddleware(app) +bottle_server = make_server('127.0.0.1', testenv["wsgi_port"], app) + +if __name__ == "__main__": + bottle_server.request_queue_size = 20 + bottle_server.serve_forever() diff --git a/tests/apps/fastapi_app/app.py b/tests/apps/fastapi_app/app.py index 1666ecd8..eac3662e 100644 --- a/tests/apps/fastapi_app/app.py +++ b/tests/apps/fastapi_app/app.py @@ -1,14 +1,11 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -from ...helpers import testenv - from fastapi import FastAPI, HTTPException, Response -from fastapi.exceptions import RequestValidationError -from fastapi.responses import PlainTextResponse from fastapi.concurrency import run_in_threadpool +from fastapi.testclient import TestClient from starlette.exceptions import HTTPException as StarletteHTTPException -import requests +from instana.span.span import get_current_span fastapi_server = FastAPI() @@ -20,48 +17,65 @@ # async def validation_exception_handler(request, exc): # return PlainTextResponse(str(exc), status_code=400) + @fastapi_server.get("/") async def root(): return {"message": "Hello World"} + @fastapi_server.get("/users/{user_id}") async def user(user_id): return {"user": user_id} + @fastapi_server.get("/response_headers") async def response_headers(): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} return Response("Stan wuz here with headers!", headers=headers) + @fastapi_server.get("/400") async def four_zero_zero(): raise HTTPException(status_code=400, detail="400 response") + @fastapi_server.get("/404") async def four_zero_four(): raise HTTPException(status_code=404, detail="Item not found") + @fastapi_server.get("/500") async def five_hundred(): raise HTTPException(status_code=500, detail="500 response") + @fastapi_server.get("/starlette_exception") async def starlette_exception(): raise StarletteHTTPException(status_code=500, detail="500 response") + def trigger_outgoing_call(): - response = requests.get(testenv["fastapi_server"]+"/users/1") + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = get_current_span().get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + client = TestClient(fastapi_server, headers=headers) + response = client.get("/users/1") return response.json() + @fastapi_server.get("/non_async_simple") def non_async_complex_call(): response = trigger_outgoing_call() return response + @fastapi_server.get("/non_async_threadpool") def non_async_threadpool(): run_in_threadpool(trigger_outgoing_call) - return {"message": "non async functions executed on a thread pool can't be followed through thread boundaries"} \ No newline at end of file + return { + "message": "non async functions executed on a thread pool can't be followed through thread boundaries" + } diff --git a/tests/apps/fastapi_app/app2.py b/tests/apps/fastapi_app/app2.py new file mode 100644 index 00000000..8f9b7edd --- /dev/null +++ b/tests/apps/fastapi_app/app2.py @@ -0,0 +1,21 @@ +# (c) Copyright IBM Corp. 2024 + +from fastapi import FastAPI, HTTPException, Response +from fastapi.concurrency import run_in_threadpool +from fastapi.middleware import Middleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware + + +fastapi_server = FastAPI( + middleware=[ + Middleware( + TrustedHostMiddleware, + allowed_hosts=["*"], + ), + ], +) + + +@fastapi_server.get("/") +async def root(): + return {"message": "Hello World"} diff --git a/tests/apps/flask_app/app.py b/tests/apps/flask_app/app.py index d7042315..e49f8fa1 100755 --- a/tests/apps/flask_app/app.py +++ b/tests/apps/flask_app/app.py @@ -6,7 +6,9 @@ import os import logging -import opentracing.ext.tags as ext + +from opentelemetry.semconv.trace import SpanAttributes + from flask import jsonify, Response from wsgiref.simple_server import make_server from flask import Flask, redirect, render_template, render_template_string @@ -20,19 +22,18 @@ pass from tests.helpers import testenv -from instana.singletons import tracer logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) -testenv["wsgi_port"] = 10811 -testenv["wsgi_server"] = ("http://127.0.0.1:" + str(testenv["wsgi_port"])) +testenv["flask_port"] = 10811 +testenv["flask_server"] = ("http://127.0.0.1:" + str(testenv["flask_port"])) app = Flask(__name__) app.debug = False app.use_reloader = False -flask_server = make_server('127.0.0.1', testenv["wsgi_port"], app.wsgi_app) +flask_server = make_server('127.0.0.1', testenv["flask_port"], app.wsgi_app) class InvalidUsage(Exception): @@ -77,28 +78,6 @@ def username_hello(username): return u"

🐍 Hello %s! 🦄

" % username -@app.route("/complex") -def gen_opentracing(): - with tracer.start_active_span('asteroid') as pscope: - pscope.span.set_tag(ext.COMPONENT, "Python simple example app") - pscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - pscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - pscope.span.set_tag(ext.HTTP_URL, "/python/simple/one") - pscope.span.set_tag(ext.HTTP_METHOD, "GET") - pscope.span.set_tag(ext.HTTP_STATUS_CODE, 200) - pscope.span.log_kv({"foo": "bar"}) - - with tracer.start_active_span('spacedust', child_of=pscope.span) as cscope: - cscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_CLIENT) - cscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - cscope.span.set_tag(ext.HTTP_URL, "/python/simple/two") - cscope.span.set_tag(ext.HTTP_METHOD, "POST") - cscope.span.set_tag(ext.HTTP_STATUS_CODE, 204) - cscope.span.set_baggage_item("someBaggage", "someValue") - - return "

🐍 Generated some OT spans... 🦄

" - - @app.route("/301") def threehundredone(): return redirect('/', code=301) @@ -139,6 +118,11 @@ def exception(): raise Exception('fake error') +@app.route("/got_request_exception") +def got_request_exception(): + raise RuntimeError() + + @app.route("/exception-invalid-usage") def exception_invalid_usage(): raise InvalidUsage("Simulated custom exception", status_code=502) diff --git a/tests/apps/grpc_server/stan_server.py b/tests/apps/grpc_server/stan_server.py index 60c446f4..e69de2a6 100644 --- a/tests/apps/grpc_server/stan_server.py +++ b/tests/apps/grpc_server/stan_server.py @@ -92,7 +92,6 @@ def start_server(self): if __name__ == "__main__": print ("Booting foreground GRPC application...") - # os.environ["INSTANA_TEST"] = "true" if sys.version_info >= (3, 5, 3): StanServicer().start_server() 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/apps/sanic_app/__init__.py b/tests/apps/sanic_app/__init__.py deleted file mode 100644 index a9daa911..00000000 --- a/tests/apps/sanic_app/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# (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 = [ - "X-Capture-This", - "X-Capture-That", - "X-Capture-This-Too", - "X-Capture-That-Too", - ] - - 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 index 055f5189..0838d29a 100644 --- a/tests/apps/sanic_app/name.py +++ b/tests/apps/sanic_app/name.py @@ -7,8 +7,5 @@ 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 index 9c290f38..a07dafc9 100644 --- a/tests/apps/sanic_app/server.py +++ b/tests/apps/sanic_app/server.py @@ -10,32 +10,35 @@ from tests.apps.sanic_app.simpleview import SimpleView from tests.apps.sanic_app.name import NameView -app = Sanic('test') +app = Sanic("test") + @app.get("/foo/") async def uuid_handler(request, foo_id: int): return text("INT - {}".format(foo_id)) + @app.route("/response_headers") async def response_headers(request): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} return text("Stan wuz here with headers!", headers=headers) + @app.route("/test_request_args") -async def test_request_args(request): +async def test_request_args_500(request): raise SanicException("Something went wrong.", status_code=500) + @app.route("/instana_exception") -async def test_request_args(request): +async def test_instana_exception(request): raise SanicException(description="Something went wrong.", status_code=500) + @app.route("/wrong") -async def test_request_args(request): +async def test_request_args_400(request): raise SanicException(message="Something went wrong.", status_code=400) + @app.get("/tag/") async def tag_handler(request, tag): return text("Tag - {}".format(tag)) @@ -45,8 +48,5 @@ async def tag_handler(request, tag): app.add_route(NameView.as_view(), "/") -if __name__ == '__main__': +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 index 646a310d..8529ecdd 100644 --- a/tests/apps/sanic_app/simpleview.py +++ b/tests/apps/sanic_app/simpleview.py @@ -5,20 +5,20 @@ from sanic.views import HTTPMethodView from sanic.response import text -class SimpleView(HTTPMethodView): - def get(self, request): - return text("I am get method") +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") + # 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 put(self, request): + return text("I am put method") - def patch(self, request): - return text("I am patch method") + def patch(self, request): + return text("I am patch method") - def delete(self, request): - return text("I am delete method") + def delete(self, request): + return text("I am delete method") diff --git a/tests/apps/starlette_app/__init__.py b/tests/apps/starlette_app/__init__.py index 2b7653a4..6b46de1c 100644 --- a/tests/apps/starlette_app/__init__.py +++ b/tests/apps/starlette_app/__init__.py @@ -2,17 +2,28 @@ # (c) Copyright Instana Inc. 2020 import uvicorn -from ...helpers import testenv -from instana.log import logger +from tests.helpers import testenv + +testenv["starlette_host"] = "127.0.0.1" testenv["starlette_port"] = 10817 -testenv["starlette_server"] = ("http://127.0.0.1:" + str(testenv["starlette_port"])) +testenv["starlette_server"] = "http://" + testenv["starlette_host"] + ":" + str(testenv["starlette_port"]) + + def launch_starlette(): from .app import starlette_server 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'] + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + ] - uvicorn.run(starlette_server, host='127.0.0.1', port=testenv['starlette_port'], log_level="critical") + uvicorn.run( + starlette_server, + host=testenv["starlette_host"], + port=testenv["starlette_port"], + log_level="critical", + ) diff --git a/tests/apps/starlette_app/app.py b/tests/apps/starlette_app/app.py index b7fdfec3..04878c12 100644 --- a/tests/apps/starlette_app/app.py +++ b/tests/apps/starlette_app/app.py @@ -1,35 +1,40 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import os + from starlette.applications import Starlette from starlette.responses import PlainTextResponse -from starlette.routing import Route, Mount, WebSocketRoute +from starlette.routing import Mount, Route, WebSocketRoute from starlette.staticfiles import StaticFiles -import os dir_path = os.path.dirname(os.path.realpath(__file__)) + def homepage(request): - return PlainTextResponse('Hello, world!') + return PlainTextResponse("Hello, world!") + def user(request): - user_id = request.path_params['user_id'] - return PlainTextResponse('Hello, user id %s!' % user_id) + user_id = request.path_params["user_id"] + return PlainTextResponse("Hello, user id %s!" % user_id) + async def websocket_endpoint(websocket): await websocket.accept() - await websocket.send_text('Hello, websocket!') + await websocket.send_text("Hello, websocket!") await websocket.close() + def startup(): - print('Ready to go') + print("Ready to go") routes = [ - Route('/', homepage), - Route('/users/{user_id}', user), - WebSocketRoute('/ws', websocket_endpoint), - Mount('/static', StaticFiles(directory=dir_path + "/static")), + Route("/", homepage), + Route("/users/{user_id}", user), + WebSocketRoute("/ws", websocket_endpoint), + Mount("/static", StaticFiles(directory=dir_path + "/static")), ] -starlette_server = Starlette(debug=True, routes=routes, on_startup=[startup]) \ No newline at end of file +starlette_server = Starlette(debug=True, routes=routes, on_startup=[startup]) diff --git a/tests/apps/starlette_app/app2.py b/tests/apps/starlette_app/app2.py new file mode 100644 index 00000000..c3be2242 --- /dev/null +++ b/tests/apps/starlette_app/app2.py @@ -0,0 +1,41 @@ +# (c) Copyright IBM Corp. 2024 + +import os + +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.trustedhost import TrustedHostMiddleware +from starlette.responses import PlainTextResponse +from starlette.routing import Route + +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +def homepage(request): + return PlainTextResponse("Hello, world!") + + +def five_hundred(request): + return PlainTextResponse("Something went wrong!", status_code=500) + + +def startup(): + print("Ready to go") + + +routes = [ + Route("/", homepage), + Route("/five", five_hundred), +] + +starlette_server = Starlette( + debug=True, + routes=routes, + on_startup=[startup], + middleware=[ + Middleware( + TrustedHostMiddleware, + allowed_hosts=["*"], + ), + ], +) diff --git a/tests/clients/boto3/README.md b/tests/clients/boto3/README.md index ac9fd2da..33c9a199 100644 --- a/tests/clients/boto3/README.md +++ b/tests/clients/boto3/README.md @@ -4,6 +4,9 @@ If you would like to run this test server manually from an ipython console: import os import urllib3 +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind + from moto import mock_aws import tests.apps.flask_app from tests.helpers import testenv @@ -13,12 +16,12 @@ http_client = urllib3.PoolManager() @mock_aws def test_app_boto3_sqs(): - with tracer.start_active_span('wsgi') as scope: - scope.span.set_tag('span.kind', 'entry') - scope.span.set_tag('http.host', 'localhost:80') - scope.span.set_tag('http.path', '/') - scope.span.set_tag('http.method', 'GET') - scope.span.set_tag('http.status_code', 200) - response = http_client.request('GET', testenv["wsgi_server"] + '/boto3/sqs') + with tracer.start_as_current_span("test") as span: + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute(SpanAttributes.HTTP_HOST, "localhost:80") + span.set_attribute("http.path", "/") + span.set_attribute(SpanAttributes.HTTP_METHOD, "GET") + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) + response = http_client.request("GET", testenv["wsgi_server"] + "/boto3/sqs") ``` diff --git a/tests/clients/boto3/test_boto3_lambda.py b/tests/clients/boto3/test_boto3_lambda.py index a850cbc1..78117804 100644 --- a/tests/clients/boto3/test_boto3_lambda.py +++ b/tests/clients/boto3/test_boto3_lambda.py @@ -1,167 +1,172 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +import pytest import json - +from typing import Generator import boto3 from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter + -class TestLambda(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestLambda: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws(config={"lambda": {"use_docker": False}}) self.mock.start() self.lambda_region = "us-east-1" - self.aws_lambda = boto3.client('lambda', region_name=self.lambda_region) + self.aws_lambda = boto3.client("lambda", region_name=self.lambda_region) self.function_name = "myfunc" - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_lambda_invoke(self): - with tracer.start_active_span('test'): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + def test_lambda_invoke(self) -> None: + with tracer.start_as_current_span("test"): + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert not test_span.ec + assert not boto_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" - def test_lambda_invoke_as_root_exit_span(self): + def test_lambda_invoke_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, Payload=json.dumps({"message": "success"}) + ) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - def test_request_header_capture_before_call(self): + assert boto_span + assert boto_span.n == "boto3" + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" + + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.lambda.Invoke', add_custom_header_before_call) + event_system.register( + "before-call.lambda.Invoke", add_custom_header_before_call + ) - with tracer.start_active_span('test'): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + with tracer.start_as_current_span("test"): + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert not test_span.ec + assert not boto_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -169,54 +174,58 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.lambda.Invoke', add_custom_header_before_sign) + event_system.register_first( + "before-sign.lambda.Invoke", add_custom_header_before_sign + ) - with tracer.start_active_span('test'): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + with tracer.start_as_current_span("test"): + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert not test_span.ec + assert not boto_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - - def test_response_header_capture(self): + def test_response_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'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events @@ -228,49 +237,52 @@ def test_response_header_capture(self): # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.lambda.Invoke', modify_after_call_args) + event_system.register("after-call.lambda.Invoke", modify_after_call_args) - with tracer.start_active_span('test'): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + with tracer.start_as_current_span("test"): + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) - - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert boto_span + + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s + + assert not test_span.ec + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" + + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_s3.py b/tests/clients/boto3/test_boto3_s3.py index bbffa7ec..6410a6ea 100644 --- a/tests/clients/boto3/test_boto3_s3.py +++ b/tests/clients/boto3/test_boto3_s3.py @@ -2,358 +2,371 @@ # (c) Copyright Instana Inc. 2020 import os -import unittest - +import pytest +from typing import Generator from moto import mock_aws import boto3 from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -upload_filename = os.path.abspath(pwd + '/../../data/boto3/test_upload_file.jpg') -download_target_filename = os.path.abspath(pwd + '/../../data/boto3/download_target_file.asdf') - - -class TestS3(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +upload_filename = os.path.abspath(pwd + "/../../data/boto3/test_upload_file.jpg") +download_target_filename = os.path.abspath( + pwd + "/../../data/boto3/download_target_file.asdf" +) + + +class TestS3: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.s3 = boto3.client('s3', region_name='us-east-1') - - def tearDown(self): + self.s3 = boto3.client("s3", region_name="us-east-1") + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - - def test_vanilla_create_bucket(self): + def test_vanilla_create_bucket(self) -> None: self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" - - def test_s3_create_bucket(self): - with tracer.start_active_span('test'): + def test_s3_create_bucket(self) -> None: + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) - def test_s3_create_bucket_as_root_exit_span(self): + def test_s3_create_bucket_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.s3.create_bucket(Bucket="aws_bucket_name") agent.options.allow_exit_as_root = False result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') - - - def test_s3_list_buckets(self): - with tracer.start_active_span('test'): + assert boto_span + assert boto_span.n == "boto3" + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) + + def test_s3_list_buckets(self) -> None: + with tracer.start_as_current_span("test"): result = self.s3.list_buckets() - self.assertEqual(0, len(result['Buckets'])) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert len(result["Buckets"]) == 0 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'ListBuckets') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/ListBuckets') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "ListBuckets" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/ListBuckets" + ) - def test_s3_vanilla_upload_file(self): - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + def test_s3_vanilla_upload_file(self) -> None: + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) result = self.s3.upload_file(upload_filename, bucket_name, object_name) - self.assertIsNone(result) + assert not result - - def test_s3_upload_file(self): - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + def test_s3_upload_file(self) -> None: + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.upload_file(upload_filename, bucket_name, object_name) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'upload_file') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - payload = {'Filename': upload_filename, 'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/upload_file') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "upload_file" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = { + "Filename": upload_filename, + "Bucket": "aws_bucket_name", + "Key": "aws_key_name", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/upload_file" + ) - def test_s3_upload_file_obj(self): - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + def test_s3_upload_file_obj(self) -> None: + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): with open(upload_filename, "rb") as fd: self.s3.upload_fileobj(fd, bucket_name, object_name) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'upload_fileobj') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/upload_fileobj') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "upload_fileobj" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = {"Bucket": "aws_bucket_name", "Key": "aws_key_name"} + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/upload_fileobj" + ) - def test_s3_download_file(self): - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + def test_s3_download_file(self) -> None: + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.download_file(bucket_name, object_name, download_target_filename) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'download_file') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name', 'Filename': '%s' % download_target_filename} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/download_file') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "download_file" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = { + "Bucket": "aws_bucket_name", + "Key": "aws_key_name", + "Filename": "%s" % download_target_filename, + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/download_file" + ) - def test_s3_download_file_obj(self): - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + def test_s3_download_file_obj(self) -> None: + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): with open(download_target_filename, "wb") as fd: self.s3.download_fileobj(bucket_name, object_name, fd) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'download_fileobj') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/download_fileobj') + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "download_fileobj" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/download_fileobj" + ) - def test_request_header_capture_before_call(self): - + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.s3.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.s3.CreateBucket', add_custom_header_before_call) + event_system.register( + "before-call.s3.CreateBucket", add_custom_header_before_call + ) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert not test_span.ec + assert not boto_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - - def test_request_header_capture_before_sign(self): - + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.s3.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -361,52 +374,54 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.s3.CreateBucket', add_custom_header_before_sign) + event_system.register_first( + "before-sign.s3.CreateBucket", add_custom_header_before_sign + ) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert not test_span.ec + assert not boto_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - - def test_response_header_capture(self): - + def test_response_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'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.s3.meta.events @@ -418,46 +433,48 @@ def test_response_header_capture(self): # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.s3.CreateBucket', modify_after_call_args) + event_system.register("after-call.s3.CreateBucket", modify_after_call_args) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) - - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') - - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert boto_span + + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s + + assert not test_span.ec + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) + + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_secretsmanager.py b/tests/clients/boto3/test_boto3_secretsmanager.py index 293a29c5..e8a715fc 100644 --- a/tests/clients/boto3/test_boto3_secretsmanager.py +++ b/tests/clients/boto3/test_boto3_secretsmanager.py @@ -3,200 +3,211 @@ import os import boto3 -import unittest - +import pytest +from typing import Generator from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSecretsManager(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + +class TestSecretsManager: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.secretsmanager = boto3.client('secretsmanager', region_name='us-east-1') - - def tearDown(self): + self.secretsmanager = boto3.client("secretsmanager", region_name="us-east-1") + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - - def test_vanilla_list_secrets(self): + def test_vanilla_list_secrets(self) -> None: result = self.secretsmanager.list_secrets(MaxResults=123) - self.assertListEqual(result['SecretList'], []) + assert result["SecretList"] == [] - - def test_get_secret_value(self): - secret_id = 'Uber_Password' + def test_get_secret_value(self) -> None: + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - self.assertEqual(response['Name'], secret_id) + assert response["Name"] == secret_id - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) - - self.assertIsNone(test_span.ec) + assert boto_span - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert not test_span.ec + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) - def test_get_secret_value_as_root_exit_span(self): - secret_id = 'Uber_Password' + def test_get_secret_value_as_root_exit_span(self) -> None: + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - self.assertEqual(response['Name'], secret_id) + assert response["Name"] == secret_id agent.options.allow_exit_as_root = True result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') - + assert boto_span + assert boto_span.n == "boto3" + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) - def test_request_header_capture_before_call(self): - secret_id = 'Uber_Password' + def test_request_header_capture_before_call(self) -> None: + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - self.assertEqual(response['Name'], secret_id) + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.secrets-manager.GetSecretValue', add_custom_header_before_call) + event_system.register( + "before-call.secrets-manager.GetSecretValue", add_custom_header_before_call + ) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert not test_span.ec - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - - def test_request_header_capture_before_sign(self): - secret_id = 'Uber_Password' + def test_request_header_capture_before_sign(self) -> None: + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - self.assertEqual(response['Name'], secret_id) + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -204,59 +215,66 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.secrets-manager.GetSecretValue', add_custom_header_before_sign) + event_system.register_first( + "before-sign.secrets-manager.GetSecretValue", add_custom_header_before_sign + ) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span - self.assertIsNone(test_span.ec) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert not test_span.ec - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - - def test_response_header_capture(self): - secret_id = 'Uber_Password' + def test_response_header_capture(self) -> None: + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - self.assertEqual(response['Name'], secret_id) + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events @@ -268,44 +286,52 @@ def test_response_header_capture(self): # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.secrets-manager.GetSecretValue', modify_after_call_args) + event_system.register( + "after-call.secrets-manager.GetSecretValue", modify_after_call_args + ) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_ses.py b/tests/clients/boto3/test_boto3_ses.py index 0e406795..afea6b0e 100644 --- a/tests/clients/boto3/test_boto3_ses.py +++ b/tests/clients/boto3/test_boto3_ses.py @@ -3,160 +3,174 @@ import os import boto3 -import unittest - +import pytest +from typing import Generator from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSes(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + +class TestSes: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.ses = boto3.client('ses', region_name='us-east-1') - - def tearDown(self): + self.ses = boto3.client("ses", region_name="us-east-1") + yield # Stop Moto after each test self.mock.stop() + def test_vanilla_verify_email(self) -> None: + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - def test_vanilla_verify_email(self): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) - - - def test_verify_email(self): - with tracer.start_active_span('test'): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + def test_verify_email(self) -> None: + with tracer.start_as_current_span("test"): + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) - def test_verify_email_as_root_exit_span(self): + def test_verify_email_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') - + assert boto_span + assert boto_span.n == "boto3" + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - def test_request_header_capture_before_call(self): + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.ses.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.ses.VerifyEmailIdentity', add_custom_header_before_call) + event_system.register( + "before-call.ses.VerifyEmailIdentity", add_custom_header_before_call + ) - with tracer.start_active_span('test'): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + with tracer.start_as_current_span("test"): + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - - def test_request_header_capture_before_sign(self): - + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.ses.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -164,50 +178,57 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.ses.VerifyEmailIdentity', add_custom_header_before_sign) + event_system.register_first( + "before-sign.ses.VerifyEmailIdentity", add_custom_header_before_sign + ) - with tracer.start_active_span('test'): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + with tracer.start_as_current_span("test"): + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - - def test_response_header_capture(self): - + def test_response_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'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.ses.meta.events @@ -219,44 +240,53 @@ def test_response_header_capture(self): # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.ses.VerifyEmailIdentity', modify_after_call_args) + event_system.register( + "after-call.ses.VerifyEmailIdentity", modify_after_call_args + ) - with tracer.start_active_span('test'): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + with tracer.start_as_current_span("test"): + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) + + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_sqs.py b/tests/clients/boto3/test_boto3_sqs.py index a673f7b9..e7755b1b 100644 --- a/tests/clients/boto3/test_boto3_sqs.py +++ b/tests/clients/boto3/test_boto3_sqs.py @@ -3,301 +3,321 @@ import os import boto3 -import unittest +import pytest import urllib3 +from typing import Generator from moto import mock_aws import tests.apps.flask_app from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter, testenv +from tests.helpers import get_first_span_by_filter, testenv pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSqs(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestSqs: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.sqs = boto3.client('sqs', region_name='us-east-1') + self.sqs = boto3.client("sqs", region_name="us-east-1") self.http_client = urllib3.PoolManager() - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - - def test_vanilla_create_queue(self): + def test_vanilla_create_queue(self) -> None: result = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '86400' - }) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) - + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "86400"}, + ) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 - def test_send_message(self): + def test_send_message(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - self.assertTrue(response['QueueUrl']) - queue_url = response['QueueUrl'] + assert response["QueueUrl"] + queue_url = response["QueueUrl"] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - self.assertTrue(response['MessageId']) + assert response["MessageId"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) - def test_send_message_as_root_exit_span(self): + def test_send_message_as_root_exit_span(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - self.assertTrue(response['QueueUrl']) + assert response["QueueUrl"] agent.options.allow_exit_as_root = True - queue_url = response['QueueUrl'] + queue_url = response["QueueUrl"] response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - self.assertTrue(response['MessageId']) + assert response["MessageId"] spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span + assert boto_span.n == "boto3" + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) - def test_app_boto3_sqs(self): - with tracer.start_active_span('test'): - self.http_client.request('GET', testenv["wsgi_server"] + '/boto3/sqs') + def test_app_boto3_sqs(self) -> None: + with tracer.start_as_current_span("test"): + self.http_client.request("GET", testenv["flask_server"] + "/boto3/sqs") spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert len(spans) == 5 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "urllib3" http_span = get_first_span_by_filter(spans, filter) - self.assertTrue(http_span) + assert http_span filter = lambda span: span.n == "wsgi" wsgi_span = get_first_span_by_filter(spans, filter) - self.assertTrue(wsgi_span) + assert wsgi_span - filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'CreateQueue' + filter = ( + lambda span: span.n == "boto3" and span.data["boto3"]["op"] == "CreateQueue" + ) bcq_span = get_first_span_by_filter(spans, filter) - self.assertTrue(bcq_span) + assert bcq_span - filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'SendMessage' + filter = ( + lambda span: span.n == "boto3" and span.data["boto3"]["op"] == "SendMessage" + ) bsm_span = get_first_span_by_filter(spans, filter) - self.assertTrue(bsm_span) - - self.assertEqual(http_span.t, test_span.t) - self.assertEqual(http_span.p, test_span.s) + assert bsm_span - self.assertEqual(wsgi_span.t, test_span.t) - self.assertEqual(wsgi_span.p, http_span.s) + assert http_span.t == test_span.t + assert http_span.p == test_span.s - self.assertEqual(bcq_span.t, test_span.t) - self.assertEqual(bcq_span.p, wsgi_span.s) + assert wsgi_span.t == test_span.t + assert wsgi_span.p == http_span.s - self.assertEqual(bsm_span.t, test_span.t) - self.assertEqual(bsm_span.p, wsgi_span.s) + assert bcq_span.t == test_span.t + assert bcq_span.p == wsgi_span.s + assert bsm_span.t == test_span.t + assert bsm_span.p == wsgi_span.s - def test_request_header_capture_before_call(self): + def test_request_header_capture_before_call(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - self.assertTrue(response['QueueUrl']) + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.sqs.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.sqs.SendMessage', add_custom_header_before_call) + event_system.register( + "before-call.sqs.SendMessage", add_custom_header_before_call + ) - queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + queue_url = response["QueueUrl"] + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - self.assertTrue(response['MessageId']) + assert response["MessageId"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - self.assertTrue(response['QueueUrl']) + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.sqs.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -305,76 +325,87 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.sqs.SendMessage', add_custom_header_before_sign) + event_system.register_first( + "before-sign.sqs.SendMessage", add_custom_header_before_sign + ) - queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + queue_url = response["QueueUrl"] + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - self.assertTrue(response['MessageId']) + assert response["MessageId"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - self.assertTrue(response['QueueUrl']) + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.sqs.meta.events @@ -386,60 +417,73 @@ def test_response_header_capture(self): # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.sqs.SendMessage', modify_after_call_args) + event_system.register("after-call.sqs.SendMessage", modify_after_call_args) - queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + queue_url = response["QueueUrl"] + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - self.assertTrue(response['MessageId']) + assert response["MessageId"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert not test_span.ec - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/test_cassandra-driver.py b/tests/clients/test_cassandra-driver.py index 44f05a21..3493de14 100644 --- a/tests/clients/test_cassandra-driver.py +++ b/tests/clients/test_cassandra-driver.py @@ -1,268 +1,271 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os -import time import random -import unittest - -from instana.singletons import agent, tracer -from ..helpers import testenv, get_first_span_by_name +import time +from typing import Generator -from cassandra.cluster import Cluster +import pytest from cassandra import ConsistencyLevel +from cassandra.cluster import Cluster from cassandra.query import SimpleStatement -cluster = Cluster([testenv['cassandra_host']], load_balancing_policy=None) +from instana.singletons import agent, tracer +from tests.helpers import get_first_span_by_name, testenv + +cluster = Cluster([testenv["cassandra_host"]], load_balancing_policy=None) session = cluster.connect() session.execute( - "CREATE KEYSPACE IF NOT EXISTS instana_tests WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};") -session.set_keyspace('instana_tests') -session.execute("CREATE TABLE IF NOT EXISTS users(" - "id int PRIMARY KEY," - "name text," - "age text," - "email varint," - "phone varint" - ");") - - -@unittest.skipUnless(os.environ.get("CASSANDRA_TEST"), reason="") -class TestCassandra(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + "CREATE KEYSPACE IF NOT EXISTS instana_tests WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};" +) +session.set_keyspace("instana_tests") +session.execute( + "CREATE TABLE IF NOT EXISTS users(" + "id int PRIMARY KEY," + "name text," + "age text," + "email varint," + "phone varint" + ");" +) + + +class TestCassandra: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield agent.options.allow_exit_as_root = False - def test_untraced_execute(self): - res = session.execute('SELECT name, age, email FROM users') + def test_untraced_execute(self) -> None: + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_untraced_execute_error(self): + def test_untraced_execute_error(self) -> None: res = None try: - res = session.execute('Not a valid query') - except: + res = session.execute("Not a valid query") + except Exception: pass - self.assertIsNone(res) + assert not res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_execute(self): + def test_execute(self) -> None: res = None - with tracer.start_active_span('test'): - res = session.execute('SELECT name, age, email FROM users') + with tracer.start_as_current_span("test"): + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_as_root_exit_span(self): + def test_execute_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - res = session.execute('SELECT name, age, email FROM users') + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan - self.assertIsNone(cspan.p) + assert not cspan.p - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_async(self): + def test_execute_async(self) -> None: res = None - with tracer.start_active_span('test'): - res = session.execute_async('SELECT name, age, email FROM users').result() + with tracer.start_as_current_span("test"): + res = session.execute_async("SELECT name, age, email FROM users").result() - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_simple_statement(self): + def test_simple_statement(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): query = SimpleStatement( - 'SELECT name, age, email FROM users', - is_idempotent=True + "SELECT name, age, email FROM users", is_idempotent=True ) res = session.execute(query) - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_error(self): + def test_execute_error(self) -> None: res = None try: - with tracer.start_active_span('test'): - res = session.execute('Not a real query') - except: + with tracer.start_as_current_span("test"): + res = session.execute("Not a real query") + except Exception: pass - self.assertIsNone(res) + assert not res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertEqual(cspan.ec, 1) + assert cspan.stack + assert cspan.ec == 1 - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'Not a real query') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertEqual(cspan.data["cassandra"]["error"], "Syntax error in CQL query") + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "Not a real query" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert cspan.data["cassandra"]["error"] == "Syntax error in CQL query" - def test_prepared_statement(self): + def test_prepared_statement(self) -> None: prepared = None - result = None - with tracer.start_active_span('test'): - prepared = session.prepare('INSERT INTO users (id, name, age) VALUES (?, ?, ?)') + with tracer.start_as_current_span("test"): + prepared = session.prepare( + "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" + ) prepared.consistency_level = ConsistencyLevel.QUORUM - result = session.execute(prepared, (random.randint(0, 1000000), "joe", "17")) + session.execute(prepared, (random.randint(0, 1000000), "joe", "17")) - self.assertIsNotNone(prepared) - self.assertIsNotNone(result) + assert prepared time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) - - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) - - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'INSERT INTO users (id, name, age) VALUES (?, ?, ?)') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertEqual(cspan.data["cassandra"]["achievedConsistency"], "QUORUM") - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert test_span.t == cspan.t + assert cspan.p == test_span.s + + assert cspan.stack + assert not cspan.ec + + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert ( + cspan.data["cassandra"]["query"] + == "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" + ) + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert cspan.data["cassandra"]["achievedConsistency"] == "QUORUM" + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] diff --git a/tests/clients/test_couchbase.py b/tests/clients/test_couchbase.py index 61cdf6ef..9064fb06 100644 --- a/tests/clients/test_couchbase.py +++ b/tests/clients/test_couchbase.py @@ -3,175 +3,192 @@ import os import time -import unittest +from typing import Generator +from unittest.mock import patch + +import pytest from instana.singletons import agent, tracer -from ..helpers import testenv, get_first_span_by_name, get_first_span_by_filter +from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter from couchbase.admin import Admin from couchbase.cluster import Cluster from couchbase.bucket import Bucket -from couchbase.exceptions import CouchbaseTransientError, HTTPError, KeyExistsError, NotFoundError +from couchbase.exceptions import ( + CouchbaseTransientError, + HTTPError, + KeyExistsError, + NotFoundError, +) import couchbase.subdocument as SD from couchbase.n1ql import N1QLQuery # Delete any pre-existing buckets. Create new. -cb_adm = Admin(testenv['couchdb_username'], testenv['couchdb_password'], host=testenv['couchdb_host'], port=8091) +cb_adm = Admin( + testenv["couchdb_username"], + testenv["couchdb_password"], + host=testenv["couchdb_host"], + port=8091, +) # Make sure a test bucket exists try: - cb_adm.bucket_create('travel-sample') - cb_adm.wait_ready('travel-sample', timeout=30) + cb_adm.bucket_create("travel-sample") + cb_adm.wait_ready("travel-sample", timeout=30) except HTTPError: pass -@unittest.skipIf(not os.environ.get("COUCHBASE_TEST"), reason="") -class TestStandardCouchDB(unittest.TestCase): - def setup_class(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder - self.cluster = Cluster('couchbase://%s' % testenv['couchdb_host']) - self.bucket = Bucket('couchbase://%s/travel-sample' % testenv['couchdb_host'], - username=testenv['couchdb_username'], password=testenv['couchdb_password']) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ - agent.options.allow_exit_as_root = False - - def setup_method(self, _): - self.bucket.upsert('test-key', 1) +class TestStandardCouchDB: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor + self.cluster = Cluster("couchbase://%s" % testenv["couchdb_host"]) + self.bucket = Bucket( + "couchbase://%s/travel-sample" % testenv["couchdb_host"], + username=testenv["couchdb_username"], + password=testenv["couchdb_password"], + ) + self.bucket.upsert("test-key", 1) time.sleep(0.5) self.recorder.clear_spans() + yield + agent.options.allow_exit_as_root = False - def test_vanilla_get(self): + def test_vanilla_get(self) -> None: res = self.bucket.get("test-key") - self.assertTrue(res) - - def test_pipeline(self): - pass + assert res - def test_upsert(self): + def test_upsert(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.upsert("test_upsert", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert" - def test_upsert_as_root_exit_span(self): + def test_upsert_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True res = self.bucket.upsert("test_upsert", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span - self.assertEqual(cb_span.p, None) + assert not cb_span.p - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert" - def test_upsert_multi(self): + def test_upsert_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_upsert_multi'] = 1 - kvs['second_test_upsert_multi'] = 1 + kvs = {} + kvs["first_test_upsert_multi"] = 1 + kvs["second_test_upsert_multi"] = 1 - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.upsert_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_upsert_multi'].success) - self.assertTrue(res['second_test_upsert_multi'].success) + assert res + assert res["first_test_upsert_multi"].success + assert res["second_test_upsert_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert_multi" - def test_insert_new(self): + def test_insert_new(self) -> None: res = None try: - self.bucket.remove('test_insert_new') + self.bucket.remove("test_insert_new") except NotFoundError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert_new", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert" - def test_insert_existing(self): + def test_insert_existing(self) -> None: res = None try: self.bucket.insert("test_insert", 1) @@ -179,113 +196,119 @@ def test_insert_existing(self): pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert", 1) except KeyExistsError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("_KeyExistsError") - self.assertFalse(found == -1, "Error substring not found.") + assert not found == -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert" - def test_insert_multi(self): + def test_insert_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_upsert_multi'] = 1 - kvs['second_test_upsert_multi'] = 1 + kvs = {} + kvs["first_test_upsert_multi"] = 1 + kvs["second_test_upsert_multi"] = 1 try: - self.bucket.remove('first_test_upsert_multi') - self.bucket.remove('second_test_upsert_multi') + self.bucket.remove("first_test_upsert_multi") + self.bucket.remove("second_test_upsert_multi") except NotFoundError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_upsert_multi'].success) - self.assertTrue(res['second_test_upsert_multi'].success) + assert res + assert res["first_test_upsert_multi"].success + assert res["second_test_upsert_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert_multi" - def test_replace(self): + def test_replace(self) -> None: res = None try: self.bucket.insert("test_replace", 1) except KeyExistsError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace" - def test_replace_non_existent(self): + def test_replace_non_existent(self) -> None: res = None try: @@ -294,969 +317,1102 @@ def test_replace_non_existent(self): pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) except NotFoundError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("NotFoundError") - self.assertFalse(found == -1, "Error substring not found.") + assert not found == -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace" - def test_replace_multi(self): + def test_replace_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_replace_multi'] = 1 - kvs['second_test_replace_multi'] = 1 + kvs = {} + kvs["first_test_replace_multi"] = 1 + kvs["second_test_replace_multi"] = 1 - self.bucket.upsert('first_test_replace_multi', "one") - self.bucket.upsert('second_test_replace_multi', "two") + self.bucket.upsert("first_test_replace_multi", "one") + self.bucket.upsert("second_test_replace_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_replace_multi'].success) - self.assertTrue(res['second_test_replace_multi'].success) + assert res + assert res["first_test_replace_multi"].success + assert res["second_test_replace_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace_multi" - def test_append(self): + def test_append(self) -> None: self.bucket.upsert("test_append", "one") res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.append("test_append", "two") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'append') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "append" - def test_append_multi(self): + def test_append_multi(self) -> None: res = None kvs = dict() - kvs['first_test_append_multi'] = "ok1" - kvs['second_test_append_multi'] = "ok2" + kvs["first_test_append_multi"] = "ok1" + kvs["second_test_append_multi"] = "ok2" - self.bucket.upsert('first_test_append_multi', "one") - self.bucket.upsert('second_test_append_multi', "two") + self.bucket.upsert("first_test_append_multi", "one") + self.bucket.upsert("second_test_append_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.append_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_append_multi'].success) - self.assertTrue(res['second_test_append_multi'].success) + assert res + assert res["first_test_append_multi"].success + assert res["second_test_append_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'append_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "append_multi" - def test_prepend(self): + def test_prepend(self) -> None: self.bucket.upsert("test_prepend", "one") res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.prepend("test_prepend", "two") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'prepend') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "prepend" - def test_prepend_multi(self): + def test_prepend_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_prepend_multi'] = "ok1" - kvs['second_test_prepend_multi'] = "ok2" + kvs = {} + kvs["first_test_prepend_multi"] = "ok1" + kvs["second_test_prepend_multi"] = "ok2" - self.bucket.upsert('first_test_prepend_multi', "one") - self.bucket.upsert('second_test_prepend_multi', "two") + self.bucket.upsert("first_test_prepend_multi", "one") + self.bucket.upsert("second_test_prepend_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.prepend_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_prepend_multi'].success) - self.assertTrue(res['second_test_prepend_multi'].success) + assert res + assert res["first_test_prepend_multi"].success + assert res["second_test_prepend_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'prepend_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "prepend_multi" - def test_get(self): + def test_get(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.get("test-key") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get" - def test_rget(self): + def test_rget(self) -> None: res = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.rget("test-key", replica_index=None) except CouchbaseTransientError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("CouchbaseTransientError") - self.assertFalse(found == -1, "Error substring not found.") + assert found != -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'rget') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "rget" - def test_get_not_found(self): + def test_get_not_found(self) -> None: res = None try: - self.bucket.remove('test_get_not_found') + self.bucket.remove("test_get_not_found") except NotFoundError: pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.get("test_get_not_found") except NotFoundError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("NotFoundError") - self.assertFalse(found == -1, "Error substring not found.") + assert found != -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get" - def test_get_multi(self): + def test_get_multi(self) -> None: res = None - self.bucket.upsert('first_test_get_multi', "one") - self.bucket.upsert('second_test_get_multi', "two") + self.bucket.upsert("first_test_get_multi", "one") + self.bucket.upsert("second_test_get_multi", "two") - with tracer.start_active_span('test'): - res = self.bucket.get_multi(['first_test_get_multi', 'second_test_get_multi']) + with tracer.start_as_current_span("test"): + res = self.bucket.get_multi( + ["first_test_get_multi", "second_test_get_multi"] + ) - self.assertTrue(res) - self.assertTrue(res['first_test_get_multi'].success) - self.assertTrue(res['second_test_get_multi'].success) + assert res + assert res["first_test_get_multi"].success + assert res["second_test_get_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get_multi" - def test_touch(self): + def test_touch(self) -> None: res = None self.bucket.upsert("test_touch", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.touch("test_touch") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'touch') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "touch" - def test_touch_multi(self): + def test_touch_multi(self) -> None: res = None - self.bucket.upsert('first_test_touch_multi', "one") - self.bucket.upsert('second_test_touch_multi', "two") + self.bucket.upsert("first_test_touch_multi", "one") + self.bucket.upsert("second_test_touch_multi", "two") - with tracer.start_active_span('test'): - res = self.bucket.touch_multi(['first_test_touch_multi', 'second_test_touch_multi']) + with tracer.start_as_current_span("test"): + res = self.bucket.touch_multi( + ["first_test_touch_multi", "second_test_touch_multi"] + ) - self.assertTrue(res) - self.assertTrue(res['first_test_touch_multi'].success) - self.assertTrue(res['second_test_touch_multi'].success) + assert res + assert res["first_test_touch_multi"].success + assert res["second_test_touch_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'touch_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "touch_multi" - def test_lock(self): + def test_lock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) - self.assertTrue(rv) - self.assertTrue(rv.success) + assert rv + assert rv.success # upsert automatically unlocks the key res = self.bucket.upsert("test_lock_unlock", "updated", rv.cas) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "upsert" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "upsert" cb_upsert_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_upsert_span) + assert cb_upsert_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_upsert_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_upsert_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_upsert_span.stack) - self.assertIsNone(cb_upsert_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock') - self.assertEqual(cb_upsert_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_upsert_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_upsert_span.data["couchbase"]["type"], 'upsert') - - def test_lock_unlock(self): + assert cb_lock_span.t == test_span.t + assert cb_upsert_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_upsert_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_upsert_span.stack + assert not cb_upsert_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_lock_span.data["couchbase"]["type"] == "lock" + assert ( + cb_upsert_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_upsert_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_upsert_span.data["couchbase"]["type"] == "upsert" + + def test_lock_unlock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) - self.assertTrue(rv) - self.assertTrue(rv.success) + assert rv + assert rv.success # upsert automatically unlocks the key res = self.bucket.unlock("test_lock_unlock", rv.cas) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock" cb_unlock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_unlock_span) + assert cb_unlock_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_unlock_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_unlock_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_unlock_span.stack) - self.assertIsNone(cb_unlock_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock') - self.assertEqual(cb_unlock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_unlock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_unlock_span.data["couchbase"]["type"], 'unlock') - - def test_lock_unlock_muilti(self): + assert cb_lock_span.t == test_span.t + assert cb_unlock_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_unlock_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_unlock_span.stack + assert not cb_unlock_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"], "travel-sample" + assert cb_lock_span.data["couchbase"]["type"], "lock" + assert ( + cb_unlock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_unlock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_unlock_span.data["couchbase"]["type"] == "unlock" + + def test_lock_unlock_muilti(self) -> None: res = None self.bucket.upsert("test_lock_unlock_multi_1", "lock_this") self.bucket.upsert("test_lock_unlock_multi_2", "lock_this") keys_to_lock = ("test_lock_unlock_multi_1", "test_lock_unlock_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock_multi(keys_to_lock, ttl=5) - self.assertTrue(rv) - self.assertTrue(rv['test_lock_unlock_multi_1'].success) - self.assertTrue(rv['test_lock_unlock_multi_2'].success) + assert rv + assert rv["test_lock_unlock_multi_1"].success + assert rv["test_lock_unlock_multi_2"].success res = self.bucket.unlock_multi(rv) - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + def filter(span): + return ( + span.n == "couchbase" and span.data["couchbase"]["type"] == "lock_multi" + ) - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock_multi" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return ( + span.n == "couchbase" + and span.data["couchbase"]["type"] == "unlock_multi" + ) - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock_multi" cb_unlock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_unlock_span) + assert cb_unlock_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_unlock_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_unlock_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_unlock_span.stack) - self.assertIsNone(cb_unlock_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock_multi') - self.assertEqual(cb_unlock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_unlock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_unlock_span.data["couchbase"]["type"], 'unlock_multi') - - def test_remove(self): + assert cb_lock_span.t == test_span.t + assert cb_unlock_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_unlock_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_unlock_span.stack + assert not cb_unlock_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_lock_span.data["couchbase"]["type"] == "lock_multi" + assert ( + cb_unlock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_unlock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_unlock_span.data["couchbase"]["type"] == "unlock_multi" + + def test_remove(self) -> None: res = None self.bucket.upsert("test_remove", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.remove("test_remove") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'remove') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "remove" - def test_remove_multi(self): + def test_remove_multi(self) -> None: res = None self.bucket.upsert("test_remove_multi_1", 1) self.bucket.upsert("test_remove_multi_2", 1) keys_to_remove = ("test_remove_multi_1", "test_remove_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.remove_multi(keys_to_remove) - self.assertTrue(res) - self.assertTrue(res['test_remove_multi_1'].success) - self.assertTrue(res['test_remove_multi_2'].success) + assert res + assert res["test_remove_multi_1"].success + assert res["test_remove_multi_2"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'remove_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "remove_multi" - def test_counter(self): + def test_counter(self) -> None: res = None self.bucket.upsert("test_counter", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.counter("test_counter", delta=10) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'counter') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "counter" - def test_counter_multi(self): + def test_counter_multi(self) -> None: res = None self.bucket.upsert("first_test_counter", 1) self.bucket.upsert("second_test_counter", 1) - with tracer.start_active_span('test'): - res = self.bucket.counter_multi(("first_test_counter", "second_test_counter")) + with tracer.start_as_current_span("test"): + res = self.bucket.counter_multi( + ("first_test_counter", "second_test_counter") + ) - self.assertTrue(res) - self.assertTrue(res['first_test_counter'].success) - self.assertTrue(res['second_test_counter'].success) + assert res + assert res["first_test_counter"].success + assert res["second_test_counter"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'counter_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "counter_multi" - def test_mutate_in(self): + def test_mutate_in(self) -> None: res = None - self.bucket.upsert('king_arthur', {'name': 'Arthur', 'email': 'kingarthur@couchbase.com', - 'interests': ['Holy Grail', 'African Swallows']}) - - with tracer.start_active_span('test'): - res = self.bucket.mutate_in('king_arthur', - SD.array_addunique('interests', 'Cats'), - SD.counter('updates', 1)) - - self.assertTrue(res) - self.assertTrue(res.success) + self.bucket.upsert( + "king_arthur", + { + "name": "Arthur", + "email": "kingarthur@couchbase.com", + "interests": ["Holy Grail", "African Swallows"], + }, + ) + + with tracer.start_as_current_span("test"): + res = self.bucket.mutate_in( + "king_arthur", + SD.array_addunique("interests", "Cats"), + SD.counter("updates", 1), + ) + + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'mutate_in') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "mutate_in" - def test_lookup_in(self): + def test_lookup_in(self) -> None: res = None - self.bucket.upsert('king_arthur', {'name': 'Arthur', 'email': 'kingarthur@couchbase.com', - 'interests': ['Holy Grail', 'African Swallows']}) - - with tracer.start_active_span('test'): - res = self.bucket.lookup_in('king_arthur', - SD.get('email'), - SD.get('interests')) - - self.assertTrue(res) - self.assertTrue(res.success) + self.bucket.upsert( + "king_arthur", + { + "name": "Arthur", + "email": "kingarthur@couchbase.com", + "interests": ["Holy Grail", "African Swallows"], + }, + ) + + with tracer.start_as_current_span("test"): + res = self.bucket.lookup_in( + "king_arthur", SD.get("email"), SD.get("interests") + ) + + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'lookup_in') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "lookup_in" - def test_stats(self): + def test_stats(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.stats() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'stats') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "stats" - def test_ping(self): + def test_ping(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.ping() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'ping') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "ping" - def test_diagnostics(self): + def test_diagnostics(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.diagnostics() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'diagnostics') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "diagnostics" - def test_observe(self): + def test_observe(self) -> None: res = None - self.bucket.upsert('test_observe', 1) + self.bucket.upsert("test_observe", 1) - with tracer.start_active_span('test'): - res = self.bucket.observe('test_observe') + with tracer.start_as_current_span("test"): + res = self.bucket.observe("test_observe") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'observe') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "observe" - def test_observe_multi(self): + def test_observe_multi(self) -> None: res = None - self.bucket.upsert('test_observe_multi_1', 1) - self.bucket.upsert('test_observe_multi_2', 1) + self.bucket.upsert("test_observe_multi_1", 1) + self.bucket.upsert("test_observe_multi_2", 1) - keys_to_observe = ('test_observe_multi_1', 'test_observe_multi_2') + keys_to_observe = ("test_observe_multi_1", "test_observe_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.observe_multi(keys_to_observe) - self.assertTrue(res) - self.assertTrue(res['test_observe_multi_1'].success) - self.assertTrue(res['test_observe_multi_2'].success) + assert res + assert res["test_observe_multi_1"].success + assert res["test_observe_multi_2"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'observe_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "observe_multi" - def test_raw_n1ql_query(self): + def test_query_with_instana_tracing_off(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.couchbase_inst.tracing_is_off", return_value=True + ): res = self.bucket.n1ql_query("SELECT 1") + assert res - self.assertTrue(res) + def test_query_with_instana_exception(self) -> None: + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.couchbase_inst.collect_attributes", + side_effect=Exception("test-error"), + ): + self.bucket.n1ql_query("SELECT 1") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + cb_span = get_first_span_by_name(spans, "couchbase") - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert cb_span.data["couchbase"]["error"] == "Exception('test-error')" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + def test_raw_n1ql_query(self) -> None: + res = None + + with tracer.start_as_current_span("test"): + res = self.bucket.n1ql_query("SELECT 1") + + assert res + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" + + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'n1ql_query') - self.assertEqual(cb_span.data["couchbase"]["sql"], 'SELECT 1') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "n1ql_query" + assert cb_span.data["couchbase"]["sql"] == "SELECT 1" - def test_n1ql_query(self): + def test_n1ql_query(self) -> None: res = None - with tracer.start_active_span('test'): - res = self.bucket.n1ql_query(N1QLQuery('SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"')) + with tracer.start_as_current_span("test"): + res = self.bucket.n1ql_query( + N1QLQuery( + 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"' + ) + ) - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) - - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) - - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'n1ql_query') - self.assertEqual(cb_span.data["couchbase"]["sql"], 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"') + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s + + assert cb_span.stack + assert not cb_span.ec + + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "n1ql_query" + assert ( + cb_span.data["couchbase"]["sql"] + == 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"' + ) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 923b3f38..6ec666c5 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -2,86 +2,115 @@ # (c) Copyright Instana Inc. 2020 import logging -import unittest +from typing import Generator +from unittest.mock import patch + import pytest -from instana.singletons import agent, tracer +from opentelemetry.trace import SpanKind +from instana.singletons import agent, tracer -class TestLogging(unittest.TestCase): - @pytest.fixture - def capture_log(self, caplog): - self.caplog = caplog - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestLogging: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() - self.logger = logging.getLogger('unit test') - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + self.logger = logging.getLogger("unit test") + yield + # tearDown + # Ensure that allow_exit_as_root has the default value agent.options.allow_exit_as_root = False - def test_no_span(self): - with tracer.start_active_span('test'): - self.logger.info('info message') + def test_no_span(self) -> None: + self.logger.setLevel(logging.INFO) + with tracer.start_as_current_span("test"): + self.logger.info("info message") + + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + + def test_extra_span(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - def test_extra_span(self): - with tracer.start_active_span('test'): - self.logger.warning('foo %s', 'bar') + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"].get("message") == "foo bar" + + def test_log_with_tuple(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("foo %s", ("bar",)) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual(2, spans[0].k) - self.assertEqual('foo bar', spans[0].data["log"].get('message')) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"].get("message") == "foo ('bar',)" - def test_log_with_tuple(self): - with tracer.start_active_span('test'): - self.logger.warning('foo %s', ("bar",)) + def test_log_with_dict(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("foo %s", {"bar": 18}) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual(2, spans[0].k) - self.assertEqual("foo ('bar',)", spans[0].data["log"].get('message')) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"].get("message") == "foo {'bar': 18}" - def test_parameters(self): - with tracer.start_active_span('test'): + def test_parameters(self) -> None: + with tracer.start_as_current_span("test"): try: a = 42 b = 0 c = a / b except Exception as e: - self.logger.exception('Exception: %s', str(e)) + self.logger.exception("Exception: %s", str(e)) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNotNone(spans[0].data["log"].get('parameters')) + assert len(spans) == 2 + assert spans[0].data["log"].get("parameters") is not None - def test_no_root_exit_span(self): + def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - self.logger.info('info message') + self.logger.info("info message") spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) - def test_root_exit_span(self): + assert len(spans) == 0 + + def test_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - self.logger.warning('foo %s', 'bar') + self.logger.warning("foo %s", "bar") + + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"].get("message") == "foo bar" + + def test_exception(self) -> None: + with tracer.start_as_current_span("test"): + with patch( + "instana.span.span.InstanaSpan.add_event", + side_effect=Exception("mocked error"), + ): + self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - self.assertEqual(2, spans[0].k) - self.assertEqual('foo bar', spans[0].data["log"].get('message')) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT + assert spans[0].data["log"] == {} - @pytest.mark.usefixtures("capture_log") - def test_log_caller(self): + def test_log_caller(self, caplog: pytest.LogCaptureFixture) -> None: handler = logging.StreamHandler() handler.setFormatter( logging.Formatter("source: %(funcName)s, message: %(message)s") @@ -91,8 +120,9 @@ def test_log_caller(self): def log_custom_warning(): self.logger.warning("foo %s", "bar") - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): log_custom_warning() - self.assertEqual(self.caplog.records[0].funcName, "log_custom_warning") + + assert caplog.records[-1].funcName == "log_custom_warning" self.logger.removeHandler(handler) diff --git a/tests/clients/test_mysqlclient.py b/tests/clients/test_mysqlclient.py index 518eff30..4f5f6013 100644 --- a/tests/clients/test_mysqlclient.py +++ b/tests/clients/test_mysqlclient.py @@ -1,22 +1,23 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging -import unittest - import MySQLdb - -from ..helpers import testenv -from instana.singletons import agent, tracer - -logger = logging.getLogger(__name__) - - -class TestMySQLPython(unittest.TestCase): - def setUp(self): - self.db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'], - user=testenv['mysql_user'], passwd=testenv['mysql_pw'], - db=testenv['mysql_db']) +import pytest + +from instana.singletons import agent, tracer +from tests.helpers import testenv + + +class TestMySQLPython: + @pytest.fixture(autouse=True) + def _resource(self): + self.db = MySQLdb.connect( + host=testenv["mysql_host"], + port=testenv["mysql_port"], + user=testenv["mysql_user"], + passwd=testenv["mysql_pw"], + db=testenv["mysql_db"], + ) database_setup_query = """ DROP TABLE IF EXISTS users; CREATE TABLE users( @@ -36,251 +37,260 @@ def setUp(self): setup_cursor.close() self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and self.cursor.connection.open: - self.cursor.close() + self.cursor.close() if self.db and self.db.open: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False def test_vanilla_query(self): affected_rows = self.cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 result = self.cursor.fetchone() - self.assertEqual(3, len(result)) + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 def test_basic_query(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_query_as_root_exit_span(self): agent.options.allow_exit_as_root = True affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 db_span = spans[0] - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_insert(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( - """INSERT INTO users(name, email) VALUES(%s, %s)""", - ('beaker', 'beaker@muppets.com')) + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_executemany(self): - with tracer.start_active_span('test'): - affected_rows = self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + with tracer.start_as_current_span("test"): + affected_rows = self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) self.db.commit() - self.assertEqual(2, affected_rows) + assert affected_rows == 2 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) - self.assertIsInstance(callproc_result, tuple) + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "test_proc" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_error_capture(self): affected_rows = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass - self.assertIsNone(affected_rows) + assert not affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertEqual(1, db_span.ec) - self.assertEqual(db_span.data["mysql"]["error"], '(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db']) + assert db_span.ec == 2 + assert ( + db_span.data["mysql"]["error"] + == f"(1146, \"Table '{testenv['mysql_db']}.blah' doesn't exist\")" + ) - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from blah" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py new file mode 100644 index 00000000..6235e6cc --- /dev/null +++ b/tests/clients/test_pep0249.py @@ -0,0 +1,325 @@ +import logging +from typing import Generator +from unittest.mock import patch + +import psycopg2 +import psycopg2.extras +import pytest +from instana.instrumentation.pep0249 import ( + ConnectionFactory, + ConnectionWrapper, + CursorWrapper, +) +from instana.singletons import tracer +from instana.span.span import InstanaSpan +from opentelemetry.trace import SpanKind +from pytest import LogCaptureFixture + +from tests.helpers import testenv + + +class TestCursorWrapper: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.connect_params = [ + "db", + { + "db": testenv["postgresql_db"], + "host": testenv["postgresql_host"], + "port": testenv["postgresql_port"], + "user": testenv["postgresql_user"], + "password": testenv["postgresql_pw"], + }, + ] + self.test_conn = psycopg2.connect( + database=self.connect_params[1]["db"], + host=self.connect_params[1]["host"], + port=self.connect_params[1]["port"], + user=self.connect_params[1]["user"], + password=self.connect_params[1]["password"], + ) + self.cursor_params = {"key": "value"} + self.test_cursor = self.test_conn.cursor() + self.cursor_name = "test-cursor" + self.test_wrapper = CursorWrapper( + self.test_cursor, + self.cursor_name, + self.connect_params, + self.cursor_params, + ) + yield + self.test_cursor.close() + self.test_conn.close() + + def reset_table(self) -> None: + self.test_cursor.execute( + """ + DROP TABLE IF EXISTS tests; + CREATE TABLE tests (id SERIAL PRIMARY KEY, name VARCHAR(50), email VARCHAR(100)); + """ + ) + self.test_cursor.execute( + """ + INSERT INTO tests (id, name, email) VALUES (1, 'test-name', 'testemail@mail.com'); + """ + ) + self.test_conn.commit() + + def reset_procedure(self) -> None: + self.test_cursor.execute(""" + DROP PROCEDURE IF EXISTS insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR); + CREATE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO tests (id, name, email) VALUES (test_id, test_name, test_email); + END; + $$; + """) + self.test_conn.commit() + + def test_cursor_wrapper_default(self) -> None: + # CursorWrapper + assert self.test_wrapper + assert self.test_wrapper._module_name == self.cursor_name + connection_params = {"db", "host", "port", "user", "password"} + assert connection_params.issubset(self.test_wrapper._connect_params[1].keys()) + assert not self.test_wrapper.closed + assert self.test_wrapper._cursor_params == self.cursor_params + + # Test Connection + assert ( + self.test_conn.dsn + == "user=root password=xxx dbname=instana_test_db host=127.0.0.1 port=5432" + ) + assert not self.test_conn.autocommit + assert self.test_conn.status == 1 + assert self.test_conn.info.dbname == "instana_test_db" + assert self.test_conn.info.host == "127.0.0.1" + assert self.test_conn.info.user == "root" + assert self.test_conn.info.port == 5432 + + # Test Cursor + assert self.test_cursor.arraysize == 1 + assert isinstance(self.test_cursor, psycopg2.extensions.cursor) + assert hasattr(self.test_cursor, "callproc") + assert hasattr(self.test_cursor, "close") + assert hasattr(self.test_cursor, "execute") + assert hasattr(self.test_cursor, "executemany") + assert hasattr(self.test_cursor, "fetchone") + assert hasattr(self.test_cursor, "fetchall") + + def test_collect_kvs(self) -> None: + self.reset_table() + with tracer.start_as_current_span("test") as span: + sample_sql = """ + select * from tests; + """ + self.test_wrapper._collect_kvs(span, sample_sql) + assert span.attributes["span.kind"] == SpanKind.CLIENT + assert span.attributes["db.name"] == "instana_test_db" + assert span.attributes["db.statement"] == sample_sql + assert span.attributes["db.user"] == "root" + assert span.attributes["host"] == "127.0.0.1" + assert span.attributes["port"] == 5432 + + def test_collect_kvs_error(self, caplog: LogCaptureFixture) -> None: + self.reset_table() + with tracer.start_as_current_span("test") as span: + connect_params = "sample" + sample_wrapper = CursorWrapper( + self.test_cursor, + self.cursor_name, + connect_params, + ) + sample_sql = "select * from tests;" + caplog.set_level(logging.DEBUG, logger="instana") + sample_wrapper._collect_kvs(span, sample_sql) + assert "string indices must be integers" in caplog.messages[0] + + def test_enter(self) -> None: + response = self.test_wrapper.__enter__() + assert response == self.test_wrapper + assert isinstance(response, CursorWrapper) + + def test_execute_with_tracing_off(self) -> None: + self.reset_table() + with tracer.start_as_current_span("sqlalchemy"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_params = (2, "sample-name", "sample-email@mail.com") + self.test_wrapper.execute(sample_sql, sample_params) + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_execute_with_tracing(self) -> None: + self.reset_table() + with tracer.start_as_current_span("test"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_params = (3, "sample-name", "sample-email@mail.com") + self.test_wrapper.execute(sample_sql, sample_params) + last_inserted_row = self.test_cursor.fetchone() + self.test_conn.commit() + assert last_inserted_row == sample_params + + # Exception Handling + with pytest.raises(Exception) as exc_info, patch.object( + CursorWrapper, "_collect_kvs", side_effect=Exception("test exception") + ) as mock_collect_kvs: + self.test_wrapper.execute(sample_sql) + assert str(exc_info.value) == "test exception" + mock_collect_kvs.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_executemany_with_tracing_off(self) -> None: + self.reset_table() + with tracer.start_as_current_span("sqlalchemy"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_seq_of_params = [ + (4, "sample-name-3", "sample-email-3@mail.com"), + (5, "sample-name-4", "sample-email-4@mail.com"), + ] + self.test_wrapper.executemany(sample_sql, sample_seq_of_params) + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + for record in sample_seq_of_params: + assert record in response + assert len(response) == 3 + + def test_executemany_with_tracing(self) -> None: + self.reset_table() + with tracer.start_as_current_span("test"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_seq_of_params = [ + (6, "sample-name-3", "sample-email-3@mail.com"), + (7, "sample-name-4", "sample-email-4@mail.com"), + ] + self.test_wrapper.executemany(sample_sql, sample_seq_of_params) + + # Exception Handling + with pytest.raises(Exception) as exc_info, patch.object( + CursorWrapper, "_collect_kvs", side_effect=Exception("test exception") + ) as mock_collect_kvs: + self.test_wrapper.executemany( + sample_sql, seq_of_parameters=sample_seq_of_params + ) + assert str(exc_info.value) == "test exception" + mock_collect_kvs.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + for record in sample_seq_of_params: + assert record in response + assert len(response) == 3 + + def test_callproc_with_tracing_off(self) -> None: + self.reset_table() + self.reset_procedure() + with tracer.start_as_current_span("sqlalchemy"): + sample_proc_name = "call insert_user(%s, %s, %s);" + sample_params = (8, "sample-name-8", "sample-email-8@mail.com") + self.test_wrapper.callproc(sample_proc_name, sample_params) + self.test_conn.commit() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_callproc_with_tracing(self) -> None: + self.reset_table() + self.reset_procedure() + with tracer.start_as_current_span("test"): + sample_proc_name = "call insert_user(%s, %s, %s);" + sample_params = (9, "sample-name-9", "sample-email-9@mail.com") + self.test_wrapper.callproc(sample_proc_name, sample_params) + self.test_conn.commit() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + # Exception Handling + error_proc_name = "erroroeus command;" + with pytest.raises(Exception) as exc_info, patch.object( + InstanaSpan, + "record_exception", + ) as mock_exception: + self.test_wrapper.callproc(error_proc_name, sample_params) + assert exc_info.typename == "SyntaxError" + mock_exception.call_count == 2 + + +class TestConnectionWrapper: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.connect_params = [ + "db", + { + "db": "instana_test_db", + "host": "localhost", + "port": "5432", + "user": "root", + "password": "passw0rd", + }, + ] + self.test_conn = psycopg2.connect( + database=self.connect_params[1]["db"], + host=self.connect_params[1]["host"], + port=self.connect_params[1]["port"], + user=self.connect_params[1]["user"], + password=self.connect_params[1]["password"], + ) + self.module_name = "test-connection" + self.connection_manager = ConnectionWrapper( + self.test_conn, self.module_name, self.connect_params + ) + yield + self.test_conn.close() + + def test_enter(self) -> None: + response = self.connection_manager.__enter__() + assert isinstance(response, ConnectionWrapper) + assert response._module_name == self.module_name + assert response._connect_params == self.connect_params + + def test_cursor(self) -> None: + response = self.connection_manager.cursor() + assert isinstance(response, CursorWrapper) + + def test_close(self) -> None: + response = self.connection_manager.close() + assert self.test_conn.closed + assert not response + + def test_commit(self) -> None: + response = self.connection_manager.commit() + assert not response + + def test_rollback(self) -> None: + if hasattr(self.connection_manager, "rollback"): + response = self.connection_manager.rollback() + assert not response + + +class TestConnectionFactory: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.test_conn_func = psycopg2.connect + self.test_module_name = "test-factory" + self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) + yield + self.test_conn_func = None + self.test_module_name = None + self.conn_fact = None + + def test_call(self) -> None: + response = self.conn_fact( + dsn="user=root password=passw0rd dbname=instana_test_db host=localhost port=5432" + ) + assert isinstance(self.conn_fact._wrapper_ctor, ConnectionWrapper.__class__) + assert self.conn_fact._connect_func == self.test_conn_func + assert self.conn_fact._module_name == self.test_module_name + assert isinstance(response, ConnectionWrapper) diff --git a/tests/clients/test_pika.py b/tests/clients/test_pika.py index 887f4bf4..093c36cd 100644 --- a/tests/clients/test_pika.py +++ b/tests/clients/test_pika.py @@ -1,328 +1,177 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -import unittest import threading import time +from typing import Generator, Optional -import pika import mock +import pika +import pika.adapters.blocking_connection +import pika.channel +import pika.spec +import pytest from instana.singletons import agent, tracer -class _TestPika(unittest.TestCase): +class _TestPika: @staticmethod - @mock.patch('pika.connection.Connection') - def _create_connection(connection_class_mock=None): + @mock.patch("pika.connection.Connection") + def _create_connection(connection_class_mock=None) -> object: return connection_class_mock() - def _create_obj(self): + def _create_obj(self) -> NotImplementedError: raise NotImplementedError() - def setUp(self): - self.recorder = tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.connection = self._create_connection() self._on_openok_callback = mock.Mock() self.obj = self._create_obj() - - def tearDown(self): + yield + # teardown del self.connection del self._on_openok_callback del self.obj + # Ensure that allow_exit_as_root has the default value agent.options.allow_exit_as_root = False -class TestPikaChannel(_TestPika): - def _create_obj(self): - return pika.channel.Channel(self.connection, 1, self._on_openok_callback) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish(self, send_method, _unused): - self.obj._set_state(self.obj.OPEN) - - with tracer.start_active_span("testing"): - self.obj.basic_publish("test.exchange", "test.queue", "Hello!") - - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - - rabbitmq_span = spans[0] - test_span = spans[1] - - self.assertIsNone(tracer.active_span) - - # Same traceId - self.assertEqual(test_span.t, rabbitmq_span.t) - - # Parent relationships - self.assertEqual(rabbitmq_span.p, test_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rabbitmq_span.ec) - - # Span tags - self.assertEqual("test.exchange", rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual('publish', rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["key"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish_as_root_exit_span(self, send_method, _unused): - agent.options.allow_exit_as_root = True - self.obj._set_state(self.obj.OPEN) - self.obj.basic_publish("test.exchange", "test.queue", "Hello!") - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) +class TestPikaBlockingChannel(_TestPika): + @mock.patch("pika.channel.Channel", spec=pika.channel.Channel) + def _create_obj( + self, channel_impl: mock.MagicMock + ) -> pika.adapters.blocking_connection.BlockingChannel: + self.impl = channel_impl() + self.impl.channel_number = 1 - # Parent relationships - self.assertIsNone(rabbitmq_span.p, None) + return pika.adapters.blocking_connection.BlockingChannel( + self.impl, self.connection + ) - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def _generate_delivery( + self, consumer_tag: str, properties: pika.BasicProperties, body: str + ) -> None: + from pika.adapters.blocking_connection import _ConsumerDeliveryEvt - # Span tags - self.assertEqual("test.exchange", rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual('publish', rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["key"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + # Wait until queue consumer is initialized + while self.obj._queue_consumer_generator is None: + time.sleep(0.25) - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish_with_headers(self, send_method, _unused): - self.obj._set_state(self.obj.OPEN) + method = pika.spec.Basic.Deliver(consumer_tag=consumer_tag) + self.obj._on_consumer_generator_event( + _ConsumerDeliveryEvt(method, properties, body) + ) - with tracer.start_active_span("testing"): - self.obj.basic_publish("test.exchange", - "test.queue", - "Hello!", - pika.BasicProperties(headers={ - "X-Custom-1": "test" - })) + def test_consume(self) -> None: + consumed_deliveries = [] - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + def __consume() -> None: + for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): + # Skip deliveries generated due to inactivity + if delivery is not None and any(delivery): + consumed_deliveries.append(delivery) - rabbitmq_span = spans[0] - test_span = spans[1] + break - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-Custom-1": "test", - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Get') - def test_basic_get(self, _unused): - self.obj._set_state(self.obj.OPEN) + consumer_tag = "test.consumer" - body = "Hello!" - properties = pika.BasicProperties() + self.impl.basic_consume.return_value = consumer_tag + self.impl._generate_consumer_tag.return_value = consumer_tag + self.impl._consumers = {} - method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) - header_frame = pika.frame.Header(1, len(body), properties) + t = threading.Thread(target=__consume) + t.start() - cb = mock.Mock() + self._generate_delivery(consumer_tag, pika.BasicProperties(), "Hello!") - self.obj.basic_get("test.queue", cb) - self.obj._on_getok(method_frame, header_frame, body) + t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s # Error logging - self.assertIsNone(rabbitmq_span.ec) + assert not rabbitmq_span.ec # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - cb.assert_called_once_with(self.obj, pika.spec.Basic.GetOk, properties, body) - - @mock.patch('pika.spec.Basic.Get') - def test_basic_get_with_trace_context(self, _unused): - self.obj._set_state(self.obj.OPEN) - - body = "Hello!" - properties = pika.BasicProperties(headers={ - "X-INSTANA-T": "0000000000000001", - "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) - - method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) - header_frame = pika.frame.Header(1, len(body), properties) - - cb = mock.Mock() - - self.obj.basic_get("test.queue", cb) - self.obj._on_getok(method_frame, header_frame, body) - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) - - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) - - @mock.patch('pika.spec.Basic.Consume') - def test_basic_consume(self, _unused): - self.obj._set_state(self.obj.OPEN) + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 - body = "Hello!" - properties = pika.BasicProperties() - - method_frame = pika.frame.Method(1, pika.spec.Basic.Deliver(consumer_tag="test")) - header_frame = pika.frame.Header(1, len(body), properties) - - cb = mock.Mock() - - self.obj.basic_consume("test.queue", cb, consumer_tag="test") - self.obj._on_deliver(method_frame, header_frame, body) - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] + assert len(consumed_deliveries) == 1 - self.assertIsNone(tracer.active_span) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + def test_consume_with_trace_context(self) -> None: + consumed_deliveries = [] - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def __consume(): + for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): + # Skip deliveries generated due to inactivity + if delivery is not None and any(delivery): + consumed_deliveries.append(delivery) + break - # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + consumer_tag = "test.consumer" - cb.assert_called_once_with(self.obj, method_frame.method, properties, body) + self.impl.basic_consume.return_value = consumer_tag + self.impl._generate_consumer_tag.return_value = consumer_tag + self.impl._consumers = {} - @mock.patch('pika.spec.Basic.Consume') - def test_basic_consume_with_trace_context(self, _unused): - self.obj._set_state(self.obj.OPEN) + t = threading.Thread(target=__consume) + t.start() - body = "Hello!" - properties = pika.BasicProperties(headers={ + instana_headers = { "X-INSTANA-T": "0000000000000001", "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) - - method_frame = pika.frame.Method(1, pika.spec.Basic.Deliver(consumer_tag="test")) - header_frame = pika.frame.Header(1, len(body), properties) + "X-INSTANA-L": "1", + } + self._generate_delivery( + consumer_tag, + pika.BasicProperties(headers=instana_headers), + "Hello!", + ) - cb = mock.Mock() - - self.obj.basic_consume(queue="test.queue", on_message_callback=cb, consumer_tag="test") - self.obj._on_deliver(method_frame, header_frame, body) + t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + def test_consume_with_not_GeneratorType(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.isinstance", + return_value=False, + ) -class TestPikaBlockingChannel(_TestPika): - @mock.patch('pika.channel.Channel', spec=pika.channel.Channel) - def _create_obj(self, channel_impl): - self.impl = channel_impl() - self.impl.channel_number = 1 - - return pika.adapters.blocking_connection.BlockingChannel(self.impl, self.connection) - - def _generate_delivery(self, consumer_tag, properties, body): - from pika.adapters.blocking_connection import _ConsumerDeliveryEvt - - # Wait until queue consumer is initialized - while self.obj._queue_consumer_generator is None: - time.sleep(0.25) - - method = pika.spec.Basic.Deliver(consumer_tag=consumer_tag) - self.obj._on_consumer_generator_event(_ConsumerDeliveryEvt(method, properties, body)) - - def test_consume(self): consumed_deliveries = [] - def __consume(): + def __consume() -> None: for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): # Skip deliveries generated due to inactivity if delivery is not None and any(delivery): @@ -344,35 +193,17 @@ def __consume(): t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) + assert len(spans) == 0 - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) - - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def test_consume_with_any_yielded(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.any", + return_value=False, + ) - # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - self.assertEqual(1, len(consumed_deliveries)) - - def test_consume_with_trace_context(self): consumed_deliveries = [] - def __consume(): + def __consume() -> None: for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): # Skip deliveries generated due to inactivity if delivery is not None and any(delivery): @@ -389,51 +220,45 @@ def __consume(): t = threading.Thread(target=__consume) t.start() - self._generate_delivery(consumer_tag, pika.BasicProperties(headers={ - "X-INSTANA-T": "0000000000000001", - "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }), "Hello!") + self._generate_delivery(consumer_tag, pika.BasicProperties(), "Hello!") t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) - - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert len(spans) == 0 class TestPikaBlockingChannelBlockingConnection(_TestPika): - @mock.patch('pika.adapters.blocking_connection.BlockingConnection', autospec=True) - def _create_connection(self, connection=None): + @mock.patch("pika.adapters.blocking_connection.BlockingConnection", autospec=True) + def _create_connection(self, connection: Optional[mock.MagicMock] = None) -> object: connection._impl = mock.create_autospec(pika.connection.Connection) connection._impl.params = pika.connection.Parameters() return connection - @mock.patch('pika.channel.Channel', spec=pika.channel.Channel) - def _create_obj(self, channel_impl): + @mock.patch("pika.channel.Channel", spec=pika.channel.Channel) + def _create_obj( + self, channel_impl: mock.MagicMock + ) -> pika.adapters.blocking_connection.BlockingChannel: self.impl = channel_impl() self.impl.channel_number = 1 - return pika.adapters.blocking_connection.BlockingChannel(self.impl, self.connection) + return pika.adapters.blocking_connection.BlockingChannel( + self.impl, self.connection + ) - def _generate_delivery(self, method, properties, body): + def _generate_delivery( + self, + method: pika.spec.Basic.Deliver, + properties: pika.BasicProperties, + body: str, + ) -> None: from pika.adapters.blocking_connection import _ConsumerDeliveryEvt + evt = _ConsumerDeliveryEvt(method, properties, body) self.obj._add_pending_event(evt) self.obj._dispatch_events() - def test_basic_consume(self): + def test_basic_consume(self) -> None: consumer_tag = "test.consumer" self.impl.basic_consume.return_value = consumer_tag @@ -449,28 +274,26 @@ def test_basic_consume(self): self._generate_delivery(method, properties, body) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s # Error logging - self.assertIsNone(rabbitmq_span.ec) + assert not rabbitmq_span.ec # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 cb.assert_called_once_with(self.obj, method, properties, body) @@ -485,25 +308,320 @@ def test_basic_consume_with_trace_context(self): self.obj.basic_consume(queue="test.queue", on_message_callback=cb) body = "Hello!" - properties = pika.BasicProperties(headers={ + instana_headers = { "X-INSTANA-T": "0000000000000001", "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) method = pika.spec.Basic.Deliver(consumer_tag) self._generate_delivery(method, properties, body) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Trace context propagation + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) + + # A new span has been started + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + + +class TestPikaChannel(_TestPika): + def _create_obj(self) -> pika.channel.Channel: + return pika.channel.Channel(self.connection, 1, self._on_openok_callback) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish(self, send_method, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + rabbitmq_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == rabbitmq_span.t + + # Parent relationships + assert rabbitmq_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not rabbitmq_span.ec + + # Span tags + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_as_root_exit_span(self, send_method, _unused) -> None: + agent.options.allow_exit_as_root = True + self.obj._set_state(self.obj.OPEN) + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Parent relationships + assert not rabbitmq_span.p + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_with_headers(self, send_method, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish( + "test.exchange", + "test.queue", + "Hello!", + pika.BasicProperties(headers={"X-Custom-1": "test"}), + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 2 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-Custom-1": "test", + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_tracing_off(self, send_method, _unused, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.tracing_is_off", + return_value=True, + ) + + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + # Span names are not "rabbitmq" + for span in spans: + assert span.n != "rabbitmq" + + @mock.patch("pika.spec.Basic.Get") + def test_basic_get(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + properties = pika.BasicProperties() + + method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_get("test.queue", cb) + self.obj._on_getok(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # A new span has been started + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + cb.assert_called_once_with(self.obj, pika.spec.Basic.GetOk, properties, body) + + @mock.patch("pika.spec.Basic.Get") + def test_basic_get_with_trace_context(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + instana_headers = { + "X-INSTANA-T": "0000000000000001", + "X-INSTANA-S": "0000000000000002", + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) + + method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_get("test.queue", cb) + self.obj._on_getok(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Trace context propagation + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) + + # A new span has been started + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + + @mock.patch("pika.spec.Basic.Consume") + def test_basic_consume(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + properties = pika.BasicProperties() + + method_frame = pika.frame.Method( + 1, pika.spec.Basic.Deliver(consumer_tag="test") + ) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_consume("test.queue", cb, consumer_tag="test") + self.obj._on_deliver(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # A new span has been started + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + cb.assert_called_once_with(self.obj, method_frame.method, properties, body) + + @mock.patch("pika.spec.Basic.Consume") + def test_basic_consume_with_trace_context(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + instana_headers = { + "X-INSTANA-T": "0000000000000001", + "X-INSTANA-S": "0000000000000002", + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) + + method_frame = pika.frame.Method( + 1, pika.spec.Basic.Deliver(consumer_tag="test") + ) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_consume( + queue="test.queue", on_message_callback=cb, consumer_tag="test" + ) + self.obj._on_deliver(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index 7a76d6b8..17b88bb4 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -2,9 +2,11 @@ # (c) Copyright Instana Inc. 2020 import logging -import unittest +import pytest -from ..helpers import testenv +from typing import Generator +from instana.instrumentation.psycopg2 import register_json_with_instana +from tests.helpers import testenv from instana.singletons import agent, tracer import psycopg2 @@ -14,15 +16,15 @@ logger = logging.getLogger(__name__) -class TestPsycoPG2(unittest.TestCase): - def setUp(self): - deprecated_param_name = self.shortDescription() == 'test_deprecated_parameter_database' +class TestPsycoPG2: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: kwargs = { - 'host': testenv['postgresql_host'], - 'port': testenv['postgresql_port'], - 'user': testenv['postgresql_user'], - 'password': testenv['postgresql_pw'], - 'dbname' if not deprecated_param_name else 'database': testenv['postgresql_db'], + "host": testenv["postgresql_host"], + "port": testenv["postgresql_port"], + "user": testenv["postgresql_user"], + "password": testenv["postgresql_pw"], + "dbname": testenv["postgresql_db"], } self.db = psycopg2.connect(**kwargs) @@ -48,341 +50,364 @@ def setUp(self): cursor = self.db.cursor() cursor.execute(database_setup_query) self.db.commit() - cursor.close() - self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and not self.cursor.connection.closed: - self.cursor.close() + self.cursor.close() if self.db and not self.db.closed: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False - def test_vanilla_query(self): - self.assertTrue(psycopg2.extras.register_uuid(None, self.db)) - self.assertTrue(psycopg2.extras.register_uuid(None, self.db.cursor())) + def test_register_json(self) -> None: + resp = register_json_with_instana(conn_or_curs=self.db) + assert resp[0].values[0] == 114 + assert resp[1].values[0] == 199 + + def test_vanilla_query(self) -> None: + assert psycopg2.extras.register_uuid(None, self.db) + assert psycopg2.extras.register_uuid(None, self.db.cursor()) self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount - self.assertEqual(1, affected_rows) + assert affected_rows == 1 result = self.cursor.fetchone() - self.assertEqual(6, len(result)) + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_basic_query(self): - with tracer.start_active_span('test'): + def test_basic_query(self) -> None: + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_basic_query_as_root_exit_span(self): + def test_basic_query_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 db_span = spans[0] - self.assertIsNone(db_span.ec) - - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) - - def test_basic_insert(self): - with tracer.start_active_span('test'): - self.cursor.execute("""INSERT INTO users(name, email) VALUES(%s, %s)""", ('beaker', 'beaker@muppets.com')) + assert not db_span.ec + + assert db_span.n, "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] + + def test_basic_insert(self) -> None: + with tracer.start_as_current_span("test"): + self.cursor.execute( + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) affected_rows = self.cursor.rowcount - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - - self.assertIsNone(db_span.ec) - - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) - - def test_executemany(self): - with tracer.start_active_span('test'): - self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s + + assert not db_span.ec + + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert ( + db_span.data["pg"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] + + def test_executemany(self) -> None: + with tracer.start_as_current_span("test"): + self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) affected_rows = self.cursor.rowcount self.db.commit() - self.assertEqual(2, affected_rows) + assert affected_rows == 2 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert ( + db_span.data["pg"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) - def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - self.assertIsInstance(callproc_result, tuple) + def test_call_proc(self) -> None: + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) + + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "test_proc" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_error_capture(self): + def test_error_capture(self) -> None: affected_rows = result = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from blah""") affected_rows = self.cursor.rowcount self.cursor.fetchone() except Exception: pass - self.assertIsNone(affected_rows) - self.assertIsNone(result) + assert not affected_rows + assert not result spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertEqual(1, db_span.ec) - self.assertEqual(db_span.data["pg"]["error"], 'relation "blah" does not exist\nLINE 1: SELECT * from blah\n ^\n') + assert db_span.ec == 2 + assert db_span.data["pg"]["error"] == ( + 'relation "blah" does not exist\nLINE 1: SELECT * from blah\n ^\n' + ) - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from blah" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] # Added to validate unicode support and register_type. - def test_unicode(self): + def test_unicode(self) -> None: ext.register_type(ext.UNICODE, self.cursor) snowman = "\u2603" self.cursor.execute("delete from users where id in (1,2,3)") # unicode in statement - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, [(1, 'x')]) + psycopg2.extras.execute_batch( + self.cursor, + "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, + [(1, "x")], + ) self.cursor.execute("select id, name from users where id = 1") - self.assertEqual(self.cursor.fetchone(), (1, 'x')) + assert self.cursor.fetchone() == (1, "x") # unicode in data - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%s, %s)", [(2, snowman)]) + psycopg2.extras.execute_batch( + self.cursor, "insert into users (id, name) values (%s, %s)", [(2, snowman)] + ) self.cursor.execute("select id, name from users where id = 2") - self.assertEqual(self.cursor.fetchone(), (2, snowman)) + assert self.cursor.fetchone() == (2, snowman) # unicode in both - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, [(3, snowman)]) + psycopg2.extras.execute_batch( + self.cursor, + "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, + [(3, snowman)], + ) self.cursor.execute("select id, name from users where id = 3") - self.assertEqual(self.cursor.fetchone(), (3, snowman)) + assert self.cursor.fetchone() == (3, snowman) - def test_register_type(self): + def test_register_type(self) -> None: import uuid oid1 = 2950 oid2 = 2951 - ext.UUID = ext.new_type((oid1,), "UUID", lambda data, cursor: data and uuid.UUID(data) or None) + ext.UUID = ext.new_type( + (oid1,), "UUID", lambda data, cursor: data and uuid.UUID(data) or None + ) ext.UUIDARRAY = ext.new_array_type((oid2,), "UUID[]", ext.UUID) ext.register_type(ext.UUID, self.cursor) ext.register_type(ext.UUIDARRAY, self.cursor) - def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_connect_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_connect_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - - self.assertIsNone(db_span.ec) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert not db_span.ec - def test_deprecated_parameter_database(self): - """test_deprecated_parameter_database""" + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - with tracer.start_active_span('test'): + def test_deprecated_parameter_database(self) -> None: + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] diff --git a/tests/clients/test_pymongo.py b/tests/clients/test_pymongo.py index b54b0525..251f0b40 100644 --- a/tests/clients/test_pymongo.py +++ b/tests/clients/test_pymongo.py @@ -2,256 +2,289 @@ # (c) Copyright Instana Inc. 2020 import json -import unittest import logging +from typing import Generator -from ..helpers import testenv -from instana.singletons import agent, tracer - -import pymongo import bson +import pymongo +import pytest -logger = logging.getLogger(__name__) +from instana.singletons import agent, tracer +from instana.span.span import get_current_span +from tests.helpers import testenv -pymongoversion = unittest.skipIf( - pymongo.version_tuple >= (4, 0), reason="map reduce is removed in pymongo 4.0" -) +logger = logging.getLogger(__name__) -class TestPyMongoTracer(unittest.TestCase): - def setUp(self): - self.client = pymongo.MongoClient(host=testenv['mongodb_host'], port=int(testenv['mongodb_port']), - username=testenv['mongodb_user'], password=testenv['mongodb_pw']) +class TestPyMongoTracer: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.client = pymongo.MongoClient( + host=testenv["mongodb_host"], + port=int(testenv["mongodb_port"]), + username=testenv["mongodb_user"], + password=testenv["mongodb_pw"], + ) self.client.test.records.delete_many(filter={}) - - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() - - def tearDown(self): + yield self.client.close() agent.options.allow_exit_as_root = False - def test_successful_find_query(self): - with tracer.start_active_span("test"): + def test_successful_find_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.find_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "find") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "find" - self.assertEqual(db_span.data["mongo"]["filter"], '{"type": "string"}') - self.assertIsNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"type": "string"}' + assert not db_span.data["mongo"]["json"] - def test_successful_find_query_as_root_span(self): + def test_successful_find_query_as_root_span(self) -> None: agent.options.allow_exit_as_root = True self.client.test.records.find_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 1) + assert len(spans) == 1 db_span = spans[0] - self.assertEqual(db_span.p, None) - - self.assertIsNone(db_span.ec) + assert not db_span.p + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "find") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "find" - self.assertEqual(db_span.data["mongo"]["filter"], '{"type": "string"}') - self.assertIsNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"type": "string"}' + assert not db_span.data["mongo"]["json"] - def test_successful_insert_query(self): - with tracer.start_active_span("test"): + def test_successful_insert_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.insert_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "insert") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "insert" - self.assertIsNone(db_span.data["mongo"]["filter"]) + assert not db_span.data["mongo"]["filter"] - def test_successful_update_query(self): - with tracer.start_active_span("test"): - self.client.test.records.update_one({"type": "string"}, {"$set": {"type": "int"}}) - - self.assertIsNone(tracer.active_span) + def test_successful_update_query(self) -> None: + with tracer.start_as_current_span("test"): + self.client.test.records.update_one( + {"type": "string"}, {"$set": {"type": "int"}} + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "update") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "update" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({ - "q": {"type": "string"}, - "u": {"$set": {"type": "int"}}, - "multi": False, - "upsert": False - }, payload) - - def test_successful_delete_query(self): - with tracer.start_active_span("test"): + assert { + "q": {"type": "string"}, + "u": {"$set": {"type": "int"}}, + "multi": False, + "upsert": False, + } in payload + + def test_successful_delete_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.delete_one(filter={"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "delete") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "delete" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({"q": {"type": "string"}, "limit": 1}, payload) + assert {"q": {"type": "string"}, "limit": 1} in payload - def test_successful_aggregate_query(self): - with tracer.start_active_span("test"): + def test_successful_aggregate_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.count_documents({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "aggregate") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "aggregate" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({"$match": {"type": "string"}}, payload) + assert {"$match": {"type": "string"}} in payload - @pymongoversion - def test_successful_map_reduce_query(self): + @pytest.mark.skipif( + pymongo.version_tuple >= (4, 0), reason="map reduce is removed in pymongo 4.0" + ) + def test_successful_map_reduce_query(self) -> None: mapper = "function () { this.tags.forEach(function(z) { emit(z, 1); }); }" reducer = "function (key, values) { return len(values); }" - with tracer.start_active_span("test"): - self.client.test.records.map_reduce(bson.code.Code(mapper), bson.code.Code(reducer), "results", - query={"x": {"$lt": 2}}) - - self.assertIsNone(tracer.active_span) + with tracer.start_as_current_span("test"): + self.client.test.records.map_reduce( + bson.code.Code(mapper), + bson.code.Code(reducer), + "results", + query={"x": {"$lt": 2}}, + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"].lower(), - "mapreduce") # mapreduce command was renamed to mapReduce in pymongo 3.9.0 + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert ( + db_span.data["mongo"]["command"].lower() == "mapreduce" + ) # mapreduce command was renamed to mapReduce in pymongo 3.9.0 - self.assertEqual(db_span.data["mongo"]["filter"], '{"x": {"$lt": 2}}') - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"x": {"$lt": 2}}' + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertEqual(payload["map"], {"$code": mapper}, db_span.data["mongo"]["json"]) - self.assertEqual(payload["reduce"], {"$code": reducer}, db_span.data["mongo"]["json"]) - - def test_successful_mutiple_queries(self): - with tracer.start_active_span("test"): - self.client.test.records.bulk_write([pymongo.InsertOne({"type": "string"}), - pymongo.UpdateOne({"type": "string"}, {"$set": {"type": "int"}}), - pymongo.DeleteOne({"type": "string"})]) - - self.assertIsNone(tracer.active_span) + assert payload["map"], {"$code": mapper} == db_span.data["mongo"]["json"] + assert payload["reduce"], {"$code": reducer} == db_span.data["mongo"]["json"] + + def test_successful_mutiple_queries(self) -> None: + with tracer.start_as_current_span("test"): + self.client.test.records.bulk_write( + [ + pymongo.InsertOne({"type": "string"}), + pymongo.UpdateOne({"type": "string"}, {"$set": {"type": "int"}}), + pymongo.DeleteOne({"type": "string"}), + ] + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 4) + assert len(spans) == 4 test_span = spans.pop() seen_span_ids = set() commands = [] for span in spans: - self.assertEqual(test_span.t, span.t) - self.assertEqual(span.p, test_span.s) + assert test_span.t == span.t + assert span.p == test_span.s # check if all spans got a unique id - self.assertNotIn(span.s, seen_span_ids) + assert span.s not in seen_span_ids seen_span_ids.add(span.s) commands.append(span.data["mongo"]["command"]) # ensure spans are ordered the same way as commands - self.assertListEqual(commands, ["insert", "update", "delete"]) - + assert commands == ["insert", "update", "delete"] diff --git a/tests/clients/test_pymysql.py b/tests/clients/test_pymysql.py index 4479b698..8e4793d5 100644 --- a/tests/clients/test_pymysql.py +++ b/tests/clients/test_pymysql.py @@ -1,26 +1,25 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging -import unittest +import time +import pytest import pymysql -from ..helpers import testenv +from typing import Generator +from tests.helpers import testenv from instana.singletons import agent, tracer -logger = logging.getLogger(__name__) - -class TestPyMySQL(unittest.TestCase): - def setUp(self): - deprecated_param_name = self.shortDescription() == 'test_deprecated_parameter_db' +class TestPyMySQL: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: kwargs = { - 'host': testenv['mysql_host'], - 'port': testenv['mysql_port'], - 'user': testenv['mysql_user'], - 'passwd': testenv['mysql_pw'], - 'database' if not deprecated_param_name else 'db': testenv['mysql_db'], + "host": testenv["mysql_host"], + "port": testenv["mysql_port"], + "user": testenv["mysql_user"], + "passwd": testenv["mysql_pw"], + "database": testenv["mysql_db"], } self.db = pymysql.connect(**kwargs) @@ -39,303 +38,314 @@ def setUp(self): END """ setup_cursor = self.db.cursor() - for s in database_setup_query.split('|'): - setup_cursor.execute(s) + for s in database_setup_query.split("|"): + setup_cursor.execute(s) self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and self.cursor.connection.open: - self.cursor.close() + self.cursor.close() if self.db and self.db.open: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False - def test_vanilla_query(self): + def test_vanilla_query(self) -> None: affected_rows = self.cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 result = self.cursor.fetchone() - self.assertEqual(3, len(result)) + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_basic_query(self): - with tracer.start_active_span('test'): + def test_basic_query(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_basic_query_as_root_exit_span(self): + def test_basic_query_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 db_span = spans[0] - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_query_with_params(self): - with tracer.start_active_span('test'): + def test_query_with_params(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users where id=1""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users where id=?') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users where id=?" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_basic_insert(self): - with tracer.start_active_span('test'): + def test_basic_insert(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( - """INSERT INTO users(name, email) VALUES(%s, %s)""", - ('beaker', 'beaker@muppets.com')) + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - - self.assertIsNone(db_span.ec) - - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) - - def test_executemany(self): - with tracer.start_active_span('test'): - affected_rows = self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s + + assert not db_span.ec + + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] + + def test_executemany(self) -> None: + with tracer.start_as_current_span("test"): + affected_rows = self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) self.db.commit() - self.assertEqual(2, affected_rows) + assert affected_rows == 2 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + def test_call_proc(self) -> None: + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) - self.assertIsInstance(callproc_result, tuple) + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "test_proc" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_error_capture(self): + def test_error_capture(self) -> None: affected_rows = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass - self.assertIsNone(affected_rows) + assert not affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - self.assertEqual(1, db_span.ec) - - self.assertEqual(db_span.data["mysql"]["error"], u'(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db']) - - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) - - def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s + assert db_span.ec == 2 + + assert ( + db_span.data["mysql"]["error"] + == f"(1146, \"Table '{testenv['mysql_db']}.blah' doesn't exist\")" + ) + + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from blah" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] + + def test_connect_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_connect_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_deprecated_parameter_db(self): + def test_deprecated_parameter_db(self) -> None: """test_deprecated_parameter_db""" - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] diff --git a/tests/clients/test_redis.py b/tests/clients/test_redis.py index e01a139b..4fa93e5c 100644 --- a/tests/clients/test_redis.py +++ b/tests/clients/test_redis.py @@ -1,376 +1,456 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +import logging +from typing import Generator +from unittest.mock import patch +import pytest import redis -from redis.sentinel import Sentinel -from ..helpers import testenv +from instana.span.span import get_current_span +from tests.helpers import testenv from instana.singletons import agent, tracer -class TestRedis(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestRedis: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() - - # self.sentinel = Sentinel([(testenv['redis_host'], 26379)], socket_timeout=0.1) - # self.sentinel_master = self.sentinel.discover_master('mymaster') - # self.client = redis.Redis(host=self.sentinel_master[0]) - - self.client = redis.Redis(host=testenv['redis_host']) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + self.client = redis.Redis(host=testenv["redis_host"], db=testenv["redis_db"]) + yield agent.options.allow_exit_as_root = False - def test_vanilla(self): - self.client.set('instrument', 'piano') - result = self.client.get('instrument') - - def test_set_get(self): + def test_set_get(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + with tracer.start_as_current_span("test"): + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_set_get_as_root_span(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_set_get_as_root_span(self) -> None: agent.options.allow_exit_as_root = True - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Parent relationships - self.assertEqual(rs1_span.p, None) - self.assertEqual(rs2_span.p, None) - self.assertEqual(rs3_span.p, None) + assert not rs1_span.p + assert not rs2_span.p + assert not rs3_span.p # Error logging - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_set_incr_get(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_set_incr_get(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('counter', '10') - self.client.incr('counter') - result = self.client.get('counter') + with tracer.start_as_current_span("test"): + self.client.set("counter", "10") + self.client.incr("counter") + result = self.client.get("counter") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'11', result) + assert result == b"11" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("INCRBY", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "INCRBY" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_old_redis_client(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_old_redis_client(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + with tracer.start_as_current_span("test"): + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] - # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_pipelined_requests(self): + # Redis span 3 + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_pipelined_requests(self) -> None: result = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): pipe = self.client.pipeline() - pipe.set('foox', 'barX') - pipe.set('fooy', 'barY') - pipe.get('foox') + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") result = pipe.execute() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - self.assertEqual([True, True, b'barX'], result) + assert result == [True, True, b"barX"] rs1_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) + assert rs1_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) + assert rs1_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) + assert not test_span.ec + assert not rs1_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("PIPELINE", rs1_span.data["redis"]["command"]) - self.assertEqual(['SET', 'SET', 'GET'], rs1_span.data["redis"]["subCommands"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "PIPELINE" + assert rs1_span.data["redis"]["subCommands"] == ["SET", "SET", "GET"] + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 + + @patch( + "instana.instrumentation.redis.collect_attributes", + side_effect=Exception("test-error"), + ) + @patch("instana.span.span.InstanaSpan.record_exception") + def test_execute_command_with_instana_exception(self, mock_record_func, _) -> None: + with tracer.start_as_current_span("test"), pytest.raises( + Exception, match="test-error" + ): + self.client.set("counter", "10") + mock_record_func.assert_called() + + def test_execute_comand_with_instana_tracing_off(self) -> None: + with tracer.start_as_current_span("redis"): + response = self.client.set("counter", "10") + assert response + + def test_execute_with_instana_tracing_off(self) -> None: + result = None + with tracer.start_as_current_span("redis"): + pipe = self.client.pipeline() + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") + result = pipe.execute() + assert result == [True, True, b"barX"] + + def test_execute_with_instana_exception( + self, caplog: pytest.LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.redis.collect_attributes", + side_effect=Exception("test-error"), + ): + pipe = self.client.pipeline() + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") + pipe.execute() + assert "Error collecting pipeline commands" in caplog.messages diff --git a/tests/clients/test_sqlalchemy.py b/tests/clients/test_sqlalchemy.py index 68afa1ec..6aef6fc9 100644 --- a/tests/clients/test_sqlalchemy.py +++ b/tests/clients/test_sqlalchemy.py @@ -1,244 +1,297 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +from typing import Generator -from ..helpers import testenv -from instana.singletons import agent, tracer - -from sqlalchemy.orm import sessionmaker -from sqlalchemy.exc import OperationalError -from sqlalchemy.orm import declarative_base +import pytest from sqlalchemy import Column, Integer, String, create_engine, text +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import declarative_base, sessionmaker +from instana.singletons import agent, tracer +from instana.span.span import get_current_span +from tests.helpers import testenv + +engine = create_engine( + f"postgresql://{testenv['postgresql_user']}:{testenv['postgresql_pw']}@{testenv['postgresql_host']}:{testenv['postgresql_port']}/{testenv['postgresql_db']}" +) -engine = create_engine("postgresql://%s:%s@%s/%s" % (testenv['postgresql_user'], testenv['postgresql_pw'], - testenv['postgresql_host'], testenv['postgresql_db'])) +Session = sessionmaker(bind=engine) Base = declarative_base() + class StanUser(Base): - __tablename__ = 'churchofstan' + __tablename__ = "churchofstan" - id = Column(Integer, primary_key=True) - name = Column(String) - fullname = Column(String) - password = Column(String) + id = Column(Integer, primary_key=True) + name = Column(String) + fullname = Column(String) + password = Column(String) - def __repr__(self): + def __repr__(self) -> None: return "" % ( - self.name, self.fullname, self.password) - -Base.metadata.create_all(engine) - -stan_user = StanUser(name='IAmStan', fullname='Stan Robot', password='3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR') -stan_user2 = StanUser(name='IAmStanToo', fullname='Stan Robot 2', password='3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR') - -Session = sessionmaker(bind=engine) -Session.configure(bind=engine) - -sqlalchemy_url = 'postgresql://%s/%s' % (testenv['postgresql_host'], testenv['postgresql_db']) - - -class TestSQLAlchemy(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + self.name, + self.fullname, + self.password, + ) + + +@pytest.fixture(scope="class") +def db_setup() -> None: + with tracer.start_as_current_span("metadata") as span: + Base.metadata.create_all(engine) + span.end() + + +stan_user = StanUser( + name="IAmStan", + fullname="Stan Robot", + password="3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR", +) +stan_user2 = StanUser( + name="IAmStanToo", + fullname="Stan Robot 2", + password="3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR", +) + +sqlalchemy_url = f"postgresql://{testenv['postgresql_host']}:{testenv['postgresql_port']}/{testenv['postgresql_db']}" + + +@pytest.mark.usefixtures("db_setup") +class TestSQLAlchemy: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() self.session = Session() - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield + """Ensure that allow_exit_as_root has the default value""" + self.session.close() agent.options.allow_exit_as_root = False - def test_session_add(self): - with tracer.start_active_span('test'): + def test_session_add(self) -> None: + with tracer.start_as_current_span("test"): self.session.add(stan_user) self.session.commit() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) sql_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span.t) + assert sql_span.t == test_span.t # Parent relationships - self.assertEqual(sql_span.p, test_span.s) + assert sql_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(sql_span.ec) + assert not test_span.ec + assert not sql_span.ec # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id', sql_span.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_session_add_as_root_exit_span(self): + assert sql_span.n == "sqlalchemy" + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id" + == sql_span.data["sqlalchemy"]["sql"] + ) + assert not sql_span.data["sqlalchemy"]["err"] + + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_session_add_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.session.add(stan_user2) self.session.commit() spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 sql_span = spans[0] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Parent relationships - self.assertEqual(sql_span.p, None) + assert not sql_span.p # Error logging - self.assertIsNone(sql_span.ec) + assert not sql_span.ec # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id', sql_span.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_transaction(self): - result = None - with tracer.start_active_span('test'): + assert sql_span.n == "sqlalchemy" + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id" + == sql_span.data["sqlalchemy"]["sql"] + ) + assert not sql_span.data["sqlalchemy"]["err"] + + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_transaction(self) -> None: + with tracer.start_as_current_span("test"): with engine.begin() as connection: - result = connection.execute(text("select 1")) - result = connection.execute(text("select (name, fullname, password) from churchofstan where name='doesntexist'")) + connection.execute(text("select 1")) + connection.execute( + text( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + ) + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 sql_span0 = spans[0] sql_span1 = spans[1] test_span = spans[2] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span0.t) - self.assertEqual(test_span.t, sql_span1.t) + assert sql_span0.t == test_span.t + assert sql_span1.t == test_span.t # Parent relationships - self.assertEqual(sql_span0.p, test_span.s) - self.assertEqual(sql_span1.p, test_span.s) + assert sql_span0.p == test_span.s + assert sql_span1.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(sql_span0.ec) - self.assertIsNone(sql_span1.ec) + assert not test_span.ec + assert not sql_span0.ec + assert not sql_span1.ec # SQLAlchemy span0 - self.assertEqual('sqlalchemy', sql_span0.n) - self.assertFalse('custom' in sql_span0.data) - self.assertTrue('sqlalchemy' in sql_span0.data) + assert sql_span0.n == "sqlalchemy" + assert "custom" not in sql_span0.data + assert "sqlalchemy" in sql_span0.data - self.assertEqual('postgresql', sql_span0.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span0.data["sqlalchemy"]["url"]) - self.assertEqual('select 1', sql_span0.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span0.data["sqlalchemy"]["err"]) + assert sql_span0.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span0.data["sqlalchemy"]["url"] + assert sql_span0.data["sqlalchemy"]["sql"] == "select 1" + assert not sql_span0.data["sqlalchemy"]["err"] - self.assertIsNotNone(sql_span0.stack) - self.assertTrue(type(sql_span0.stack) is list) - self.assertGreater(len(sql_span0.stack), 0) + assert sql_span0.stack + assert isinstance(sql_span0.stack, list) + assert len(sql_span0.stack) > 0 # SQLAlchemy span1 - self.assertEqual('sqlalchemy', sql_span1.n) - self.assertFalse('custom' in sql_span1.data) - self.assertTrue('sqlalchemy' in sql_span1.data) - - self.assertEqual('postgresql', sql_span1.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span1.data["sqlalchemy"]["url"]) - self.assertEqual("select (name, fullname, password) from churchofstan where name='doesntexist'", sql_span1.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span1.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span1.stack) - self.assertTrue(type(sql_span1.stack) is list) - self.assertGreater(len(sql_span1.stack), 0) - - def test_error_logging(self): - with tracer.start_active_span('test'): + assert sql_span1.n == "sqlalchemy" + assert "custom" not in sql_span1.data + assert "sqlalchemy" in sql_span1.data + + assert sql_span1.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span1.data["sqlalchemy"]["url"] + assert ( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + == sql_span1.data["sqlalchemy"]["sql"] + ) + assert not sql_span1.data["sqlalchemy"]["err"] + + assert sql_span1.stack + assert isinstance(sql_span1.stack, list) + assert len(sql_span1.stack) > 0 + + def test_error_logging(self) -> None: + with tracer.start_as_current_span("test"): try: self.session.execute(text("htVwGrCwVThisIsInvalidSQLaw4ijXd88")) - self.session.commit() - except: + # self.session.commit() + except Exception: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 sql_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span.t) + assert sql_span.t == test_span.t # Parent relationships - self.assertEqual(sql_span.p, test_span.s) + assert sql_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIs(sql_span.ec, 1) + assert not test_span.ec + assert sql_span.ec == 1 # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('htVwGrCwVThisIsInvalidSQLaw4ijXd88', sql_span.data["sqlalchemy"]["sql"]) - self.assertIn('syntax error at or near "htVwGrCwVThisIsInvalidSQLaw4ijXd88', sql_span.data["sqlalchemy"]["err"]) - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_error_before_tracing(self): + assert sql_span.n == "sqlalchemy" + + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "htVwGrCwVThisIsInvalidSQLaw4ijXd88" == sql_span.data["sqlalchemy"]["sql"] + ) + assert ( + 'syntax error at or near "htVwGrCwVThisIsInvalidSQLaw4ijXd88' + in sql_span.data["sqlalchemy"]["err"] + ) + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_error_before_tracing(self) -> None: """Test the scenario, in which instana is loaded, - but connection fails before tracing begins. - This is typical in test container scenario, - where it is "normal" to just start hammering a database container - which is still starting and not ready to handle requests yet. - In this scenario it is important that we get - an sqlalachemy exception, and not something else - like an AttributeError. Because testcontainer has a logic - to retry in case of certain sqlalchemy exceptions but it - can't handle an AttributeError.""" + but connection fails before tracing begins. + This is typical in test container scenario, + where it is "normal" to just start hammering a database container + which is still starting and not ready to handle requests yet. + In this scenario it is important that we get + an sqlalachemy exception, and not something else + like an AttributeError. Because testcontainer has a logic + to retry in case of certain sqlalchemy exceptions but it + can't handle an AttributeError.""" # https://github.com/instana/python-sensor/issues/362 - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() - invalid_connection_url = 'postgresql://user1:pwd1@localhost:9999/mydb1' - with self.assertRaisesRegex( - OperationalError, - r'\(psycopg2.OperationalError\) connection .* failed.*' - ) as context_manager: + invalid_connection_url = "postgresql://user1:pwd1@localhost:9999/mydb1" + with pytest.raises( + OperationalError, + match=r"\(psycopg2.OperationalError\) connection .* failed.*", + ) as context_manager: engine = create_engine(invalid_connection_url) with engine.connect() as connection: - version, = connection.execute(text("select version()")).fetchone() - - the_exception = context_manager.exception - self.assertFalse(the_exception.connection_invalidated) + (version,) = connection.execute(text("select version()")).fetchone() + + the_exception = context_manager.value + assert not the_exception.connection_invalidated + + def test_if_not_tracing(self) -> None: + with engine.begin() as connection: + connection.execute(text("select 1")) + connection.execute( + text( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + ) + ) + + current_span = get_current_span() + assert not current_span.is_recording() diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index de8337de..77b49ece 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -1,311 +1,377 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import logging +import sys from multiprocessing.pool import ThreadPool from time import sleep -import unittest +from typing import TYPE_CHECKING, Generator -import urllib3 +import pytest import requests - -import tests.apps.flask_app -from ..helpers import testenv +import urllib3 +from instana.instrumentation.urllib3 import ( + _collect_kvs as collect_kvs, + _extract_custom_headers as extract_custom_headers, + collect_response, +) from instana.singletons import agent, tracer +import tests.apps.flask_app # noqa: F401 +from tests.helpers import testenv + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from pytest import LogCaptureFixture + -class TestUrllib3(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ +class TestUrllib3: + @pytest.fixture(autouse=True) + def _setup(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # 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): - """ Ensure that allow_exit_as_root has the default value """ + yield + # teardown + # Ensure that allow_exit_as_root has the default value""" agent.options.allow_exit_as_root = False - def test_vanilla_requests(self): - r = self.http.request('GET', testenv["wsgi_server"] + '/') - self.assertEqual(r.status, 200) + def test_vanilla_requests(self) -> None: + r = self.http.request("GET", testenv["flask_server"] + "/") + assert r.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - def test_parallel_requests(self): + def test_parallel_requests(self) -> None: http_pool_5 = urllib3.PoolManager(num_pools=5) def task(num): - r = http_pool_5.request('GET', testenv["wsgi_server"] + '/', fields={'num': num}) - return r + r = http_pool_5.request( + "GET", testenv["flask_server"] + "/", fields={"num": num} + ) + return r with ThreadPool(processes=5) as executor: # iterate over results as they become available for result in executor.map(task, (1, 2, 3, 4, 5)): - self.assertEqual(result.status, 200) + assert result.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) - nums = map(lambda s: s.data['http']['params'].split('=')[1], spans) - self.assertEqual(set(nums), set(('1', '2', '3', '4', '5'))) - - def test_customers_setup_zd_26466(self): - def make_request(u=None): - sleep(10) - x = requests.get(testenv["wsgi_server"] + '/') - sleep(10) - return x.status_code + assert len(spans) == 5 + nums = map(lambda s: s.data["http"]["params"].split("=")[1], spans) + assert set(nums) == set(("1", "2", "3", "4", "5")) + + @pytest.mark.skipif( + sys.platform == "darwin", + reason="Avoiding ConnectionError when calling multi processes of Flask app.", + ) + def test_customers_setup_zd_26466(self) -> None: + def make_request(u=None) -> int: + sleep(10) + x = requests.get(testenv["flask_server"] + "/") + sleep(10) + return x.status_code status = make_request() - #print(f'request made outside threadpool, instana should instrument - status: {status}') + assert status == 200 + # print(f'request made outside threadpool, instana should instrument - status: {status}') threadpool_size = 15 pool = ThreadPool(processes=threadpool_size) res = pool.map(make_request, [u for u in range(threadpool_size)]) - #print(f'requests made within threadpool, instana does not instrument - statuses: {res}') + # print(f'requests made within threadpool, instana does not instrument - statuses: {res}') spans = self.recorder.queued_spans() - self.assertEqual(16, len(spans)) - + assert len(spans) == 16 def test_get_request(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack + + # urllib3 + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_get_request_https(self): + request_url = "https://reqres.in:443/api/users" + with tracer.start_as_current_span("test"): + r = self.http.request("GET", request_url) + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + urllib3_span = spans[0] + test_span = spans[1] + + assert r + assert r.status == 200 + + # Same traceId + assert test_span.t == urllib3_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not urllib3_span.ec # 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["wsgi_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 urllib3_span.data["http"]["url"] == request_url + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_as_root_exit_span(self): agent.options.allow_exit_as_root = True - r = self.http.request('GET', testenv["wsgi_server"] + '/') + r = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 wsgi_span = spans[0] urllib3_span = spans[1] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, None) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert not urllib3_span.p + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_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 urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_with_query(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/?one=1&two=2') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/?one=1&two=2") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertTrue(urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] ) - 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 urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_with_alt_query(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/', fields={'one': '1', 'two': 2}) + with tracer.start_as_current_span("test"): + r = self.http.request( + "GET", testenv["flask_server"] + "/", fields={"one": "1", "two": 2} + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertTrue(urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] ) - 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 urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_put_request(self): - with tracer.start_active_span('test'): - r = self.http.request('PUT', testenv["wsgi_server"] + '/notfound') + with tracer.start_as_current_span("test"): + r = self.http.request("PUT", testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(404, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 404 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/notfound', wsgi_span.data["http"]["url"]) - self.assertEqual('PUT', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/notfound" + assert wsgi_span.data["http"]["method"] == "PUT" + assert wsgi_span.data["http"]["status"] == 404 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/notfound", urllib3_span.data["http"]["url"]) - self.assertEqual("PUT", 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"] == 404 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/notfound" + assert urllib3_span.data["http"]["method"] == "PUT" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_301_redirect(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/301') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/301") spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert len(spans) == 5 wsgi_span2 = spans[0] urllib3_span2 = spans[1] @@ -313,71 +379,75 @@ def test_301_redirect(self): urllib3_span1 = spans[3] test_span = spans[4] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, wsgi_span2.t) + assert urllib3_span1.t == traceId + assert wsgi_span1.t == traceId + assert urllib3_span2.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(urllib3_span1.p, test_span.s) - self.assertEqual(wsgi_span1.p, urllib3_span1.s) - self.assertEqual(urllib3_span2.p, test_span.s) - self.assertEqual(wsgi_span2.p, urllib3_span2.s) + assert urllib3_span1.p == test_span.s + assert wsgi_span1.p == urllib3_span1.s + assert urllib3_span2.p == test_span.s + assert wsgi_span2.p == urllib3_span2.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span1.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(urllib3_span2.ec) - self.assertIsNone(wsgi_span2.ec) + assert not test_span.ec + assert not urllib3_span1.ec + assert not wsgi_span1.ec + assert not urllib3_span2.ec + assert not wsgi_span2.ec # wsgi - self.assertEqual("wsgi", wsgi_span1.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span1.data["http"]["host"]) - self.assertEqual('/', wsgi_span1.data["http"]["url"]) - self.assertEqual('GET', wsgi_span1.data["http"]["method"]) - self.assertEqual(200, wsgi_span1.data["http"]["status"]) - self.assertIsNone(wsgi_span1.data["http"]["error"]) - self.assertIsNone(wsgi_span1.stack) - - self.assertEqual("wsgi", wsgi_span2.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span2.data["http"]["host"]) - self.assertEqual('/301', wsgi_span2.data["http"]["url"]) - self.assertEqual('GET', wsgi_span2.data["http"]["method"]) - self.assertEqual(301, wsgi_span2.data["http"]["status"]) - self.assertIsNone(wsgi_span2.data["http"]["error"]) - self.assertIsNone(wsgi_span2.stack) + assert wsgi_span1.n == "wsgi" + assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span1.data["http"]["url"] == "/" + assert wsgi_span1.data["http"]["method"] == "GET" + assert wsgi_span1.data["http"]["status"] == 200 + assert not wsgi_span1.data["http"]["error"] + assert not wsgi_span1.stack + + assert wsgi_span2.n == "wsgi" + assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span2.data["http"]["url"] == "/301" + assert wsgi_span2.data["http"]["method"] == "GET" + assert wsgi_span2.data["http"]["status"] == 301 + assert not wsgi_span2.data["http"]["error"] + assert not wsgi_span2.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span1.n) - self.assertEqual(200, urllib3_span1.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span1.data["http"]["url"]) - self.assertEqual("GET", urllib3_span1.data["http"]["method"]) - self.assertIsNotNone(urllib3_span1.stack) - self.assertTrue(type(urllib3_span1.stack) is list) - self.assertTrue(len(urllib3_span1.stack) > 1) - - self.assertEqual("urllib3", urllib3_span2.n) - self.assertEqual(301, urllib3_span2.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/301", urllib3_span2.data["http"]["url"]) - self.assertEqual("GET", urllib3_span2.data["http"]["method"]) - self.assertIsNotNone(urllib3_span2.stack) - self.assertTrue(type(urllib3_span2.stack) is list) - self.assertTrue(len(urllib3_span2.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span1.n == "urllib3" + assert urllib3_span1.data["http"]["status"] == 200 + assert urllib3_span1.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span1.data["http"]["method"] == "GET" + assert urllib3_span1.stack + assert isinstance(urllib3_span1.stack, list) + assert len(urllib3_span1.stack) > 1 + + assert urllib3_span2.n == "urllib3" + assert urllib3_span2.data["http"]["status"] == 301 + assert urllib3_span2.data["http"]["url"] == testenv["flask_server"] + "/301" + assert urllib3_span2.data["http"]["method"] == "GET" + assert urllib3_span2.stack + assert isinstance(urllib3_span2.stack, list) + assert len(urllib3_span2.stack) > 1 def test_302_redirect(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/302') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/302") spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert len(spans) == 5 wsgi_span2 = spans[0] urllib3_span2 = spans[1] @@ -385,117 +455,123 @@ def test_302_redirect(self): urllib3_span1 = spans[3] test_span = spans[4] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, wsgi_span2.t) + assert urllib3_span1.t == traceId + assert wsgi_span1.t == traceId + assert urllib3_span2.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(urllib3_span1.p, test_span.s) - self.assertEqual(wsgi_span1.p, urllib3_span1.s) - self.assertEqual(urllib3_span2.p, test_span.s) - self.assertEqual(wsgi_span2.p, urllib3_span2.s) + assert urllib3_span1.p == test_span.s + assert wsgi_span1.p == urllib3_span1.s + assert urllib3_span2.p == test_span.s + assert wsgi_span2.p == urllib3_span2.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span1.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(urllib3_span2.ec) - self.assertIsNone(wsgi_span2.ec) + assert not test_span.ec + assert not urllib3_span1.ec + assert not wsgi_span1.ec + assert not urllib3_span2.ec + assert not wsgi_span2.ec # wsgi - self.assertEqual("wsgi", wsgi_span1.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span1.data["http"]["host"]) - self.assertEqual('/', wsgi_span1.data["http"]["url"]) - self.assertEqual('GET', wsgi_span1.data["http"]["method"]) - self.assertEqual(200, wsgi_span1.data["http"]["status"]) - self.assertIsNone(wsgi_span1.data["http"]["error"]) - self.assertIsNone(wsgi_span1.stack) - - self.assertEqual("wsgi", wsgi_span2.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span2.data["http"]["host"]) - self.assertEqual('/302', wsgi_span2.data["http"]["url"]) - self.assertEqual('GET', wsgi_span2.data["http"]["method"]) - self.assertEqual(302, wsgi_span2.data["http"]["status"]) - self.assertIsNone(wsgi_span2.data["http"]["error"]) - self.assertIsNone(wsgi_span2.stack) + assert wsgi_span1.n == "wsgi" + assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span1.data["http"]["url"] == "/" + assert wsgi_span1.data["http"]["method"] == "GET" + assert wsgi_span1.data["http"]["status"] == 200 + assert not wsgi_span1.data["http"]["error"] + assert not wsgi_span1.stack + + assert wsgi_span2.n == "wsgi" + assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span2.data["http"]["url"] == "/302" + assert wsgi_span2.data["http"]["method"] == "GET" + assert wsgi_span2.data["http"]["status"] == 302 + assert not wsgi_span2.data["http"]["error"] + assert not wsgi_span2.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span1.n) - self.assertEqual(200, urllib3_span1.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span1.data["http"]["url"]) - self.assertEqual("GET", urllib3_span1.data["http"]["method"]) - self.assertIsNotNone(urllib3_span1.stack) - self.assertTrue(type(urllib3_span1.stack) is list) - self.assertTrue(len(urllib3_span1.stack) > 1) - - self.assertEqual("urllib3", urllib3_span2.n) - self.assertEqual(302, urllib3_span2.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/302", urllib3_span2.data["http"]["url"]) - self.assertEqual("GET", urllib3_span2.data["http"]["method"]) - self.assertIsNotNone(urllib3_span2.stack) - self.assertTrue(type(urllib3_span2.stack) is list) - self.assertTrue(len(urllib3_span2.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span1.n == "urllib3" + assert urllib3_span1.data["http"]["status"] == 200 + assert urllib3_span1.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span1.data["http"]["method"] == "GET" + assert urllib3_span1.stack + assert isinstance(urllib3_span1.stack, list) + assert len(urllib3_span1.stack) > 1 + + assert urllib3_span2.n == "urllib3" + assert urllib3_span2.data["http"]["status"] == 302 + assert urllib3_span2.data["http"]["url"] == testenv["flask_server"] + "/302" + assert urllib3_span2.data["http"]["method"] == "GET" + assert urllib3_span2.stack + assert isinstance(urllib3_span2.stack, list) + assert len(urllib3_span2.stack) > 1 def test_5xx_request(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/504') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/504") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(504, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 504 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert urllib3_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert wsgi_span.ec == 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/504', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(504, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/504" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 504 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(504, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/504", 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"] == 504 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/504" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_exception_logging(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: - r = self.http.request('GET', testenv["wsgi_server"] + '/exception') + r = self.http.request("GET", testenv["flask_server"] + "/exception") except Exception: pass @@ -511,352 +587,406 @@ def test_exception_logging(self): # we will just discard the optional log span if present # Without blinker, our instrumentation logs roughly the same exception data onto the # already existing wsgi span. Which we validate in this TC if present. - self.assertIn(len(spans), (3, 4)) + assert len(spans) in (3, 4) + with_blinker = len(spans) == 3 if not with_blinker: spans = spans[1:] wsgi_span, urllib3_span, test_span = spans - self.assertTrue(r) - self.assertEqual(500, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 500 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert urllib3_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert wsgi_span.ec == 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/exception" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 500 if with_blinker: - self.assertEqual('fake error', wsgi_span.data["http"]["error"]) + assert wsgi_span.data["http"]["error"] == "fake error" else: - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_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) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/exception" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_client_error(self): r = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: - r = self.http.request('GET', 'http://doesnotexist.asdf:5000/504', - retries=False, - timeout=urllib3.Timeout(connect=0.5, read=0.5)) + r = self.http.request( + "GET", + "http://doesnotexist.asdf:5000/504", + retries=False, + timeout=urllib3.Timeout(connect=0.5, read=0.5), + ) except Exception: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 urllib3_span = spans[0] test_span = spans[1] - self.assertIsNone(r) + assert not r # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) + assert urllib3_span.p == test_span.s # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) + assert urllib3_span.t == traceId - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertIsNone(urllib3_span.data["http"]["status"]) - self.assertEqual("http://doesnotexist.asdf:5000/504", 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 not urllib3_span.data["http"]["status"] + assert urllib3_span.data["http"]["url"] == "http://doesnotexist.asdf:5000/504" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 2 - def test_requestspkg_get(self): + def test_requests_pkg_get(self): self.recorder.clear_spans() - with tracer.start_active_span('test'): - r = requests.get(testenv["wsgi_server"] + '/', timeout=2) + with tracer.start_as_current_span("test"): + r = requests.get(testenv["flask_server"] + "/", timeout=2) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status_code) - self.assertIsNone(tracer.active_span) + assert r + assert r.status_code == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_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_requestspkg_get_with_custom_headers(self): + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_requests_pkg_get_with_custom_headers(self): my_custom_headers = dict() - my_custom_headers['X-PGL-1'] = '1' + my_custom_headers["X-PGL-1"] = "1" - with tracer.start_active_span('test'): - r = requests.get(testenv["wsgi_server"] + '/', timeout=2, headers=my_custom_headers) + with tracer.start_as_current_span("test"): + r = requests.get( + testenv["flask_server"] + "/", timeout=2, headers=my_custom_headers + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status_code) - self.assertIsNone(tracer.active_span) + assert r + assert r.status_code == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_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_requestspkg_put(self): - with tracer.start_active_span('test'): - r = requests.put(testenv["wsgi_server"] + '/notfound') + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_requests_pkg_put(self): + with tracer.start_as_current_span("test"): + r = requests.put(testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertEqual(404, r.status_code) - self.assertIsNone(tracer.active_span) + assert r.status_code == 404 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/notfound', wsgi_span.data["http"]["url"]) - self.assertEqual('PUT', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/notfound" + assert wsgi_span.data["http"]["method"] == "PUT" + assert wsgi_span.data["http"]["status"] == 404 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/notfound", urllib3_span.data["http"]["url"]) - self.assertEqual("PUT", 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"] == 404 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/notfound" + assert urllib3_span.data["http"]["method"] == "PUT" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_response_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["flask_server"] + "/response_headers") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/response_headers', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/response_headers" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_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.assertIn("X-Capture-This", urllib3_span.data["http"]["header"]) - self.assertEqual("Ok", urllib3_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", urllib3_span.data["http"]["header"]) - self.assertEqual("Ok too", urllib3_span.data["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 ( + urllib3_span.data["http"]["url"] + == testenv["flask_server"] + "/response_headers" + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + assert "X-Capture-This" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-This"] == "Ok" + assert "X-Capture-That" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers def test_request_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] request_headers = { "X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too", } - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): r = self.http.request( - "GET", testenv["wsgi_server"] + "/", headers=request_headers + "GET", testenv["flask_server"] + "/", headers=request_headers ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # 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["wsgi_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) - - self.assertIn("X-Capture-This-Too", urllib3_span.data["http"]["header"]) - self.assertEqual("this too", urllib3_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", urllib3_span.data["http"]["header"]) - self.assertEqual("that too", urllib3_span.data["http"]["header"]["X-Capture-That-Too"]) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + assert "X-Capture-This-Too" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers + + def test_extract_custom_headers_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + caplog.set_level(logging.DEBUG, logger="instana") + extract_custom_headers(span, request_headers) + assert "urllib3 _extract_custom_headers error: " in caplog.messages + + def test_collect_response_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + + caplog.set_level(logging.DEBUG, logger="instana") + collect_response(span, {}) + assert "urllib3 collect_response error: " in caplog.messages + + def test_collect_kvs_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + + caplog.set_level(logging.DEBUG, logger="instana") + collect_kvs({}, (), {}) + assert "urllib3 _collect_kvs error: " in caplog.messages diff --git a/tests/opentracing/__init__.py b/tests/collector/__init__.py similarity index 100% rename from tests/opentracing/__init__.py rename to tests/collector/__init__.py diff --git a/tests/collector/conftest.py b/tests/collector/conftest.py new file mode 100644 index 00000000..115e93ad --- /dev/null +++ b/tests/collector/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from instana.collector.aws_fargate import AWSFargateCollector + +# Mocking AWSFargateCollector.get_ecs_metadata() +@pytest.fixture(autouse=True) +def get_ecs_metadata(monkeypatch, request) -> None: + """Return always True for AWSFargateCollector.get_ecs_metadata()""" + + def _always_true(_: object) -> bool: + return True + + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original AWSFargateCollector.get_ecs_metadata() + monkeypatch.setattr(AWSFargateCollector, "get_ecs_metadata", AWSFargateCollector.get_ecs_metadata) + else: + monkeypatch.setattr(AWSFargateCollector, "get_ecs_metadata", _always_true) diff --git a/tests/collector/test_base_collector.py b/tests/collector/test_base_collector.py new file mode 100644 index 00000000..890fb4ca --- /dev/null +++ b/tests/collector/test_base_collector.py @@ -0,0 +1,200 @@ +import logging +import multiprocessing +import multiprocessing.queues +import queue +import threading +import time +from typing import Generator +from unittest.mock import patch + +import pytest +from instana.agent.test import TestAgent +from instana.collector.base import BaseCollector +from instana.recorder import StanRecorder +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from pytest import LogCaptureFixture + +from instana.span_context import SpanContext + + +class TestBaseCollector: + @pytest.fixture(autouse=True) + def _resource(self, caplog: LogCaptureFixture) -> Generator[None, None, None]: + self.collector = BaseCollector(TestAgent()) + yield + self.collector.shutdown(report_final=False) + self.collector = None + caplog.clear() + + def test_default(self) -> None: + assert isinstance(self.collector.agent, TestAgent) + assert self.collector.THREAD_NAME == "Instana Collector" + assert isinstance(self.collector.span_queue, multiprocessing.queues.Queue) + assert isinstance(self.collector.profile_queue, queue.Queue) + assert not self.collector.reporting_thread + assert isinstance(self.collector.thread_shutdown, threading.Event) + assert self.collector.snapshot_data_last_sent == 0 + assert self.collector.snapshot_data_interval == 300 + assert len(self.collector.helpers) == 0 + assert self.collector.report_interval == 1 + assert not self.collector.started + assert self.collector.fetching_start_time == 0 + + def test_default_env_is_prod(self): + with patch("instana.collector.base.env_is_test", False): + self.collector = BaseCollector(TestAgent()) + assert isinstance(self.collector.span_queue, queue.Queue) + + def test_is_reporting_thread_running(self) -> None: + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread( + name=self.collector.THREAD_NAME, target=reporting_function + ) + sample_thread.start() + try: + assert self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_is_reporting_thread_running_with_different_name(self) -> None: + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread(name="test-thread", target=reporting_function) + sample_thread.start() + try: + assert not self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_start_collector_while_running_thread( + self, caplog: LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + self.collector.start() + assert ( + "BaseCollector.start non-fatal: call but thread already running (started: False)" + in caplog.messages + ) + + def test_start_agent_shutdown_is_set(self) -> None: + self.collector.thread_shutdown.set() + isThreadFound = False + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + response = self.collector.start() + assert not response + for thread in threading.enumerate(): + if thread.name == "Collector Timed Start": + isThreadFound = True + assert isThreadFound + + def test_start_collector_when_agent_is_ready( + self, caplog: LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + if not self.collector.started: + self.collector.start() + assert "BaseCollector.start: launching collection thread" in caplog.messages + assert self.collector.started + assert self.collector.reporting_thread.daemon + assert self.collector.reporting_thread.name == self.collector.THREAD_NAME + + def test_start_agent_can_not_send(self, caplog: LogCaptureFixture) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ), patch("instana.agent.host.HostAgent.can_send", return_value=False): + caplog.set_level(logging.WARNING, logger="instana") + self.collector.agent.machine.fsm.current = "test" + self.collector.start() + assert ( + "BaseCollector.start: the agent tells us we can't send anything out" + in caplog.messages + ) + + def test_shutdown(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.shutdown() + assert "Collector.shutdown: Reporting final data." in caplog.messages + assert not self.collector.started + + def test_background_report(self) -> None: + assert self.collector.background_report() + self.collector.thread_shutdown.set() + assert not self.collector.background_report() + + def test_prepare_and_report_data(self) -> None: + assert self.collector.prepare_and_report_data() + with patch("instana.collector.base.env_is_test", False): + assert self.collector.prepare_and_report_data() + + def test_should_send_snapshot_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.should_send_snapshot_data() + assert ( + "BaseCollector: should_send_snapshot_data needs to be overridden" + in caplog.messages + ) + + def test_collect_snapshot(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.collect_snapshot() + assert ( + "BaseCollector: collect_snapshot needs to be overridden" in caplog.messages + ) + + def test_queued_spans( + self, span_context: SpanContext, span_processor: StanRecorder + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.span_queue.put(span) + time.sleep(0.1) + spans = self.collector.queued_spans() + assert len(spans) == 3 + + def test_queued_profiles( + self, span_context: SpanContext, span_processor: StanRecorder + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.profile_queue.put(span) + time.sleep(0.1) + profiles = self.collector.queued_profiles() + assert len(profiles) == 3 diff --git a/tests/platforms/test_eksfargate_collector.py b/tests/collector/test_eksfargate_collector.py similarity index 100% rename from tests/platforms/test_eksfargate_collector.py rename to tests/collector/test_eksfargate_collector.py diff --git a/tests/platforms/test_fargate_collector.py b/tests/collector/test_fargate_collector.py similarity index 100% rename from tests/platforms/test_fargate_collector.py rename to tests/collector/test_fargate_collector.py diff --git a/tests/platforms/test_gcr_collector.py b/tests/collector/test_gcr_collector.py similarity index 100% rename from tests/platforms/test_gcr_collector.py rename to tests/collector/test_gcr_collector.py diff --git a/tests/collector/test_host_collector.py b/tests/collector/test_host_collector.py new file mode 100644 index 00000000..0802f607 --- /dev/null +++ b/tests/collector/test_host_collector.py @@ -0,0 +1,303 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import logging +import os +import sys +import threading +from typing import Generator + +import pytest +from instana.agent.host import HostAgent +from instana.collector.helpers.runtime import ( + PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR, +) +from instana.collector.host import HostCollector +from instana.recorder import StanRecorder +from instana.singletons import get_agent, get_tracer, set_agent, set_tracer +from instana.tracer import InstanaTracer, InstanaTracerProvider +from instana.version import VERSION +from mock import patch +from pytest import LogCaptureFixture + + +class TestHostCollector: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.collector = HostCollector(HostAgent()) + self.original_agent = get_agent() + self.original_tracer = get_tracer() + self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + "3.8.0" + yield + self.collector.shutdown(report_final=False) + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_AGENT_KEY", + "INSTANA_ZONE", + "INSTANA_TAGS", + "INSTANA_DISABLE_METRICS_COLLECTION", + "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", + "AUTOWRAPT_BOOTSTRAP", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + if self.webhook_sitedir_path in sys.path: + sys.path.remove(self.webhook_sitedir_path) + + def create_agent_and_setup_tracer( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_start(self) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ): + self.collector.start() + assert self.collector.started + assert self.collector.THREAD_NAME == "Instana Collector" + assert self.collector.snapshot_data_interval == 300 + assert self.collector.snapshot_data_last_sent == 0 + assert isinstance(self.collector.helpers[0].collector, HostCollector) + assert len(self.collector.helpers) == 1 + assert isinstance(self.collector.reporting_thread, threading.Thread) + self.collector.ready_to_start = False + assert not self.collector.start() + + def test_prepare_and_report_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=True): + self.collector.prepare_and_report_data() + assert "Agent is ready. Getting to work." in caplog.messages + assert "Harmless state machine thread disagreement. Will self-correct on next timer cycle." + self.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=False): + assert not self.collector.prepare_and_report_data() + self.collector.agent.machine.fsm.current = "good2go" + caplog.clear() + with patch("instana.agent.host.HostAgent.is_timed_out", return_value=True): + self.collector.prepare_and_report_data() + assert ( + "The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically." + in caplog.messages + ) + + def test_should_send_snapshot_data(self) -> None: + self.collector.snapshot_data_interval = 999999999999 + assert not self.collector.should_send_snapshot_data() + + def test_prepare_payload_basics( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + + assert len(payload.keys()) == 3 + assert "spans" in payload + assert isinstance(payload["spans"], list) + assert len(payload["spans"]) == 0 + assert "metrics", payload + assert len(payload["metrics"].keys()) == 1 + assert "plugins", payload["metrics"] + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + + python_plugin = payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" in python_plugin["data"] + + assert "ru_utime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_utime"]) in [float, int] + assert "ru_stime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_stime"]) in [float, int] + assert "ru_maxrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_maxrss"]) in [float, int] + assert "ru_ixrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_ixrss"]) in [float, int] + assert "ru_idrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_idrss"]) in [float, int] + assert "ru_isrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_isrss"]) in [float, int] + assert "ru_minflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_minflt"]) in [float, int] + assert "ru_majflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_majflt"]) in [float, int] + assert "ru_nswap" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nswap"]) in [float, int] + assert "ru_inblock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_inblock"]) in [float, int] + assert "ru_oublock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_oublock"]) in [float, int] + assert "ru_msgsnd" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgsnd"]) in [float, int] + assert "ru_msgrcv" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgrcv"]) in [float, int] + assert "ru_nsignals" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nsignals"]) in [float, int] + assert "ru_nvcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nvcsw"]) in [float, int] + assert "ru_nivcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nivcsw"]) in [float, int] + assert "alive_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["alive_threads"]) in [float, int] + assert "dummy_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["dummy_threads"]) in [float, int] + assert "daemon_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["daemon_threads"]) in [float, int] + + assert "gc" in python_plugin["data"]["metrics"] + assert isinstance(python_plugin["data"]["metrics"]["gc"], dict) + assert "collect0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect0"]) in [float, int] + assert "collect1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect1"]) in [float, int] + assert "collect2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect2"]) in [float, int] + assert "threshold0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold0"]) in [ + float, + int, + ] + assert "threshold1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold1"]) in [ + float, + int, + ] + assert "threshold2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold2"]) in [ + float, + int, + ] + + def test_prepare_payload_basics_disable_runtime_metrics( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + + assert len(payload.keys()) == 3 + assert "spans" in payload + assert isinstance(payload["spans"], list) + assert len(payload["spans"]) == 0 + assert "metrics" in payload + assert len(payload["metrics"].keys()) == 1 + assert "plugins" in payload["metrics"] + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + + python_plugin = payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" not in python_plugin["data"] + + def test_prepare_payload_with_snapshot_with_python_packages( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + assert snapshot["versions"]["instana"] == VERSION + assert "wrapt" in snapshot["versions"] + assert "fysom" in snapshot["versions"] + + def test_prepare_payload_with_snapshot_disabled_python_packages( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) == 1 + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autowrapt( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Autowrapt" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autotrace( + self, tracer_provider: InstanaTracerProvider + ) -> None: + sys.path.append(self.webhook_sitedir_path) + + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "AutoTrace" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION diff --git a/tests/collector/test_runtime.py b/tests/collector/test_runtime.py new file mode 100644 index 00000000..b42ad1b4 --- /dev/null +++ b/tests/collector/test_runtime.py @@ -0,0 +1,65 @@ +from typing import Generator +from unittest.mock import patch +import pytest + +from instana.agent.host import HostAgent +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.host import HostCollector + + +class TestRuntimeHelper: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.helper = RuntimeHelper( + collector=HostCollector( + HostAgent(), + ), + ) + yield + self.helper = None + + def test_default_while_gc_disabled(self) -> None: + import gc + + gc.disable() + helper = RuntimeHelper(collector=HostCollector(HostAgent())) + assert helper.previous_gc_count is None + + def test_collect_metrics(self) -> None: + response = self.helper.collect_metrics() + assert response[0]["name"] == "com.instana.plugin.python" + + def test_collect_runtime_snapshot_default(self) -> None: + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Manual" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_autowrapt(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_autowrapt_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Autowrapt" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_webhook(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_webhook_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "AutoTrace" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_gc_metrics(self) -> None: + plugin_data = self.helper.collect_metrics() + + self.helper._collect_gc_metrics(plugin_data[0], True) + assert len(self.helper.previous["data"]["metrics"]["gc"]) == 6 diff --git a/tests/collector/test_utils.py b/tests/collector/test_utils.py new file mode 100644 index 00000000..d8b4ad40 --- /dev/null +++ b/tests/collector/test_utils.py @@ -0,0 +1,34 @@ +import pytest +from typing import Generator +from instana.collector.utils import format_span +from instana.singletons import tracer +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from opentelemetry.trace.span import format_span_id + + +class TestUtils: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor + yield + + def test_format_span(self, trace_id, span_id, span_context, span_processor) -> None: + expected_trace_id = format_span_id(trace_id) + expected_span_id = format_span_id(span_id) + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + formatted_spans = format_span(span_list) + assert len(formatted_spans) == 3 + assert formatted_spans[0].t == expected_trace_id + assert formatted_spans[0].s == expected_span_id + assert formatted_spans[0].n == "span1" diff --git a/tests/conftest.py b/tests/conftest.py index d63dea98..25079b90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,31 +4,62 @@ import importlib.util import os import sys +from typing import Any, Dict + import pytest +from opentelemetry.context.context import Context +from opentelemetry.trace import set_span_in_context if importlib.util.find_spec("celery"): pytest_plugins = ("celery.contrib.pytest",) -# Set our testing flags -os.environ["INSTANA_TEST"] = "true" -# os.environ["INSTANA_DEBUG"] = "true" - -# Make sure the instana package is fully loaded -import instana - -collect_ignore_glob = [] - -# Cassandra and gevent tests are run in dedicated jobs on CircleCI and will -# be run explicitly. (So always exclude them here) +from instana.agent.host import HostAgent +from instana.collector.base import BaseCollector +from instana.recorder import StanRecorder +from instana.span.base_span import BaseSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from instana.tracer import InstanaTracerProvider + +# Ignoring tests during OpenTelemetry migration. +collect_ignore_glob = [ + "*autoprofile*", + # "*clients*", + # "*frameworks*", + # "*platforms*", + # "*propagators*", + "*w3c_trace_context*", +] + +# TODO: remove the following entries as the migration of the instrumentation +# codes are finalised. +collect_ignore_glob.append("*clients/test_google*") + +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_tornado*") + +collect_ignore_glob.append("*propagators/test_binary*") +collect_ignore_glob.append("*propagators/test_http*") + +collect_ignore_glob.append("*agents/test_aws*") +collect_ignore_glob.append("*agents/test_google*") +collect_ignore_glob.append("*collector/test_gcr*") +collect_ignore_glob.append("*collector/test_eks*") +collect_ignore_glob.append("*collector/test_fargate*") + +# # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will +# # be run explicitly. (So always exclude them here) if not os.environ.get("CASSANDRA_TEST"): collect_ignore_glob.append("*test_cassandra*") if not os.environ.get("COUCHBASE_TEST"): collect_ignore_glob.append("*test_couchbase*") -if not os.environ.get("GEVENT_STARLETTE_TEST"): - collect_ignore_glob.append("*test_gevent*") - collect_ignore_glob.append("*test_starlette*") +# if not os.environ.get("GEVENT_STARLETTE_TEST"): +# collect_ignore_glob.append("*test_gevent*") +# collect_ignore_glob.append("*test_starlette*") # Python 3.10 support is incomplete yet # TODO: Remove this once we start supporting Tornado >= 6.0 @@ -54,13 +85,9 @@ # Currently there is a runtime incompatibility caused by the library: # `undefined symbol: _PyInterpreterState_Get` collect_ignore_glob.append("*test_psycopg2*") + 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*") @@ -86,3 +113,103 @@ def celery_enable_logging(): @pytest.fixture(scope="session") def celery_includes(): return {"tests.frameworks.test_celery"} + + +@pytest.fixture +def trace_id() -> int: + return 1812338823475918251 + + +@pytest.fixture +def span_id() -> int: + return 6895521157646639861 + + +@pytest.fixture +def span_processor() -> StanRecorder: + rec = StanRecorder(HostAgent()) + rec.THREAD_NAME = "InstanaSpan Recorder Test" + return rec + + +@pytest.fixture +def tracer_provider(span_processor: StanRecorder) -> InstanaTracerProvider: + return InstanaTracerProvider(span_processor=span_processor, exporter=HostAgent()) + + +@pytest.fixture +def span_context(trace_id: int, span_id: int) -> SpanContext: + return SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + + +@pytest.fixture +def span(span_context: SpanContext, span_processor: StanRecorder) -> InstanaSpan: + span_name = "test-span" + return InstanaSpan(span_name, span_context, span_processor) + + +@pytest.fixture +def base_span(span: InstanaSpan) -> BaseSpan: + return BaseSpan(span, None) + + +@pytest.fixture +def context(span: InstanaSpan) -> Context: + return set_span_in_context(span) + + +def always_true(_: object) -> bool: + return True + + +# Mocking HostAgent.can_send() +@pytest.fixture(autouse=True) +def can_send(monkeypatch, request) -> None: + """Return always True for HostAgent.can_send()""" + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original HostAgent.can_send() + monkeypatch.setattr(HostAgent, "can_send", HostAgent.can_send) + else: + monkeypatch.setattr(HostAgent, "can_send", always_true) + + +# Mocking HostAgent.get_from_structure() +@pytest.fixture(autouse=True) +def get_from_structure(monkeypatch, request) -> None: + """ + Retrieves the From data that is reported alongside monitoring data. + @return: dict() + """ + + def _get_from_structure(_: object) -> Dict[str, Any]: + return {"e": os.getpid(), "h": "fake"} + + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original HostAgent.get_from_structure() + monkeypatch.setattr( + HostAgent, "get_from_structure", HostAgent.get_from_structure + ) + else: + monkeypatch.setattr(HostAgent, "get_from_structure", _get_from_structure) + + +# Mocking BaseCollector.prepare_and_report_data() +@pytest.fixture(autouse=True) +def prepare_and_report_data(monkeypatch, request): + """Return always True for BaseCollector.prepare_and_report_data()""" + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original BaseCollector.prepare_and_report_data() + monkeypatch.setattr( + BaseCollector, + "prepare_and_report_data", + BaseCollector.prepare_and_report_data, + ) + else: + monkeypatch.setattr(BaseCollector, "prepare_and_report_data", always_true) diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index 0fc9455f..f1231fa8 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -1,459 +1,438 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +from typing import Any, Dict, Generator, Optional import aiohttp import asyncio -import unittest -from instana.singletons import async_tracer, agent +import pytest -import tests.apps.flask_app -import tests.apps.aiohttp_app -from ..helpers import testenv +from instana.singletons import tracer, agent +import tests.apps.flask_app # noqa: F401 +import tests.apps.aiohttp_app # noqa: F401 +from tests.helpers import testenv -class TestAiohttp(unittest.TestCase): - async def fetch(self, session, url, headers=None, params=None): +class TestAiohttpClient: + async def fetch( + self, + session: aiohttp.client.ClientSession, + url: str, + headers: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ): try: async with session.get(url, headers=headers, params=params) as response: return response except aiohttp.web_exceptions.HTTPException: pass - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield + # teardown + # Ensure that allow_exit_as_root has the default value""" agent.options.allow_exit_as_root = False - def test_client_get(self): + def test_client_get(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_as_root_exit_span(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True + async def test(): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 wsgi_span = spans[0] aiohttp_span = spans[1] - self.assertIsNone(async_tracer.active_span) - - self.assertEqual(aiohttp_span.t, wsgi_span.t) - # Same traceId - traceId = aiohttp_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == wsgi_span.t # Parent relationships - self.assertIsNone(aiohttp_span.p) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert not aiohttp_span.p + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_301(self): + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={wsgi_span.t}" + + def test_client_get_301(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/301") + return await self.fetch(session, testenv["flask_server"] + "/301") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 wsgi_span1 = spans[0] wsgi_span2 = spans[1] aiohttp_span = spans[2] test_span = spans[3] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, wsgi_span2.t) + assert aiohttp_span.t == traceId + assert wsgi_span1.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span1.p, aiohttp_span.s) - self.assertEqual(wsgi_span2.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span1.p == aiohttp_span.s + assert wsgi_span2.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(wsgi_span2.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/301", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span2.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_405(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span1.ec + assert not wsgi_span2.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/301" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span2.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_405(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/405") + return await self.fetch(session, testenv["flask_server"] + "/405") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(405, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/405", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_500(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 405 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/405" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_500(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/500") + return await self.fetch(session, testenv["flask_server"] + "/500") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(500, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/500", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('INTERNAL SERVER ERROR', - aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_504(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert wsgi_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 500 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/500" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] == "INTERNAL SERVER ERROR" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_504(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/504") + return await self.fetch(session, testenv["flask_server"] + "/504") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(504, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/504", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('GATEWAY TIMEOUT', aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_with_params_to_scrub(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert wsgi_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 504 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/504" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] == "GATEWAY TIMEOUT" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_with_params_to_scrub(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"], params={"secret": "yeah"}) + return await self.fetch( + session, testenv["flask_server"], params={"secret": "yeah"} + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual("secret=", - aiohttp_span.data["http"]["params"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_response_header_capture(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["params"] == "secret=" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This'] + agent.options.extra_http_headers = ["X-Capture-This"] async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/response_headers") + return await self.fetch( + session, testenv["flask_server"] + "/response_headers" + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-Capture-This", aiohttp_span.data["http"]["header"]) - self.assertEqual("Ok", aiohttp_span.data["http"]["header"]["X-Capture-This"]) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/response_headers" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-Capture-This" in aiohttp_span.data["http"]["header"] + assert aiohttp_span.data["http"]["header"]["X-Capture-This"] == "Ok" + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" agent.options.extra_http_headers = original_extra_http_headers - def test_client_error(self): + def test_client_error(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, 'http://doesnotexist:10/') + return await self.fetch(session, "http://doesnotexist:10/") response = None try: @@ -462,33 +441,62 @@ async def test(): pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 aiohttp_span = spans[0] test_span = spans[1] - self.assertIsNone(async_tracer.active_span) - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) + assert aiohttp_span.t == test_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) + assert aiohttp_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertIsNone(aiohttp_span.data["http"]["status"]) - self.assertEqual("http://doesnotexist:10/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.data["http"]["error"]) - self.assertTrue(len(aiohttp_span.data["http"]["error"])) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIsNone(response) + assert test_span.ec + assert aiohttp_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert not aiohttp_span.data["http"]["status"] + assert aiohttp_span.data["http"]["url"] == "http://doesnotexist:10/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] + assert len(aiohttp_span.data["http"]["error"]) + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert not response + + def test_client_get_tracing_off(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.aiohttp.client.tracing_is_off", + return_value=True, + ) + + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["flask_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + # Span names are not "aiohttp-client" + for span in spans: + assert span.n != "aiohttp-client" + + def test_client_get_provided_tracing_config(self, mocker) -> None: + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession(trace_configs=[]) as session: + return await self.fetch(session, testenv["flask_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 diff --git a/tests/frameworks/test_aiohttp_server.py b/tests/frameworks/test_aiohttp_server.py index 41dd2ce8..aa4b15e3 100644 --- a/tests/frameworks/test_aiohttp_server.py +++ b/tests/frameworks/test_aiohttp_server.py @@ -1,18 +1,17 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import aiohttp import asyncio -import unittest - -import tests.apps.aiohttp_app -from ..helpers import testenv +from typing import Generator -from instana.singletons import async_tracer, agent +import aiohttp +import pytest +from instana.singletons import agent, tracer +from tests.helpers import testenv -class TestAiohttpServer(unittest.TestCase): +class TestAiohttpServer: async def fetch(self, session, url, headers=None, params=None): try: async with session.get(url, headers=headers, params=params) as response: @@ -20,461 +19,440 @@ async def fetch(self, session, url, headers=None, params=None): except aiohttp.web_exceptions.HTTPException: pass - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Load test server application + import tests.apps.aiohttp_app # noqa: F401 + + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) - - def tearDown(self): - pass + yield def test_server_get(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Synthetic - self.assertIsNone(test_span.sy) - self.assertIsNone(aioclient_span.sy) - self.assertIsNone(aioserver_span.sy) + assert not test_span.sy + assert not aioclient_span.sy + assert not aioserver_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_204(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/204") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId trace_id = test_span.t - self.assertEqual(trace_id, aioclient_span.t) - self.assertEqual(trace_id, aioserver_span.t) + assert aioclient_span.t == trace_id + assert aioserver_span.t == trace_id # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Synthetic - self.assertIsNone(test_span.sy) - self.assertIsNone(aioclient_span.sy) - self.assertIsNone(aioserver_span.sy) + assert not test_span.sy + assert not aioclient_span.sy + assert not aioserver_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(204, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/204", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(204, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/204", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(isinstance(aioclient_span.stack, list)) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], trace_id) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % trace_id) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 204 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/204" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(trace_id) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={trace_id}" def test_server_synthetic_request(self): async def test(): - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + headers = {"X-INSTANA-SYNTHETIC": "1"} - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"] + "/", headers=headers) + return await self.fetch( + session, testenv["aiohttp_server"] + "/", headers=headers + ) response = self.loop.run_until_complete(test()) + assert response spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertTrue(aioserver_span.sy) - self.assertIsNone(aioclient_span.sy) - self.assertIsNone(test_span.sy) + assert aioserver_span.sy + assert not aioclient_span.sy + assert not test_span.sy def test_server_get_with_params_to_scrub(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"], params={"secret": "iloveyou"}) + return await self.fetch( + session, + testenv["aiohttp_server"], + params={"secret": "iloveyou"}, + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertEqual("secret=", - aioserver_span.data["http"]["params"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual("secret=", - aioclient_span.data["http"]["params"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert aioserver_span.data["http"]["params"] == "secret=" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_custom_header_capture(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list agent.options.extra_http_headers = [ - u'X-Capture-This', u'X-Capture-That'] + "X-Capture-This", + "X-Capture-That", + ] headers = dict() - headers['X-Capture-This'] = 'this' - headers['X-Capture-That'] = 'that' + headers["X-Capture-This"] = "this" + headers["X-Capture-That"] = "that" - return await self.fetch(session, testenv["aiohttp_server"], headers=headers, params={"secret": "iloveyou"}) + return await self.fetch( + session, + testenv["aiohttp_server"], + headers=headers, + params={"secret": "iloveyou"}, + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertEqual("secret=", - aioserver_span.data["http"]["params"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual("secret=", - aioclient_span.data["http"]["params"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - self.assertIn("X-Capture-This", aioserver_span.data["http"]["header"]) - self.assertEqual("this", aioserver_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", aioserver_span.data["http"]["header"]) - self.assertEqual("that", aioserver_span.data["http"]["header"]["X-Capture-That"]) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert aioserver_span.data["http"]["params"] == "secret=" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + assert "X-Capture-This" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-That"] == "that" def test_server_get_401(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/401") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(401, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/401", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(401, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/401", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 401 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/401" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_500(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/500") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aioclient_span.ec, 1) - self.assertEqual(aioserver_span.ec, 1) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(500, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/500", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(500, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/500", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual('I must simulate errors.', - aioclient_span.data["http"]["error"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert aioclient_span.ec == 1 + assert aioserver_span.ec == 1 + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 500 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/500" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_exception(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"] + "/exception") + return await self.fetch( + session, testenv["aiohttp_server"] + "/exception" + ) response = self.loop.run_until_complete(test()) + assert response spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aioclient_span.ec, 1) - self.assertEqual(aioserver_span.ec, 1) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(500, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/exception", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(500, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/exception", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual('Internal Server Error', - aioclient_span.data["http"]["error"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) + assert not test_span.ec + assert aioclient_span.ec == 1 + assert aioserver_span.ec == 1 + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 500 + assert ( + aioserver_span.data["http"]["url"] + == f"{testenv['aiohttp_server']}/exception" + ) + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert aioclient_span.n == "aiohttp-client" + assert aioclient_span.data["http"]["status"] == 500 + assert aioclient_span.data["http"]["error"] == "Internal Server Error" + assert aioclient_span.stack + assert isinstance(aioclient_span.stack, list) + assert len(aioclient_span.stack) > 1 + + +class TestAiohttpServerMiddleware: + async def fetch(self, session, url, headers=None, params=None): + try: + async with session.get(url, headers=headers, params=params) as response: + return response + except aiohttp.web_exceptions.HTTPException: + pass + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Load test server application + import tests.apps.aiohttp_app2 # noqa: F401 + + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() + + # New event loop for every test + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + yield + + def test_server_get(self): + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["aiohttp_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + aioserver_span = spans[0] + aioclient_span = spans[1] + test_span = spans[2] + + # Same traceId + traceId = test_span.t + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId + + # Parent relationships + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s diff --git a/tests/frameworks/test_asyncio.py b/tests/frameworks/test_asyncio.py index 73bcf95b..5a3fbe61 100644 --- a/tests/frameworks/test_asyncio.py +++ b/tests/frameworks/test_asyncio.py @@ -2,19 +2,37 @@ # (c) Copyright Instana Inc. 2020 import asyncio +from typing import Any, Dict, Generator, Optional + import aiohttp -import unittest +import pytest -import tests.apps.flask_app -from ..helpers import testenv +import tests.apps.flask_app # noqa: F401 from instana.configurator import config -from instana.singletons import async_tracer - +from instana.singletons import tracer +from tests.helpers import testenv + + +class TestAsyncio: + async def fetch( + self, + session: aiohttp.ClientSession, + url: str, + headers: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ): + try: + async with session.get(url, headers=headers, params=params) as response: + return response + except aiohttp.web_exceptions.HTTPException: + pass -class TestAsyncio(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -22,120 +40,111 @@ def setUp(self): asyncio.set_event_loop(None) # Restore default - config['asyncio_task_context_propagation']['enabled'] = False - - def tearDown(self): - """ Purge the queue """ - pass - - async def fetch(self, session, url, headers=None): - try: - async with session.get(url, headers=headers) as response: - return response - except aiohttp.web_exceptions.HTTPException: - pass - - def test_ensure_future_with_context(self): + config["asyncio_task_context_propagation"]["enabled"] = False + yield + # teardown + # Close the loop if running + if self.loop.is_running(): + self.loop.close() + + def test_ensure_future_with_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.ensure_future(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) # Override default task context propagation - config['asyncio_task_context_propagation']['enabled'] = True + config["asyncio_task_context_propagation"]["enabled"] = True self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 test_span = spans[0] wsgi_span = spans[1] aioclient_span = spans[2] - self.assertEqual(test_span.t, wsgi_span.t) - self.assertEqual(test_span.t, aioclient_span.t) + assert test_span.t == wsgi_span.t + assert aioclient_span.t == test_span.t - self.assertEqual(test_span.p, None) - self.assertEqual(wsgi_span.p, aioclient_span.s) - self.assertEqual(aioclient_span.p, test_span.s) + assert not test_span.p + assert wsgi_span.p == aioclient_span.s + assert aioclient_span.p == test_span.s - def test_ensure_future_without_context(self): + def test_ensure_future_without_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.ensure_future(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual("sdk", spans[0].n) - self.assertEqual("wsgi", spans[1].n) + assert len(spans) == 2 + assert spans[0].n == "sdk" + assert spans[1].n == "wsgi" # Without the context propagated, we should get two separate traces - self.assertNotEqual(spans[0].t, spans[1].t) + assert spans[0].t != spans[1].t if hasattr(asyncio, "create_task"): - def test_create_task_with_context(self): + + def test_create_task_with_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.create_task(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) # Override default task context propagation - config['asyncio_task_context_propagation']['enabled'] = True + config["asyncio_task_context_propagation"]["enabled"] = True self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 test_span = spans[0] wsgi_span = spans[1] aioclient_span = spans[2] - self.assertEqual(test_span.t, wsgi_span.t) - self.assertEqual(test_span.t, aioclient_span.t) + assert wsgi_span.t == test_span.t + assert aioclient_span.t == test_span.t - self.assertEqual(test_span.p, None) - self.assertEqual(wsgi_span.p, aioclient_span.s) - self.assertEqual(aioclient_span.p, test_span.s) + assert not test_span.p + assert wsgi_span.p == aioclient_span.s + assert aioclient_span.p == test_span.s - def test_create_task_without_context(self): + def test_create_task_without_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.create_task(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual("sdk", spans[0].n) - self.assertEqual("wsgi", spans[1].n) + assert len(spans) == 2 + assert spans[0].n == "sdk" + assert spans[1].n == "wsgi" # Without the context propagated, we should get two separate traces - self.assertNotEqual(spans[0].t, spans[1].t) + assert spans[0].t != spans[1].t diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 02778efc..f79642a6 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -4,109 +4,119 @@ import os import urllib3 +import pytest +from typing import Generator from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from ..apps.app_django import INSTALLED_APPS +from tests.apps.app_django import INSTALLED_APPS from instana.singletons import agent, tracer -from ..helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list +from tests.helpers import ( + fail_with_message_and_span_dump, + get_first_span_by_filter, + drop_log_spans_from_list, +) +from instana.instrumentation.django.middleware import url_pattern_route apps.populate(INSTALLED_APPS) class TestDjango(StaticLiveServerTestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder - self.recorder.clear_spans() + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" self.http = urllib3.PoolManager() - - def tearDown(self): - """ Clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable """ + self.recorder = tracer.span_processor + # clear all spans before a test run + self.recorder.clear_spans() + yield + # clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "" - def test_basic_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/', fields={"test": 1}) + def test_basic_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", self.live_server_url + "/", fields={"test": 1} + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers["Server-Timing"] == server_timing_value - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertIsNone(django_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert django_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None - self.assertEqual(None, django_span.ec) - self.assertEqual('/', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('test=1', django_span.data["http"]["params"]) - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert django_span.ec is None + assert "/" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert "test=1" == django_span.data["http"]["params"] + assert "^$" == django_span.data["http"]["path_tpl"] - self.assertIsNone(django_span.stack) + assert django_span.stack is None - def test_synthetic_request(self): - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + def test_synthetic_request(self) -> None: + headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/', headers=headers) + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", self.live_server_url + "/", headers=headers + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert "^$" == django_span.data["http"]["path_tpl"] - self.assertTrue(django_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert django_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None - def test_request_with_error(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/cause_error') + def test_request_with_error(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", self.live_server_url + "/cause_error") - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -116,58 +126,58 @@ def test_request_with_error(self): msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test' + filter = lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span - filter = lambda span: span.n == 'urllib3' + filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, filter) - self.assertTrue(urllib3_span) + assert urllib3_span - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) + assert django_span - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers["Server-Timing"] == server_timing_value - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(1, django_span.ec) + assert 1 == django_span.ec - self.assertEqual('/cause_error', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(500, django_span.data["http"]["status"]) - self.assertEqual('This is a fake error: /cause-error', django_span.data["http"]["error"]) - self.assertEqual('^cause_error$', django_span.data["http"]["path_tpl"]) - self.assertIsNone(django_span.stack) + assert "/cause_error" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] + assert 500 == django_span.data["http"]["status"] + assert "This is a fake error: /cause-error" == django_span.data["http"]["error"] + assert "^cause_error$" == django_span.data["http"]["path_tpl"] + assert django_span.stack is None - def test_request_with_not_found(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/not_found') + def test_request_with_not_found(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", self.live_server_url + "/not_found") - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -177,19 +187,19 @@ def test_request_with_not_found(self): msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) + assert django_span - self.assertIsNone(django_span.ec) - self.assertEqual(404, django_span.data["http"]["status"]) + assert django_span.ec is None + assert 404 == django_span.data["http"]["status"] - def test_request_with_not_found_no_route(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/no_route') + def test_request_with_not_found_no_route(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", self.live_server_url + "/no_route") - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -199,373 +209,452 @@ def test_request_with_not_found_no_route(self): msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) - self.assertIsNone(django_span.data["http"]["path_tpl"]) - self.assertIsNone(django_span.ec) - self.assertEqual(404, django_span.data["http"]["status"]) + assert django_span + assert django_span.data["http"]["path_tpl"] is None + assert django_span.ec is None + assert 404 == django_span.data["http"]["status"] - def test_complex_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/complex') + def test_complex_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", self.live_server_url + "/complex") - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert 5 == len(spans) test_span = spans[4] urllib3_span = spans[3] django_span = spans[2] - ot_span1 = spans[1] - ot_span2 = spans[0] + otel_span1 = spans[1] + otel_span2 = spans[0] - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) - - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) - self.assertEqual("sdk", ot_span1.n) - self.assertEqual("sdk", ot_span2.n) - - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) - self.assertEqual(django_span.t, ot_span1.t) - self.assertEqual(ot_span1.t, ot_span2.t) - - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) - self.assertEqual(ot_span1.p, django_span.s) - self.assertEqual(ot_span2.p, ot_span1.s) - - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) - - self.assertEqual('/complex', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^complex$', django_span.data["http"]["path_tpl"]) - - def test_request_header_capture(self): + assert response.headers["Server-Timing"] == server_timing_value + + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n + assert "sdk" == otel_span1.n + assert "sdk" == otel_span2.n + + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t + assert django_span.t == otel_span1.t + assert otel_span1.t == otel_span2.t + + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s + assert otel_span1.p == django_span.s + assert otel_span2.p == otel_span1.s + + assert django_span.ec is None + assert django_span.stack is None + + assert otel_span1.data["sdk"]["type"] == "exit" + assert otel_span2.data["sdk"]["type"] == otel_span1.data["sdk"]["type"] + otel_span1.data["sdk"]["name"] == "asteroid" + otel_span2.data["sdk"]["name"] == "spacedust" + + assert "/complex" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert "^complex$" == django_span.data["http"]["path_tpl"] + + def test_request_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 = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) # response = self.client.get('/') - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) + assert django_span.ec is None + assert django_span.stack is None - self.assertEqual('/', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert "/" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert "^$" == django_span.data["http"]["path_tpl"] - self.assertIn("X-Capture-This", django_span.data["http"]["header"]) - self.assertEqual("this", django_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", django_span.data["http"]["header"]) - self.assertEqual("that", django_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in django_span.data["http"]["header"] + assert "this" == django_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in django_span.data["http"]["header"] + assert "that" == django_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + 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 = [u'X-Capture-This-Too', u'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] - with tracer.start_active_span('test'): - response = self.http.request('GET', self.live_server_url + '/response_with_headers') + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", self.live_server_url + "/response_with_headers" + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) + assert django_span.ec is None + assert django_span.stack is None - self.assertEqual('/response_with_headers', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^response_with_headers$', django_span.data["http"]["path_tpl"]) + assert "/response_with_headers" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert "^response_with_headers$" == django_span.data["http"]["path_tpl"] - self.assertIn("X-Capture-This-Too", django_span.data["http"]["header"]) - self.assertEqual("this too", django_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", django_span.data["http"]["header"]) - self.assertEqual("that too", django_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in django_span.data["http"]["header"] + assert "this too" == django_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in django_span.data["http"]["header"] + assert "that too" == django_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers - def test_with_incoming_context(self): + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_context(self) -> None: request_headers = dict() - request_headers['X-INSTANA-T'] = '1' - request_headers['X-INSTANA-S'] = '1' - request_headers['traceparent'] = '01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-788777' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' - - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - - self.assertTrue(response) - self.assertEqual(200, response.status) + request_headers["X-INSTANA-T"] = "1" + request_headers["X-INSTANA-S"] = "1" + request_headers["traceparent"] = ( + "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-788777" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) + + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) + + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, '0000000000000001') - self.assertEqual(django_span.p, '0000000000000001') + # assert django_span.t == '0000000000000001' + # assert django_span.p == '0000000000000001' + assert django_span.t == 1 + assert django_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('traceparent', response.headers) - # The incoming traceparent header had version 01 (which does not exist at the time of writing), but since we - # support version 00, we also need to pass down 00 for the version field. - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) - - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers["Server-Timing"] == server_timing_value - def test_with_incoming_context_and_correlation(self): + assert "traceparent" in response.headers + # The incoming traceparent header had version 01 (which does not exist at the time of writing), but since we + # support version 00, we also need to pass down 00 for the version field. + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) + + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_context_and_correlation(self) -> None: request_headers = dict() - request_headers['X-INSTANA-T'] = '1' - request_headers['X-INSTANA-S'] = '1' - request_headers['X-INSTANA-L'] = '1, correlationType=web; correlationId=1234567890abcdef' - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' - - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - - self.assertTrue(response) - self.assertEqual(200, response.status) + request_headers["X-INSTANA-T"] = "1" + request_headers["X-INSTANA-S"] = "1" + request_headers["X-INSTANA-L"] = ( + "1, correlationType=web; correlationId=1234567890abcdef" + ) + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) + + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) + + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.p, '00f067aa0ba902b7') - self.assertEqual(django_span.ia.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.ia.p, '8357ccd9da194656') - self.assertEqual(django_span.lt, '4bf92f3577b34da6a3ce929d0e0e4736') - self.assertEqual(django_span.tp, True) - self.assertEqual(django_span.crtp, 'web') - self.assertEqual(django_span.crid, '1234567890abcdef') - - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert django_span.t == "a3ce929d0e0e4736" + assert django_span.p == "00f067aa0ba902b7" + assert django_span.ia.t == "a3ce929d0e0e4736" + assert django_span.ia.p == "8357ccd9da194656" + assert django_span.lt == "4bf92f3577b34da6a3ce929d0e0e4736" + assert django_span.tp + assert django_span.crtp == "web" + assert django_span.crid == "1234567890abcdef" - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) - - def test_with_incoming_traceparent_tracestate(self): + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) + + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_traceparent_tracestate(self) -> None: request_headers = dict() - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') # last 16 chars from traceparent trace_id - self.assertEqual(django_span.p, '00f067aa0ba902b7') - self.assertEqual(django_span.ia.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.ia.p, '8357ccd9da194656') - self.assertEqual(django_span.lt, '4bf92f3577b34da6a3ce929d0e0e4736') - self.assertEqual(django_span.tp, True) - - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert ( + django_span.t == "a3ce929d0e0e4736" + ) # last 16 chars from traceparent trace_id + assert django_span.p == "00f067aa0ba902b7" + assert django_span.ia.t == "a3ce929d0e0e4736" + assert django_span.ia.p == "8357ccd9da194656" + assert django_span.lt == "4bf92f3577b34da6a3ce929d0e0e4736" + assert django_span.tp - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) - - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.s), response.headers['tracestate']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) - - def test_with_incoming_traceparent_tracestate_disable_traceparent(self): + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.s + ) + == response.headers["tracestate"] + ) + + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_traceparent_tracestate_disable_traceparent(self) -> None: os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "1" request_headers = dict() - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') # last 16 chars from traceparent trace_id - self.assertEqual(django_span.p, '8357ccd9da194656') - - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert ( + django_span.t == "a3ce929d0e0e4736" + ) # last 16 chars from traceparent trace_id + assert django_span.p == "8357ccd9da194656" - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) - - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) - - def test_with_incoming_mixed_case_context(self): + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) + + def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() - request_headers['X-InSTANa-T'] = '0000000000000001' - request_headers['X-instana-S'] = '0000000000000001' + request_headers["X-InSTANa-T"] = "0000000000000001" + request_headers["X-instana-S"] = "0000000000000001" - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, '0000000000000001') - self.assertEqual(django_span.p, '0000000000000001') + # assert django_span.t == '0000000000000001' + # assert django_span.p == '0000000000000001' + assert django_span.t == 1 + assert django_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + 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" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers["Server-Timing"] == server_timing_value + + def test_url_pattern_route(self) -> None: + view_name = "app_django.another" + path_tpl = "".join(url_pattern_route(view_name)) + assert path_tpl == "^another$" + + view_name = "app_django.complex" + try: + path_tpl = "".join(url_pattern_route(view_name)) + except Exception: + path_tpl = None + assert path_tpl is None diff --git a/tests/frameworks/test_fastapi.py b/tests/frameworks/test_fastapi.py index 6a276e26..7e82df22 100644 --- a/tests/frameworks/test_fastapi.py +++ b/tests/frameworks/test_fastapi.py @@ -1,628 +1,585 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time -import unittest -import multiprocessing - -import requests - -from instana.singletons import async_tracer -from tests.apps.fastapi_app import launch_fastapi -from ..helpers import testenv -from ..helpers import get_first_span_by_filter - - -class TestFastAPI(unittest.TestCase): - def setUp(self): - self.proc = multiprocessing.Process(target=launch_fastapi, args=(), daemon=True) - self.proc.start() - time.sleep(2) - - def tearDown(self): - # Kill server after tests - self.proc.kill() - - def test_vanilla_get(self): - result = requests.get(testenv["fastapi_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 = async_tracer.recorder.queued_spans() - # FastAPI instrumentation (like all instrumentation) _always_ traces unless told otherwise - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].n, "asgi") - - def test_basic_get(self): +from typing import Generator + +from fastapi.testclient import TestClient +import pytest +from instana.singletons import tracer, agent + +from tests.apps.fastapi_app.app import fastapi_server +from tests.helpers import get_first_span_by_filter + + +class TestFastAPI: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette/FastAPI to make it easier. + self.client = TestClient(fastapi_server) + + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + "X-Capture-This-Too", + "X-Capture-That-Too", + ] + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # FastAPI instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_400(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_400(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/400") - - self.assertEqual(result.status_code, 400) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/400", headers=headers) + + assert result + assert result.status_code == 400 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/400") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/400") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 400) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_500(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/400" + assert asgi_span.data["http"]["path_tpl"] == "/400" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 400 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/500") - - self.assertEqual(result.status_code, 500) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/500", headers=headers) + + assert result + assert result.status_code == 500 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertEqual(asgi_span.ec, 1) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/500") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/500") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 500) - self.assertEqual(asgi_span.data["http"]["error"], "500 response") - - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_path_templates(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/500" + assert asgi_span.data["http"]["path_tpl"] == "/500" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["error"] == "500 response" + assert not asgi_span.data["http"]["params"] + + def test_path_templates(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/users/1") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/users/1", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/users/1") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/users/{user_id}") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_secret_scrubbing(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/users/1" + assert asgi_span.data["http"]["path_tpl"] == "/users/{user_id}" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/?secret=shhh") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/?secret=shhh", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertEqual(asgi_span.data["http"]["params"], "secret=") - - def test_synthetic_request(self): - request_headers = {"X-INSTANA-SYNTHETIC": "1"} - with async_tracer.start_active_span("test"): - result = requests.get( - testenv["fastapi_server"] + "/", headers=request_headers - ) - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertTrue(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_request_header_capture(self): - from instana.singletons import agent - - # The background FastAPI server is pre-configured with custom headers to capture - - request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} - - with async_tracer.start_active_span("test"): - result = requests.get( - testenv["fastapi_server"] + "/", headers=request_headers - ) - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not test_span.sy + + def test_request_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) - - def test_response_header_capture(self): - from instana.singletons import agent - - # The background FastAPI server is pre-configured with custom headers to capture - - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/response_headers") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-That"] == "that" + + def test_response_header_capture(self) -> None: + # The background FastAPI server is pre-configured with custom headers + # to capture. + + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/response_headers", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This-Too", asgi_span.data["http"]["header"]) - self.assertEqual("this too", asgi_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", asgi_span.data["http"]["header"]) - self.assertEqual("that too", asgi_span.data["http"]["header"]["X-Capture-That-Too"]) - - def test_non_async_simple(self): - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/non_async_simple") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(5, len(spans)) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/response_headers" + assert asgi_span.data["http"]["path_tpl"] == "/response_headers" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This-Too" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" + + def test_non_async_simple(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/non_async_simple", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = ( - lambda span: span.n == "urllib3" and span.p == test_span.s - ) - urllib3_span1 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span1) + assert test_span - span_filter = ( - lambda span: span.n == "asgi" and span.p == urllib3_span1.s - ) + span_filter = lambda span: span.n == "asgi" and span.p == test_span.s # noqa: E731 asgi_span1 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span1) - - span_filter = ( - lambda span: span.n == "urllib3" and span.p == asgi_span1.s - ) - urllib3_span2 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span2) + assert asgi_span1 - span_filter = ( - lambda span: span.n == "asgi" and span.p == urllib3_span2.s - ) + span_filter = lambda span: span.n == "asgi" and span.p == asgi_span1.s # noqa: E731 asgi_span2 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span2) + assert asgi_span2 # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, asgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, asgi_span2.t) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span1.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span1.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span1.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span1.ec) - self.assertEqual(asgi_span1.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span1.data["http"]["path"], "/non_async_simple") - self.assertEqual(asgi_span1.data["http"]["path_tpl"], "/non_async_simple") - self.assertEqual(asgi_span1.data["http"]["method"], "GET") - self.assertEqual(asgi_span1.data["http"]["status"], 200) - - self.assertIsNone(asgi_span1.data["http"]["error"]) - self.assertIsNone(asgi_span1.data["http"]["params"]) - - def test_non_async_threadpool(self): - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/non_async_threadpool") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - span_filter = ( + assert asgi_span1.t == traceId + assert asgi_span2.t == traceId + + assert result.headers["X-INSTANA-T"] == str(asgi_span1.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span1.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span1.t}" + + assert not asgi_span1.ec + assert asgi_span1.data["http"]["host"] == "testserver" + assert asgi_span1.data["http"]["path"] == "/non_async_simple" + assert asgi_span1.data["http"]["path_tpl"] == "/non_async_simple" + assert asgi_span1.data["http"]["method"] == "GET" + assert asgi_span1.data["http"]["status"] == 200 + assert not asgi_span1.data["http"]["error"] + assert not asgi_span1.data["http"]["params"] + + assert not asgi_span2.ec + assert asgi_span2.data["http"]["host"], "testserver" + assert asgi_span2.data["http"]["path"], "/users/1" + assert asgi_span2.data["http"]["path_tpl"], "/users/{user_id}" + assert asgi_span2.data["http"]["method"], "GET" + assert asgi_span2.data["http"]["status"], 200 + assert not asgi_span2.data["http"]["error"] + assert not asgi_span2.data["http"]["params"] + + def test_non_async_threadpool(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/non_async_threadpool", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/non_async_threadpool") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/non_async_threadpool") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/non_async_threadpool" + assert asgi_span.data["http"]["path_tpl"] == "/non_async_threadpool" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] diff --git a/tests/frameworks/test_fastapi_middleware.py b/tests/frameworks/test_fastapi_middleware.py new file mode 100644 index 00000000..5c915f25 --- /dev/null +++ b/tests/frameworks/test_fastapi_middleware.py @@ -0,0 +1,96 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import logging +from typing import Generator + +import pytest +from instana.singletons import tracer +from fastapi.testclient import TestClient + +from tests.helpers import get_first_span_by_filter + + +class TestFastAPIMiddleware: + """ + Tests FastAPI with provided Middleware. + """ + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from FastAPI to make it easier. + from tests.apps.fastapi_app.app2 import fastapi_server + self.client = TestClient(fastapi_server) + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + del fastapi_server + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # FastAPI instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 65bf0ea7..145d0e33 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -4,954 +4,1128 @@ import unittest import urllib3 import flask +from unittest.mock import patch if hasattr(flask.signals, 'signals_available'): - from flask.signals import signals_available + from flask.signals import signals_available else: - # Beginning from 2.3.0 as stated in the notes - # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 - # "Signals are always available. blinker>=1.6.2 is a required dependency. - # The signals_available attribute is deprecated. #5056" - signals_available = True + # Beginning from 2.3.0 as stated in the notes + # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 + # "Signals are always available. blinker>=1.6.2 is a required dependency. + # The signals_available attribute is deprecated. #5056" + signals_available = True + +from opentelemetry.trace import SpanKind import tests.apps.flask_app from instana.singletons import tracer -from ..helpers import testenv +from instana.span.span import get_current_span +from tests.helpers import testenv class TestFlask(unittest.TestCase): - def setUp(self): + + def setUp(self) -> 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): + def tearDown(self) -> None: """ Do nothing for now """ return None - def test_vanilla_requests(self): - r = self.http.request('GET', testenv["wsgi_server"] + '/') - self.assertEqual(r.status, 200) + def test_vanilla_requests(self) -> None: + r = self.http.request('GET', testenv["flask_server"] + '/') + assert r.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + def test_get_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 3 + + wsgi_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(wsgi_span.t) + + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t + assert response.headers["Server-Timing"] == server_timing_value + + assert get_current_span().is_recording() is False + + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + + # Synthetic + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None + + # Error logging + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None + + # wsgi + assert "wsgi" == wsgi_span.n + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["flask_port"] + ) + assert "/" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + # urllib3 + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + # We should NOT have a path template for this route + assert wsgi_span.data["http"]["path_tpl"] is None - def test_get_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/') + def test_get_request_with_query_params(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/" + "?key1=val1&key2=val2" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_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'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Synthetic - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/" == wsgi_span.data["http"]["url"] + assert wsgi_span.data["http"]["params"] == "key1=&key2=" + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_get_request_with_suppression(self): + def test_get_request_with_suppression(self) -> None: headers = {'X-INSTANA-L':'0'} - response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) + response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() - self.assertEqual(response.headers.get('X-INSTANA-L', None), '0') + assert response.headers.get("X-INSTANA-L", None) == "0" # The traceparent has to be present - self.assertIsNotNone(response.headers.get('traceparent', None)) + assert response.headers.get("traceparent", None) is not None # The last digit of the traceparent has to be 0 - self.assertEqual(response.headers['traceparent'][-1], '0') + assert response.headers["traceparent"][-1] == "0" # This should not be present - self.assertIsNone(response.headers.get('tracestate', None)) + assert response.headers.get("tracestate", None) is None # Assert that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert any(map(lambda x: x.l != 0, spans)) is False # Assert that there are no spans in the recorded list - self.assertEqual(spans, []) + assert spans == [] - def test_get_request_with_suppression_and_w3c(self): + @unittest.skip("Handled when type of trace and span ids are modified to str") + def test_get_request_with_suppression_and_w3c(self) -> None: headers = { 'X-INSTANA-L':'0', 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', 'tracestate': 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7'} - response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) + response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() - self.assertEqual(response.headers.get('X-INSTANA-L', None), '0') - self.assertIsNotNone(response.headers.get('traceparent', None)) - self.assertEqual(response.headers['traceparent'][-1], '0') + assert response.headers.get("X-INSTANA-L", None) == "0" + assert response.headers.get("traceparent", None) is not None + assert response.headers["traceparent"][-1] == "0" # The tracestate has to be present - self.assertIsNotNone(response.headers.get('tracestate', None)) + assert response.headers.get("tracestate", None) is not None # The 'in=' section can not be in the tracestate - self.assertTrue('in=' not in response.headers['tracestate']) + assert "in=" not in response.headers["tracestate"] # Assert that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert any(map(lambda x: x.l != 0, spans)) is False # Assert that there are no spans in the recorded list - self.assertEqual(spans, []) + assert spans == [] - def test_synthetic_request(self): + def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' } - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None - def test_render_template(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/render') + def test_render_template(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/render') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 render_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - 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'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, render_span.t) - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == render_span.t + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(render_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert render_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(render_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None + assert render_span.ec is None # render - self.assertEqual("render", render_span.n) - self.assertEqual(3, render_span.k) - self.assertEqual('flask_render_template.html', render_span.data["render"]["name"]) - self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["log"]["message"]) - self.assertIsNone(render_span.data["log"]["parameters"]) + assert "render" == render_span.n + assert SpanKind.INTERNAL == render_span.k + assert "flask_render_template.html" == render_span.data["render"]["name"] + assert "template" == render_span.data["render"]["type"] + assert render_span.data["log"]["message"] is None + assert render_span.data["log"]["parameters"] is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_server"] + '/render', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/render" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_render_template_string(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/render_string') + def test_render_template_string(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/render_string') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 render_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - 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'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, render_span.t) - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == render_span.t + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(render_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert render_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(render_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None + assert render_span.ec is None # render - self.assertEqual("render", render_span.n) - self.assertEqual(3, render_span.k) - self.assertEqual('(from string)', render_span.data["render"]["name"]) - self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["log"]["message"]) - self.assertIsNone(render_span.data["log"]["parameters"]) + assert "render" == render_span.n + assert SpanKind.INTERNAL == render_span.k + assert "(from string)" == render_span.data["render"]["name"] + assert "template" == render_span.data["render"]["type"] + assert render_span.data["log"]["message"] is None + assert render_span.data["log"]["parameters"] is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render_string', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render_string" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_server"] + '/render_string', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/render_string" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_301(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/301', redirect=False) + def test_301(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/301', redirect=False) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(301, response.status) + assert response + assert 301 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/301', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(301, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/301" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 301 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(301, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/301', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 301 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/301" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_custom_404(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/custom-404') + def test_custom_404(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/custom-404') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_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'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/custom-404', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/custom-404" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 404 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/custom-404', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 404 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/custom-404" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_404(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/11111111111') + def test_404(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/11111111111') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_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'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/11111111111', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/11111111111" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 404 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/11111111111', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 404 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/11111111111" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_500(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/500') + def test_500(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/500') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/500', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/500" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_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) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/500" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_render_error(self): + def test_render_error(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/render_error') + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/render_error') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_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'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual('Exception on /render_error [GET]', log_span.data["log"]['message']) - self.assertEqual(" unexpected '}'", log_span.data["log"]['parameters']) + assert "log" == log_span.n + assert log_span.data["log"]["message"] == "Exception on /render_error [GET]" + assert ( + log_span.data["log"]["parameters"] + == " unexpected '}'" + ) # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render_error', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render_error" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_server"] + '/render_error', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/render_error" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_exception(self): + def test_exception(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/exception') + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/exception') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(log_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert log_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) - self.assertEqual(1, log_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec + assert 1 == log_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual('Exception on /exception [GET]', log_span.data["log"]['message']) - self.assertEqual(" fake error", log_span.data["log"]['parameters']) - + assert "log" == log_span.n + assert log_span.data["log"]["message"] == "Exception on /exception [GET]" + assert log_span.data["log"]["parameters"] == " fake error" - # wsgis - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + # wsgi + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/exception" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_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) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert testenv["flask_server"] + "/exception" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_custom_exception_with_log(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/exception-invalid-usage') + def test_custom_exception_with_log(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/exception-invalid-usage') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(502, response.status) + assert response + assert 502 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) - self.assertEqual(1, log_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec + assert 1 == log_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual('InvalidUsage error handler invoked', log_span.data["log"]['message']) - self.assertEqual(" ", log_span.data["log"]['parameters']) + assert "log" == log_span.n + assert log_span.data["log"]["message"] == "InvalidUsage error handler invoked" + assert ( + log_span.data["log"]["parameters"] + == " " + ) # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception-invalid-usage', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(502, wsgi_span.data["http"]["status"]) - self.assertEqual('Simulated custom exception', wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/exception-invalid-usage" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 502 == wsgi_span.data["http"]["status"] + assert "Simulated custom exception" == wsgi_span.data["http"]["error"] + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(502, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/exception-invalid-usage', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 502 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/exception-invalid-usage" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None - def test_path_templates(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/users/Ricky/sayhello') + def test_path_templates(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/users/Ricky/sayhello') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_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'], wsgi_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/users/Ricky/sayhello', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/users/Ricky/sayhello" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # 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["wsgi_server"] + '/users/Ricky/sayhello', 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" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/users/Ricky/sayhello" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should have a reported path template for this route - self.assertEqual("/users/{username}/sayhello", wsgi_span.data["http"]["path_tpl"]) + assert "/users/{username}/sayhello" == wsgi_span.data["http"]["path_tpl"] - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list from instana.singletons import agent original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') + with tracer.start_as_current_span("test"): + response = self.http.request('GET', testenv["flask_server"] + '/response_headers') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + 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(wsgi_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'], wsgi_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_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 get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Synthetic - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # 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["wsgi_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) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["flask_server"] + "/response_headers" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/response_headers', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - self.assertIn("X-Capture-This", wsgi_span.data["http"]["header"]) - self.assertEqual("Ok", wsgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", wsgi_span.data["http"]["header"]) - self.assertEqual("Ok too", wsgi_span.data["http"]["header"]["X-Capture-That"]) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/response_headers" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + assert "X-Capture-This" in wsgi_span.data["http"]["header"] + assert "Ok" == wsgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in wsgi_span.data["http"]["header"] + + assert "Ok too" == wsgi_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers + + def test_request_started_exception(self) -> None: + with tracer.start_as_current_span("test"): + with patch( + "instana.singletons.tracer.extract", + side_effect=Exception("mocked error"), + ): + self.http.request("GET", testenv["flask_server"] + "/") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + @unittest.skipIf( + not signals_available, + "log_exception_with_instana needs to be covered only with blinker", + ) + def test_got_request_exception(self) -> None: + response = self.http.request( + "GET", testenv["flask_server"] + "/got_request_exception" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + wsgi_span = spans[0] + + assert response + assert 500 == response.status + + assert get_current_span().is_recording() is False + + # Error logging + assert wsgi_span.ec == 1 + + # wsgi + assert wsgi_span.n == "wsgi" + assert ( + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/got_request_exception" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert wsgi_span.data["http"]["status"] == 500 + assert wsgi_span.data["http"]["error"] == "RuntimeError()" + assert wsgi_span.stack is None diff --git a/tests/frameworks/test_gevent.py b/tests/frameworks/test_gevent.py index 71a724ad..69a9a6c8 100644 --- a/tests/frameworks/test_gevent.py +++ b/tests/frameworks/test_gevent.py @@ -18,7 +18,7 @@ @unittest.skipIf(not os.environ.get("GEVENT_STARLETTE_TEST"), reason="") class TestGEvent(unittest.TestCase): def setUp(self): - self.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["wsgi_port"], maxsize=20) + self.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["flask_port"], maxsize=20) self.recorder = tracer.recorder self.recorder.clear_spans() tracer._scope_manager = GeventScopeManager() @@ -28,7 +28,7 @@ def tearDown(self): pass def make_http_call(self, n=None): - return self.http.request('GET', testenv["wsgi_server"] + '/') + return self.http.request('GET', testenv["flask_server"] + '/') def spawn_calls(self): with tracer.start_active_span('spawn_calls'): 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 diff --git a/tests/frameworks/test_sanic.py b/tests/frameworks/test_sanic.py index 93de3cd0..275b3e4f 100644 --- a/tests/frameworks/test_sanic.py +++ b/tests/frameworks/test_sanic.py @@ -1,479 +1,523 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -import time -import requests -import multiprocessing -import unittest - -from instana.singletons import tracer -from ..helpers import testenv -from ..helpers import get_first_span_by_filter -from ..test_utils import _TraceContextMixin - - -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): - """ Kill server after tests """ - 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, 'asgi') - - def test_basic_get(self): - 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' +import pytest +from typing import Generator +from sanic_testing.testing import SanicTestClient + +from instana.singletons import tracer, agent +from tests.helpers import get_first_span_by_filter +from tests.test_utils import _TraceContextMixin +from tests.apps.sanic_app.server import app + + +class TestSanic(_TraceContextMixin): + @classmethod + def setup_class(cls) -> None: + cls.client = SanicTestClient(app, port=1337, host="127.0.0.1") + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + "X-Capture-This-Too", + "X-Capture-That-Too", + ] + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() + + def test_vanilla_get(self) -> None: + request, response = self.client.get("/") + + assert response.status_code == 200 + assert "X-INSTANA-T" in response.headers + assert "X-INSTANA-S" in response.headers + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + spans = self.recorder.queued_spans() + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert 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 == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_404(self): - 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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_404(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/foo/not_an_int", headers=headers) + + assert response.status_code == 404 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/foo/not_an_int') - self.assertIsNone(asgi_span.data['http']['path_tpl']) - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 404) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_sanic_exception(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/wrong') - - self.assertEqual(result.status_code, 400) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/foo/not_an_int" + assert not asgi_span.data["http"]["path_tpl"] + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 404 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_sanic_exception(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/wrong", headers=headers) + + assert response.status_code == 400 + + spans = self.recorder.queued_spans() + assert 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/wrong') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/wrong') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 400) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_500_instana_exception(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/instana_exception') - - self.assertEqual(result.status_code, 500) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 4) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/wrong" + assert asgi_span.data["http"]["path_tpl"] == "/wrong" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 400 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500_instana_exception(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/instana_exception", headers=headers) + + assert response.status_code == 500 + + spans = self.recorder.queued_spans() + assert 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) + assert 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 == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/instana_exception') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/instana_exception') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 500) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_500(self): - 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), 4) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/instana_exception" + assert asgi_span.data["http"]["path_tpl"] == "/instana_exception" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/test_request_args", headers=headers) + + assert response.status_code == 500 + + spans = self.recorder.queued_spans() + assert 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/test_request_args') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/test_request_args') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 500) - self.assertEqual(asgi_span.data['http']['error'], 'Something went wrong.') - self.assertIsNone(asgi_span.data['http']['params']) - - def test_path_templates(self): - 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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/test_request_args" + assert asgi_span.data["http"]["path_tpl"] == "/test_request_args" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["error"] == "Something went wrong." + assert not asgi_span.data["http"]["params"] + + def test_path_templates(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/foo/1", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert 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 == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/foo/1') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/foo/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_secret_scrubbing(self): - 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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/foo/1" + assert asgi_span.data["http"]["path_tpl"] == "/foo/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/?secret=shhh", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertEqual(asgi_span.data['http']['params'], 'secret=') - - 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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIsNotNone(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_request_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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not test_span.sy + + def test_request_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert 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 == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) - - def test_response_header_capture(self): - with tracer.start_active_span("test"): - result = requests.get(testenv["sanic_server"] + "/response_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' + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert "this" == asgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] + + def test_response_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/response_headers", headers=headers) + + assert response.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + 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) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "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['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data["http"]["path"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This-Too", asgi_span.data["http"]["header"]) - self.assertEqual("this too", asgi_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", asgi_span.data["http"]["header"]) - self.assertEqual("that too", asgi_span.data["http"]["header"]["X-Capture-That-Too"]) + assert asgi_span + + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/response_headers" + assert asgi_span.data["http"]["path_tpl"] == "/response_headers" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This-Too" in asgi_span.data["http"]["header"] + assert "this too" == asgi_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in asgi_span.data["http"]["header"] + assert "that too" == asgi_span.data["http"]["header"]["X-Capture-That-Too"] diff --git a/tests/frameworks/test_starlette.py b/tests/frameworks/test_starlette.py index ad67f4aa..7c15dd2b 100644 --- a/tests/frameworks/test_starlette.py +++ b/tests/frameworks/test_starlette.py @@ -1,275 +1,300 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import multiprocessing -import time +from typing import Generator + import pytest -import requests -import unittest - -from ..helpers import testenv -from instana.singletons import tracer -from ..helpers import get_first_span_by_filter - - -class TestStarlette(unittest.TestCase): - def setUp(self): - from tests.apps.starlette_app import launch_starlette - self.proc = multiprocessing.Process(target=launch_starlette, args=(), daemon=True) - self.proc.start() - time.sleep(2) - - def tearDown(self): - self.proc.kill() # Kill server after tests - - def test_vanilla_get(self): - result = requests.get(testenv["starlette_server"] + '/') - self.assertTrue(result) - spans = tracer.recorder.queued_spans() - # Starlette instrumentation (like all instrumentation) _always_ traces unless told otherwise - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].n, 'asgi') - - 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) - - def test_basic_get(self): +from instana.singletons import agent, tracer +from starlette.testclient import TestClient + +from tests.apps.starlette_app.app import starlette_server +from tests.helpers import get_first_span_by_filter + + +class TestStarlette: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette to make it easier. + self.client = TestClient(starlette_server) + # Configure to capture custom headers + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + ] + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # Starlette instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/') - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_path_templates(self): - result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/users/1') + assert asgi_span - self.assertTrue(result) + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' - test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + def test_path_templates(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/users/1", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/users/1') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/users/{user_id}') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_secret_scrubbing(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/users/1" + assert asgi_span.data["http"]["path_tpl"] == "/users/{user_id}" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/?secret=shhh') - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - assert len(spans) == 3 - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/?secret=shhh", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertEqual(asgi_span.data['http']['params'], 'secret=') - - def test_synthetic_request(self): - request_headers = { - 'X-INSTANA-SYNTHETIC': '1' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - assert len(spans) == 3 - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertTrue(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_custom_header_capture(self): - from instana.singletons import agent - - # The background Starlette server is pre-configured with custom headers to capture - - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not test_span.sy + + def test_custom_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - 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['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert "this" == asgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] diff --git a/tests/frameworks/test_starlette_middleware.py b/tests/frameworks/test_starlette_middleware.py new file mode 100644 index 00000000..d02039d4 --- /dev/null +++ b/tests/frameworks/test_starlette_middleware.py @@ -0,0 +1,143 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +from typing import Generator + +import pytest +from instana.singletons import agent, tracer +from starlette.testclient import TestClient + +from tests.apps.starlette_app.app2 import starlette_server +from tests.helpers import get_first_span_by_filter + + +class TestStarletteMiddleware: + """ + Tests Starlette with provided Middleware. + """ + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette to make it easier. + self.client = TestClient(starlette_server) + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # Starlette instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_basic_get_500(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/five", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["path"] == "/five" + assert asgi_span.data["http"]["path_tpl"] == "/five" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 3c66b79b..7e9f3484 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -3,172 +3,111 @@ import time import urllib3 -import unittest +import pytest +from typing import Generator -import tests.apps.flask_app -from ..helpers import testenv +from tests.apps import bottle_app +from tests.helpers import testenv from instana.singletons import agent, tracer +from instana.span.span import get_current_span -class TestWSGI(unittest.TestCase): - def setUp(self): +class TestWSGI: + @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() time.sleep(0.1) - def tearDown(self): - """ Do nothing for now """ - return None - - def test_vanilla_requests(self): + def test_vanilla_requests(self) -> None: response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - self.assertIsNone(tracer.active_span) - self.assertEqual(response.status, 200) + assert 1 == len(spans) + assert get_current_span().is_recording() is False + assert response.status == 200 - def test_get_request(self): - with tracer.start_active_span('test'): + def test_get_request(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert 3 == len(spans) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - def test_synthetic_request(self): + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' } - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert 3 == len(spans) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_complex_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/complex') - - spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) - self.assertIsNone(tracer.active_span) - - spacedust_span = spans[0] - asteroid_span = spans[1] - wsgi_span = spans[2] - urllib3_span = spans[3] - test_span = spans[4] + assert wsgi_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None - self.assertTrue(response) - self.assertEqual(200, response.status) - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_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'], wsgi_span.s) - - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') - - self.assertIn('Server-Timing', response.headers) - server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) - - # Same traceId - trace_id = test_span.t - self.assertEqual(trace_id, urllib3_span.t) - self.assertEqual(trace_id, wsgi_span.t) - self.assertEqual(trace_id, asteroid_span.t) - self.assertEqual(trace_id, spacedust_span.t) - - # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(asteroid_span.p, wsgi_span.s) - self.assertEqual(spacedust_span.p, asteroid_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(asteroid_span.ec) - self.assertIsNone(spacedust_span.ec) - - # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/complex', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - def test_custom_header_capture(self): + def test_custom_header_capture(self) -> None: # Hack together a manual custom headers list agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] @@ -176,210 +115,214 @@ def test_custom_header_capture(self): request_headers['X-Capture-This'] = 'this' request_headers['X-Capture-That'] = 'that' - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert 3 == len(spans) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - self.assertIn("X-Capture-This", wsgi_span.data["http"]["header"]) - self.assertEqual("this", wsgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", wsgi_span.data["http"]["header"]) - self.assertEqual("that", wsgi_span.data["http"]["header"]["X-Capture-That"]) - - def test_secret_scrubbing(self): - with tracer.start_active_span('test'): + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + assert "X-Capture-This" in wsgi_span.data["http"]["header"] + assert "this" == wsgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in wsgi_span.data["http"]["header"] + assert "that" == wsgi_span.data["http"]["header"]["X-Capture-That"] + + def test_secret_scrubbing(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/?secret=shhh') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert 3 == len(spans) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('secret=', wsgi_span.data["http"]["params"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - def test_with_incoming_context(self): + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'secret=' == wsgi_span.data["http"]["params"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + def test_with_incoming_context(self) -> None: request_headers = dict() request_headers['X-INSTANA-T'] = '0000000000000001' request_headers['X-INSTANA-S'] = '0000000000000001' response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) wsgi_span = spans[0] - self.assertEqual(wsgi_span.t, '0000000000000001') - self.assertEqual(wsgi_span.p, '0000000000000001') + # assert wsgi_span.t == '0000000000000001' + # assert wsgi_span.p == '0000000000000001' + assert wsgi_span.t == 1 + assert wsgi_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value - def test_with_incoming_mixed_case_context(self): + def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() request_headers['X-InSTANa-T'] = '0000000000000001' request_headers['X-instana-S'] = '0000000000000001' response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) wsgi_span = spans[0] - self.assertEqual(wsgi_span.t, '0000000000000001') - self.assertEqual(wsgi_span.p, '0000000000000001') + # assert wsgi_span.t == '0000000000000001' + # assert wsgi_span.p == '0000000000000001' + assert wsgi_span.t == 1 + assert wsgi_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value - def test_response_headers(self): - with tracer.start_active_span('test'): + def test_response_headers(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert 3 == len(spans) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_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'], wsgi_span.s) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_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" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value diff --git a/tests/helpers.py b/tests/helpers.py index 95fe3e61..7a24bdc8 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -9,51 +9,52 @@ """ Cassandra Environment """ -testenv['cassandra_host'] = os.environ.get('CASSANDRA_HOST', '127.0.0.1') -testenv['cassandra_username'] = os.environ.get('CASSANDRA_USERNAME', 'Administrator') -testenv['cassandra_password'] = os.environ.get('CASSANDRA_PASSWORD', 'password') +testenv["cassandra_host"] = os.environ.get("CASSANDRA_HOST", "127.0.0.1") +testenv["cassandra_username"] = os.environ.get("CASSANDRA_USERNAME", "Administrator") +testenv["cassandra_password"] = os.environ.get("CASSANDRA_PASSWORD", "password") """ CouchDB Environment """ -testenv['couchdb_host'] = os.environ.get('COUCHDB_HOST', '127.0.0.1') -testenv['couchdb_username'] = os.environ.get('COUCHDB_USERNAME', 'Administrator') -testenv['couchdb_password'] = os.environ.get('COUCHDB_PASSWORD', 'password') +testenv["couchdb_host"] = os.environ.get("COUCHDB_HOST", "127.0.0.1") +testenv["couchdb_username"] = os.environ.get("COUCHDB_USERNAME", "Administrator") +testenv["couchdb_password"] = os.environ.get("COUCHDB_PASSWORD", "password") """ MySQL Environment """ -if 'MYSQL_HOST' in os.environ: - testenv['mysql_host'] = os.environ['MYSQL_HOST'] +if "MYSQL_HOST" in os.environ: + testenv["mysql_host"] = os.environ["MYSQL_HOST"] else: - testenv['mysql_host'] = '127.0.0.1' + testenv["mysql_host"] = "127.0.0.1" -testenv['mysql_port'] = int(os.environ.get('MYSQL_PORT', '3306')) -testenv['mysql_db'] = os.environ.get('MYSQL_DATABASE', 'instana_test_db') -testenv['mysql_user'] = os.environ.get('MYSQL_USER', 'root') -testenv['mysql_pw'] = os.environ.get('MYSQL_ROOT_PASSWORD', 'passw0rd') +testenv["mysql_port"] = int(os.environ.get("MYSQL_PORT", "3306")) +testenv["mysql_db"] = os.environ.get("MYSQL_DATABASE", "instana_test_db") +testenv["mysql_user"] = os.environ.get("MYSQL_USER", "root") +testenv["mysql_pw"] = os.environ.get("MYSQL_ROOT_PASSWORD", "passw0rd") """ PostgreSQL Environment """ -testenv['postgresql_host'] = os.environ.get('POSTGRES_HOST', '127.0.0.1') -testenv['postgresql_port'] = int(os.environ.get('POSTGRES_PORT', '5432')) -testenv['postgresql_db'] = os.environ.get('POSTGRES_DB', 'instana_test_db') -testenv['postgresql_user'] = os.environ.get('POSTGRES_USER', 'root') -testenv['postgresql_pw'] = os.environ.get('POSTGRES_PW', 'passw0rd') +testenv["postgresql_host"] = os.environ.get("POSTGRES_HOST", "127.0.0.1") +testenv["postgresql_port"] = int(os.environ.get("POSTGRES_PORT", "5432")) +testenv["postgresql_db"] = os.environ.get("POSTGRES_DB", "instana_test_db") +testenv["postgresql_user"] = os.environ.get("POSTGRES_USER", "root") +testenv["postgresql_pw"] = os.environ.get("POSTGRES_PW", "passw0rd") """ Redis Environment """ -testenv['redis_host'] = os.environ.get('REDIS_HOST', '127.0.0.1') +testenv["redis_host"] = os.environ.get("REDIS_HOST", "127.0.0.1") +testenv["redis_db"] = os.environ.get("REDIS_DB", 0) """ MongoDB Environment """ -testenv['mongodb_host'] = os.environ.get('MONGO_HOST', '127.0.0.1') -testenv['mongodb_port'] = os.environ.get('MONGO_PORT', '27017') -testenv['mongodb_user'] = os.environ.get('MONGO_USER', None) -testenv['mongodb_pw'] = os.environ.get('MONGO_PW', None) +testenv["mongodb_host"] = os.environ.get("MONGO_HOST", "127.0.0.1") +testenv["mongodb_port"] = os.environ.get("MONGO_PORT", "27017") +testenv["mongodb_user"] = os.environ.get("MONGO_USER", None) +testenv["mongodb_pw"] = os.environ.get("MONGO_PW", None) def drop_log_spans_from_list(spans): @@ -66,7 +67,7 @@ def drop_log_spans_from_list(spans): """ new_list = [] for span in spans: - if span.n != 'log': + if span.n != "log": new_list.append(span) return new_list @@ -84,8 +85,8 @@ def fail_with_message_and_span_dump(msg, spans): span_dump = "\nDumping all collected spans (%d) -->\n" % span_count if span_count > 0: for span in spans: - span.stack = '' - span_dump += repr(span) + '\n' + span.stack = "" + span_dump += repr(span) + "\n" pytest.fail(msg + span_dump, True) @@ -144,9 +145,11 @@ def launch_traced_request(url): from instana.log import logger from instana.singletons import tracer - logger.warn("Launching request with a root SDK span name of 'launch_traced_request'") + logger.warn( + "Launching request with a root SDK span name of 'launch_traced_request'" + ) - with tracer.start_active_span('launch_traced_request'): + with tracer.start_as_current_span("launch_traced_request"): response = requests.get(url) return response diff --git a/tests/opentracing/test_opentracing.py b/tests/opentracing/test_opentracing.py deleted file mode 100644 index 0ed9e508..00000000 --- a/tests/opentracing/test_opentracing.py +++ /dev/null @@ -1,28 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -from unittest import SkipTest -from opentracing.harness.api_check import APICompatibilityCheckMixin - -from instana.tracer import InstanaTracer - - -class TestInstanaTracer(InstanaTracer, APICompatibilityCheckMixin): - def tracer(self): - return self - - def test_binary_propagation(self): - raise SkipTest('Binary format is not supported') - - def test_mandatory_formats(self): - raise SkipTest('Binary format is not supported') - - def check_baggage_values(self): - return True - - def is_parent(self, parent, span): - # use `Span` ids to check parenting - if parent is None: - return span.parent_id is None - - return parent.context.span_id == span.parent_id diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py deleted file mode 100644 index de26f471..00000000 --- a/tests/opentracing/test_ot_propagators.py +++ /dev/null @@ -1,309 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import inspect -import unittest - -import opentracing as ot - -import instana.propagators.http_propagator as ihp -import instana.propagators.text_propagator as itp -import instana.propagators.binary_propagator as ibp -from instana.span_context import SpanContext -from instana.tracer import InstanaTracer - - -class TestOTSpan(unittest.TestCase): - def test_http_basics(self): - inspect.isclass(ihp.HTTPPropagator) - - inject_func = getattr(ihp.HTTPPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(ihp.HTTPPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_http_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier) - - self.assertIn('X-INSTANA-T', carrier) - self.assertEqual(carrier['X-INSTANA-T'], span.context.trace_id) - self.assertIn('X-INSTANA-S', carrier) - self.assertEqual(carrier['X-INSTANA-S'], span.context.span_id) - self.assertIn('X-INSTANA-L', carrier) - self.assertEqual(carrier['X-INSTANA-L'], "1") - - - def test_http_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier) - - self.assertIn(('X-INSTANA-T', span.context.trace_id), carrier) - self.assertIn(('X-INSTANA-S', span.context.span_id), carrier) - self.assertIn(('X-INSTANA-L', "1"), carrier) - - - def test_http_basic_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-T': '1', 'X-INSTANA-S': '1', 'X-INSTANA-L': '1', 'X-INSTANA-SYNTHETIC': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_extract_with_byte_keys(self): - ot.tracer = InstanaTracer() - - carrier = {b'X-INSTANA-T': '1', b'X-INSTANA-S': '1', b'X-INSTANA-L': '1', b'X-INSTANA-SYNTHETIC': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_extract_from_list_of_tuples(self): - ot.tracer = InstanaTracer() - - carrier = [(b'user-agent', b'python-requests/2.23.0'), (b'accept-encoding', b'gzip, deflate'), - (b'accept', b'*/*'), (b'connection', b'keep-alive'), - (b'x-instana-t', b'1'), (b'x-instana-s', b'1'), (b'x-instana-l', b'1'), (b'X-INSTANA-SYNTHETIC', '1')] - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertFalse(ctx.synthetic) - - - def test_http_extract_synthetic_only(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-SYNTHETIC': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertTrue(ctx.synthetic) - - - def test_http_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - def test_http_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-T': '0000000000000000b0789916ff8f319f', - 'X-INSTANA-S': '0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') - - - def test_text_basics(self): - inspect.isclass(itp.TextPropagator) - - inject_func = getattr(itp.TextPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(itp.TextPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_text_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.TEXT_MAP, carrier) - - self.assertIn('x-instana-t', carrier) - self.assertEqual(carrier['x-instana-t'], span.context.trace_id) - self.assertIn('x-instana-s', carrier) - self.assertEqual(carrier['x-instana-s'], span.context.span_id) - self.assertIn('x-instana-l', carrier) - self.assertEqual(carrier['x-instana-l'], "1") - - - def test_text_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.TEXT_MAP, carrier) - - self.assertIn(('x-instana-t', span.context.trace_id), carrier) - self.assertIn(('x-instana-s', span.context.span_id), carrier) - self.assertIn(('x-instana-l', "1"), carrier) - - - def test_text_basic_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-instana-t': '1', 'x-instana-s': '1', 'x-instana-l': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - - - def test_text_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - - - def test_text_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - - def test_text_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'x-instana-t': '0000000000000000b0789916ff8f319f', - 'x-instana-s': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') - - def test_binary_basics(self): - inspect.isclass(ibp.BinaryPropagator) - - inject_func = getattr(ibp.BinaryPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(ibp.BinaryPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_binary_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.BINARY, carrier) - - self.assertIn(b'x-instana-t', carrier) - self.assertEqual(carrier[b'x-instana-t'], str.encode(span.context.trace_id)) - self.assertIn(b'x-instana-s', carrier) - self.assertEqual(carrier[b'x-instana-s'], str.encode(span.context.span_id)) - self.assertIn(b'x-instana-l', carrier) - self.assertEqual(carrier[b'x-instana-l'], b'1') - - - def test_binary_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.BINARY, carrier) - - self.assertIn((b'x-instana-t', str.encode(span.context.trace_id)), carrier) - self.assertIn((b'x-instana-s', str.encode(span.context.span_id)), carrier) - self.assertIn((b'x-instana-l', b'1'), carrier) - - - def test_binary_basic_extract(self): - ot.tracer = InstanaTracer() - - carrier = {b'X-INSTANA-T': b'1', b'X-INSTANA-S': b'1', b'X-INSTANA-L': b'1', b'X-INSTANA-SYNTHETIC': b'1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_binary_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1', b'X-inStaNa-SYNtheTIC': b'1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_binary_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - - def test_binary_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-T': '0000000000000000b0789916ff8f319f', - 'X-INSTANA-S': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') diff --git a/tests/opentracing/test_ot_span.py b/tests/opentracing/test_ot_span.py deleted file mode 100644 index 9280df76..00000000 --- a/tests/opentracing/test_ot_span.py +++ /dev/null @@ -1,290 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import re -import sys -import json -import time -import unittest -from uuid import UUID - -import opentracing - -from instana.util import to_json -from instana.singletons import agent, tracer -from ..helpers import get_first_span_by_filter - - -class TestOTSpan(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - agent.options.service_name = None - opentracing.tracer = tracer - recorder = opentracing.tracer.recorder - recorder.clear_spans() - - def tearDown(self): - """ Do nothing for now """ - return None - - def test_span_interface(self): - span = opentracing.tracer.start_span("blah") - self.assertTrue(hasattr(span, "finish")) - self.assertTrue(hasattr(span, "set_tag")) - self.assertTrue(hasattr(span, "tags")) - self.assertTrue(hasattr(span, "operation_name")) - self.assertTrue(hasattr(span, "set_baggage_item")) - self.assertTrue(hasattr(span, "get_baggage_item")) - self.assertTrue(hasattr(span, "context")) - self.assertTrue(hasattr(span, "log")) - - def test_span_ids(self): - count = 0 - while count <= 1000: - count += 1 - span = opentracing.tracer.start_span("test_span_ids") - context = span.context - self.assertTrue(0 <= int(context.span_id, 16) <= 18446744073709551615) - self.assertTrue(0 <= int(context.trace_id, 16) <= 18446744073709551615) - - # Python 3.11 support is incomplete yet - # TODO: Remove this once we find a workaround or DROP opentracing! - @unittest.skipIf(sys.version_info >= (3, 11), reason="Raises not Implemented exception in OSX") - def test_stacks(self): - # Entry spans have no stack attached by default - wsgi_span = opentracing.tracer.start_span("wsgi") - self.assertIsNone(wsgi_span.stack) - - # SDK spans have no stack attached by default - sdk_span = opentracing.tracer.start_span("unregistered_span_type") - self.assertIsNone(sdk_span.stack) - - # Exit spans are no longer than 30 frames - exit_span = opentracing.tracer.start_span("urllib3") - self.assertLessEqual(len(exit_span.stack), 30) - - def test_span_fields(self): - span = opentracing.tracer.start_span("mycustom") - self.assertEqual("mycustom", span.operation_name) - self.assertTrue(span.context) - - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - - self.assertEqual("string", span.tags['tagone']) - self.assertEqual(150, span.tags['tagtwo']) - - @unittest.skipIf(sys.platform == "darwin", reason="Raises not Implemented exception in OSX") - def test_span_queueing(self): - recorder = opentracing.tracer.recorder - - count = 1 - while count <= 20: - count += 1 - span = opentracing.tracer.start_span("queuethisplz") - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - span.finish() - - self.assertEqual(20, recorder.queue_size()) - - def test_sdk_spans(self): - recorder = opentracing.tracer.recorder - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - span.set_tag('span.kind', "entry") - time.sleep(0.5) - span.finish() - - spans = recorder.queued_spans() - self.assertEqual(1, len(spans)) - - sdk_span = spans[0] - self.assertEqual('sdk', sdk_span.n) - self.assertEqual(None, sdk_span.p) - self.assertEqual(sdk_span.s, sdk_span.t) - self.assertTrue(sdk_span.ts) - self.assertGreater(sdk_span.ts, 0) - self.assertTrue(sdk_span.d) - self.assertGreater(sdk_span.d, 0) - - self.assertTrue(sdk_span.data) - self.assertTrue(sdk_span.data["sdk"]) - self.assertEqual('entry', sdk_span.data["sdk"]["type"]) - self.assertEqual('custom_sdk_span', sdk_span.data["sdk"]["name"]) - self.assertTrue(sdk_span.data["sdk"]["custom"]) - self.assertTrue(sdk_span.data["sdk"]["custom"]["tags"]) - - def test_span_kind(self): - recorder = opentracing.tracer.recorder - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "consumer") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "server") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "producer") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "client") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "blah") - span.finish() - - spans = recorder.queued_spans() - self.assertEqual(5, len(spans)) - - span = spans[0] - self.assertEqual('entry', span.data["sdk"]["type"]) - - span = spans[1] - self.assertEqual('entry', span.data["sdk"]["type"]) - - span = spans[2] - self.assertEqual('exit', span.data["sdk"]["type"]) - - span = spans[3] - self.assertEqual('exit', span.data["sdk"]["type"]) - - span = spans[4] - self.assertEqual('intermediate', span.data["sdk"]["type"]) - - span = spans[0] - self.assertEqual(1, span.k) - - span = spans[1] - self.assertEqual(1, span.k) - - span = spans[2] - self.assertEqual(2, span.k) - - span = spans[3] - self.assertEqual(2, span.k) - - span = spans[4] - self.assertEqual(3, span.k) - - def test_tag_values(self): - with tracer.start_active_span('test') as scope: - # Set a UUID class as a tag - # If unchecked, this causes a json.dumps error: "ValueError: Circular reference detected" - scope.span.set_tag('uuid', UUID(bytes=b'\x12\x34\x56\x78'*4)) - # Arbitrarily setting an instance of some class - scope.span.set_tag('tracer', tracer) - scope.span.set_tag('none', None) - scope.span.set_tag('mylist', [1, 2, 3]) - scope.span.set_tag('myset', {"one", 2}) - - spans = tracer.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - test_span = spans[0] - self.assertTrue(test_span) - self.assertEqual(len(test_span.data['sdk']['custom']['tags']), 5) - self.assertEqual(test_span.data['sdk']['custom']['tags']['uuid'], "UUID('12345678-1234-5678-1234-567812345678')") - self.assertTrue(test_span.data['sdk']['custom']['tags']['tracer']) - self.assertEqual(test_span.data['sdk']['custom']['tags']['none'], 'None') - self.assertListEqual(test_span.data['sdk']['custom']['tags']['mylist'], [1, 2, 3]) - self.assertRegex(test_span.data['sdk']['custom']['tags']['myset'], r"\{.*,.*\}") - - # Convert to JSON - json_data = to_json(test_span) - self.assertTrue(json_data) - - # And back - span_dict = json.loads(json_data) - self.assertEqual(len(span_dict['data']['sdk']['custom']['tags']), 5) - self.assertEqual(span_dict['data']['sdk']['custom']['tags']['uuid'], "UUID('12345678-1234-5678-1234-567812345678')") - self.assertTrue(span_dict['data']['sdk']['custom']['tags']['tracer']) - self.assertEqual(span_dict['data']['sdk']['custom']['tags']['none'], 'None') - self.assertListEqual(span_dict['data']['sdk']['custom']['tags']['mylist'], [1, 2, 3]) - self.assertRegex(test_span.data['sdk']['custom']['tags']['myset'], r"{.*,.*}") - - def test_tag_names(self): - with tracer.start_active_span('test') as scope: - # Tag names (keys) must be strings - scope.span.set_tag(1234567890, 'This should not get set') - # Unicode key name - scope.span.set_tag(u'asdf', 'This should be ok') - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 1) - - test_span = spans[0] - self.assertTrue(test_span) - self.assertEqual(len(test_span.data['sdk']['custom']['tags']), 1) - self.assertEqual(test_span.data['sdk']['custom']['tags']['asdf'], 'This should be ok') - - json_data = to_json(test_span) - self.assertTrue(json_data) - - def test_custom_service_name(self): - # Set a custom service name - agent.options.service_name = "custom_service_name" - - with tracer.start_active_span('entry_span') as scope: - scope.span.set_tag('span.kind', 'server') - scope.span.set_tag(u'type', 'entry_span') - - with tracer.start_active_span('intermediate_span', child_of=scope.span) as exit_scope: - exit_scope.span.set_tag(u'type', 'intermediate_span') - - with tracer.start_active_span('exit_span', child_of=scope.span) as exit_scope: - exit_scope.span.set_tag('span.kind', 'client') - exit_scope.span.set_tag(u'type', 'exit_span') - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "entry_span" - entry_span = get_first_span_by_filter(spans, filter) - self.assertTrue(entry_span) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "intermediate_span" - intermediate_span = get_first_span_by_filter(spans, filter) - self.assertTrue(intermediate_span) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "exit_span" - exit_span = get_first_span_by_filter(spans, filter) - self.assertTrue(exit_span) - - self.assertTrue(entry_span) - self.assertEqual(len(entry_span.data['sdk']['custom']['tags']), 2) - self.assertEqual(entry_span.data['sdk']['custom']['tags']['type'], 'entry_span') - self.assertEqual(entry_span.data['service'], 'custom_service_name') - self.assertEqual(entry_span.k, 1) - - self.assertTrue(intermediate_span) - self.assertEqual(len(intermediate_span.data['sdk']['custom']['tags']), 1) - self.assertEqual(intermediate_span.data['sdk']['custom']['tags']['type'], 'intermediate_span') - self.assertEqual(intermediate_span.data['service'], 'custom_service_name') - self.assertEqual(intermediate_span.k, 3) - - self.assertTrue(exit_span) - self.assertEqual(len(exit_span.data['sdk']['custom']['tags']), 2) - self.assertEqual(exit_span.data['sdk']['custom']['tags']['type'], 'exit_span') - self.assertEqual(exit_span.data['service'], 'custom_service_name') - self.assertEqual(exit_span.k, 2) - - def test_span_log(self): - with tracer.start_active_span('mylogspan') as scope: - scope.span.log_kv({'Don McLean': 'American Pie'}) - scope.span.log_kv({'Elton John': 'Your Song'}) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 1) - - my_log_span = spans[0] - self.assertEqual(my_log_span.n, 'sdk') - - log_data = my_log_span.data['sdk']['custom']['logs'] - self.assertEqual(len(log_data), 2) diff --git a/tests/opentracing/test_ot_tracer.py b/tests/opentracing/test_ot_tracer.py deleted file mode 100644 index c73037f4..00000000 --- a/tests/opentracing/test_ot_tracer.py +++ /dev/null @@ -1,10 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import opentracing - - -def test_tracer_basics(): - assert hasattr(opentracing.tracer, "start_span") - assert hasattr(opentracing.tracer, "inject") - assert hasattr(opentracing.tracer, "extract") diff --git a/tests/platforms/__init__.py b/tests/platforms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/platforms/test_host.py b/tests/platforms/test_host.py deleted file mode 100644 index 2ca09804..00000000 --- a/tests/platforms/test_host.py +++ /dev/null @@ -1,273 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import logging -import unittest - -from mock import MagicMock, patch -import requests - -from instana.agent.host import HostAgent -from instana.fsm import Discovery -from instana.log import logger -from instana.options import StandardOptions -from instana.recorder import StanRecorder -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.tracer import InstanaTracer - - -class TestHost(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestHost, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - pass - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", - "INSTANA_SERVICE_NAME", "INSTANA_SECRETS", "INSTANA_TAGS", - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_secrets(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_options_have_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, StandardOptions)) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) - - def test_agent_instana_debug(self): - os.environ['INSTANA_DEBUG'] = "asdf" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.DEBUG) - - def test_agent_instana_service_name(self): - os.environ['INSTANA_SERVICE_NAME'] = "greycake" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.service_name, "greycake") - - @patch.object(requests.Session, "put") - def test_announce_is_successful(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "pid": {test_pid}, ' - f' "agentUuid": "{test_agent_uuid}"' - '}') - - # This mocks the call to self.agent.client.put - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - payload = self.agent.announce(d) - - self.assertIn('pid', payload) - self.assertEqual(test_pid, payload['pid']) - - self.assertIn('agentUuid', payload) - self.assertEqual(test_agent_uuid, payload['agentUuid']) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_non_200(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 404 - mock_response.content = '' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response status code', log.output[0]) - self.assertIn('is NOT 200', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_non_json(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = '' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response is not JSON', log.output[0]) - - @patch.object(requests.Session, "put") - def test_announce_fails_with_empty_list_json(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = '[]' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('payload has no fields', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_pid(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "agentUuid": "{test_agent_uuid}"' - '}') - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no pid', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_uuid(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "pid": {test_pid} ' - '}') - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no agentUuid', log.output[0]) - - - @patch.object(requests.Session, "get") - def test_agent_connection_attempt(self, mock_requests_session_get): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_requests_session_get.return_value = mock_response - - self.create_agent_and_setup_tracer() - host = self.agent.options.agent_host - port = self.agent.options.agent_port - msg = f"Instana host agent found on {host}:{port}" - - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) - - self.assertTrue(result) - self.assertIn(msg, log.output[0]) - - - @patch.object(requests.Session, "get") - def test_agent_connection_attempt_fails_with_404(self, mock_requests_session_get): - mock_response = MagicMock() - mock_response.status_code = 404 - mock_requests_session_get.return_value = mock_response - - self.create_agent_and_setup_tracer() - host = self.agent.options.agent_host - port = self.agent.options.agent_port - msg = "The attempt to connect to the Instana host agent on " \ - f"{host}:{port} has failed with an unexpected status code. " \ - f"Expected HTTP 200 but received: {mock_response.status_code}" - - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) - - self.assertFalse(result) - self.assertIn(msg, log.output[0]) diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py deleted file mode 100644 index a53c801e..00000000 --- a/tests/platforms/test_host_collector.py +++ /dev/null @@ -1,242 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import unittest -import sys - -from mock import patch - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.host import HostAgent -from instana.collector.helpers.runtime import PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR -from instana.collector.host import HostCollector -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.version import VERSION - -class TestHostCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestHostCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + '3.8.0' - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_AGENT_KEY", "INSTANA_ZONE", - "INSTANA_TAGS", "INSTANA_DISABLE_METRICS_COLLECTION", - "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", - "AUTOWRAPT_BOOTSTRAP" - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - if self.webhook_sitedir_path in sys.path: - sys.path.remove(self.webhook_sitedir_path) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_prepare_payload_basics(self): - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn('spans', payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(len(payload['spans']), 0) - self.assertIn('metrics', payload) - self.assertEqual(len(payload['metrics'].keys()), 1) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(len(payload['metrics']['plugins']), 1) - - python_plugin = payload['metrics']['plugins'][0] - self.assertEqual(python_plugin['name'], 'com.instana.plugin.python') - self.assertEqual(python_plugin['entityId'], str(os.getpid())) - self.assertIn('data', python_plugin) - self.assertIn('snapshot', python_plugin['data']) - self.assertIn('m', python_plugin['data']['snapshot']) - self.assertEqual('Manual', python_plugin['data']['snapshot']['m']) - self.assertIn('metrics', python_plugin['data']) - - # Validate that all metrics are reported on the first run - self.assertIn('ru_utime', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_utime']), [float, int]) - self.assertIn('ru_stime', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_stime']), [float, int]) - self.assertIn('ru_maxrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_maxrss']), [float, int]) - self.assertIn('ru_ixrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_ixrss']), [float, int]) - self.assertIn('ru_idrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_idrss']), [float, int]) - self.assertIn('ru_isrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_isrss']), [float, int]) - self.assertIn('ru_minflt', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_minflt']), [float, int]) - self.assertIn('ru_majflt', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_majflt']), [float, int]) - self.assertIn('ru_nswap', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nswap']), [float, int]) - self.assertIn('ru_inblock', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_inblock']), [float, int]) - self.assertIn('ru_oublock', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_oublock']), [float, int]) - self.assertIn('ru_msgsnd', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_msgsnd']), [float, int]) - self.assertIn('ru_msgrcv', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_msgrcv']), [float, int]) - self.assertIn('ru_nsignals', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nsignals']), [float, int]) - self.assertIn('ru_nvcsw', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nvcsw']), [float, int]) - self.assertIn('ru_nivcsw', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nivcsw']), [float, int]) - self.assertIn('alive_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['alive_threads']), [float, int]) - self.assertIn('dummy_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['dummy_threads']), [float, int]) - self.assertIn('daemon_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['daemon_threads']), [float, int]) - - self.assertIn('gc', python_plugin['data']['metrics']) - self.assertIsInstance(python_plugin['data']['metrics']['gc'], dict) - self.assertIn('collect0', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect0']), [float, int]) - self.assertIn('collect1', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect1']), [float, int]) - self.assertIn('collect2', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect2']), [float, int]) - self.assertIn('threshold0', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold0']), [float, int]) - self.assertIn('threshold1', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold1']), [float, int]) - self.assertIn('threshold2', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold2']), [float, int]) - - def test_prepare_payload_basics_disable_runtime_metrics(self): - os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn('spans', payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(len(payload['spans']), 0) - self.assertIn('metrics', payload) - self.assertEqual(len(payload['metrics'].keys()), 1) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(len(payload['metrics']['plugins']), 1) - - python_plugin = payload['metrics']['plugins'][0] - self.assertEqual(python_plugin['name'], 'com.instana.plugin.python') - self.assertEqual(python_plugin['entityId'], str(os.getpid())) - self.assertIn('data', python_plugin) - self.assertIn('snapshot', python_plugin['data']) - self.assertIn('m', python_plugin['data']['snapshot']) - self.assertEqual('Manual', python_plugin['data']['snapshot']['m']) - self.assertNotIn('metrics', python_plugin['data']) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_with_python_packages(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] - self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Manual', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - self.assertEqual(snapshot['versions']['instana'], VERSION) - self.assertIn('wrapt', snapshot['versions']) - self.assertIn('fysom', snapshot['versions']) - self.assertIn('opentracing', snapshot['versions']) - self.assertIn('basictracer', snapshot['versions']) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_disabled_python_packages(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] - self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Manual', snapshot['m']) - self.assertIn('version', snapshot) - self.assertEqual(len(snapshot['versions']), 1) - self.assertEqual(snapshot['versions']['instana'], VERSION) - - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] - self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Autowrapt', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom', 'opentracing', 'basictracer') - for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) - - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - - sys.path.append(self.webhook_sitedir_path) - - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] - self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('AutoTrace', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom', 'opentracing', 'basictracer') - for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) diff --git a/tests/propagators/test_base_propagator.py b/tests/propagators/test_base_propagator.py new file mode 100644 index 00000000..4c86a009 --- /dev/null +++ b/tests/propagators/test_base_propagator.py @@ -0,0 +1,93 @@ +import pytest + +from typing import Generator +from instana.propagators.base_propagator import BasePropagator +from unittest.mock import Mock + + +class TestBasePropagator: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.propagator = BasePropagator() + yield + self.propagator = None + + def test_extract_headers_dict(self) -> None: + carrier_as_a_dict = {"key": "value"} + assert carrier_as_a_dict == self.propagator.extract_headers_dict( + carrier_as_a_dict + ) + mocked_carrier = Mock() + mocked_carrier.__dict__ = carrier_as_a_dict + assert carrier_as_a_dict == self.propagator.extract_headers_dict(mocked_carrier) + wrong_carrier = "value" + assert self.propagator.extract_headers_dict(wrong_carrier) is None + + def test_get_ctx_level(self) -> None: + assert 3 == self.propagator._get_ctx_level("3,4") + assert 1 == self.propagator._get_ctx_level("wrong_data") + + def test_get_correlation_properties(self) -> None: + a, b = self.propagator._get_correlation_properties( + ",correlationType=3;correlationId=5;" + ) + assert a == "3" + assert b == "5" + assert "3", None == self.propagator._get_correlation_properties( # noqa: E711 + ",correlationType=3;" + ) + + def test_get_participating_trace_context(self, span_context) -> None: + traceparent, tracestate = self.propagator._get_participating_trace_context( + span_context + ) + assert traceparent == "00-1812338823475918251-6895521157646639861-01" + assert tracestate == "in=1812338823475918251;6895521157646639861" + + def test_extract_instana_headers(self) -> None: + dc = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode(",correlationType=3;correlationId=5;"), + "x-instana-synthetic": "1", + } + trace_id, span_id, level, synthetic = self.propagator.extract_instana_headers( + dc=dc + ) + assert trace_id == 123456789 + assert span_id == 12345 + assert level == ",correlationType=3;correlationId=5;" + assert synthetic + + def test_extract(self) -> None: + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode("3,correlationId=5;"), + "x-instana-synthetic": "1", + "traceparent": "00-1812338823475918251-6895521157646639861-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract( + carrier=carrier, disable_w3c_trace_context=True + ) + assert span_context + span_context = self.propagator.extract(carrier=carrier) + span_context = self.propagator.extract( + carrier=None, disable_w3c_trace_context=True + ) + assert not span_context + carrier.pop("x-instana-t", None) + carrier.pop("x-instana-s", None) + span_context = self.propagator.extract(carrier=carrier) + assert span_context + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": "2,correlationType=3;correlationId=5;", + "x-instana-synthetic": "1", + "traceparent": "00-4bf92f3577b34da61234567899999999-1234567890888888-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract(carrier=carrier) + assert span_context diff --git a/tests/recorder/test_stan_recorder.py b/tests/recorder/test_stan_recorder.py index adb08e78..5f9940f6 100644 --- a/tests/recorder/test_stan_recorder.py +++ b/tests/recorder/test_stan_recorder.py @@ -1,9 +1,17 @@ -from instana.recorder import StanRecorder - from multiprocessing import Queue +import sys from unittest import TestCase from unittest.mock import NonCallableMagicMock, PropertyMock +import pytest + +from instana.recorder import StanRecorder + + +@pytest.mark.skipif( + sys.platform == "darwin", + reason="Avoiding NotImplementedError when calling multiprocessing.Queue.qsize()", +) class TestStanRecorderTC(TestCase): def setUp(self): mock_agent = NonCallableMagicMock() @@ -13,7 +21,9 @@ def setUp(self): self.mock_suppressed_span = NonCallableMagicMock() self.mock_suppressed_span.context = NonCallableMagicMock() self.mock_suppressed_property = PropertyMock(return_value=True) - type(self.mock_suppressed_span.context).suppression = self.mock_suppressed_property + type( + self.mock_suppressed_span.context + ).suppression = self.mock_suppressed_property def test_record_span_with_suppression(self): # Ensure that the queue is empty diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 22514153..88be77c3 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0 @@ -29,12 +30,15 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index 8e8aeb34..e2fdf83d 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0a1 --pre @@ -27,12 +28,15 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 44261b13..46e24cc6 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0a1 --pre @@ -15,6 +16,9 @@ markupsafe>=2.1.0 # Depends on grpcio #google-cloud-pubsub<=2.1.0 #google-cloud-storage>=1.24.0 +# The `legacy-cgi` package is a drop-in replacement for the `cgi` package, +# which was removed from Python 3.13 onwards. `Bottle` framework still uses `cgi`. +legacy-cgi>=2.6.1 lxml>=4.9.2 mock>=4.0.3 moto>=4.1.2 @@ -34,15 +38,18 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -# Newer versions of sanic are not supported -# And this old version is not installable on 3.13 because of the `httptools` dependency fails to compile: +# Sanic is not installable on 3.13 because `httptools, uvloop` dependencies fail to compile: # `too few arguments to function ‘_PyLong_AsByteArray’` -#sanic==21.6.2 +#sanic>=19.9.0 +#sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 +starlette>=0.38.2 diff --git a/tests/requirements-gevent-starlette.txt b/tests/requirements-gevent-starlette.txt index 1333f76c..869b7186 100644 --- a/tests/requirements-gevent-starlette.txt +++ b/tests/requirements-gevent-starlette.txt @@ -7,3 +7,4 @@ pytest>=4.6 starlette>=0.12.13 urllib3>=1.26.5 uvicorn>=0.13.4 +httpx>=0.27.0 diff --git a/tests/requirements.txt b/tests/requirements.txt index 2310401c..01cdd36f 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=4.2.4 @@ -28,12 +29,15 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 tornado>=4.5.3,<6.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 diff --git a/tests/span/test_base_span.py b/tests/span/test_base_span.py new file mode 100644 index 00000000..5f1a16c4 --- /dev/null +++ b/tests/span/test_base_span.py @@ -0,0 +1,160 @@ +# (c) Copyright IBM Corp. 2024 + +from unittest.mock import Mock, patch + +from instana.recorder import StanRecorder +from instana.span.base_span import BaseSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from instana.util import DictionaryOfStan + + +def test_basespan( + span: InstanaSpan, + trace_id: int, + span_id: int, +) -> None: + base_span = BaseSpan(span, None) + + expected_dict = { + "t": trace_id, + "p": None, + "s": span_id, + "l": 1, + "ts": round(span.start_time / 10**6), + "d": None, + "f": None, + "ec": None, + "data": DictionaryOfStan(), + "stack": None, + } + + assert expected_dict["t"] == base_span.t + assert expected_dict["s"] == base_span.s + assert expected_dict["p"] == base_span.p + assert expected_dict["l"] == base_span.l + assert expected_dict["ts"] == base_span.ts + assert expected_dict["d"] == base_span.d + assert not base_span.f + assert expected_dict["ec"] == base_span.ec + assert isinstance(base_span.data, dict) + assert expected_dict["stack"] == base_span.stack + assert not base_span.sy + + expected_dict_str = str(expected_dict) + assert expected_dict_str == repr(base_span) + assert f"BaseSpan({expected_dict_str})" == str(base_span) + + +def test_basespan_with_synthetic_source_and_kwargs( + span: InstanaSpan, + trace_id: int, + span_id: int, +) -> None: + span.synthetic = True + source = "source test" + _kwarg1 = "value1" + base_span = BaseSpan(span, source, arg1=_kwarg1) + + assert trace_id == base_span.t + assert span_id == base_span.s + assert base_span.sy + assert source == base_span.f + assert _kwarg1 == base_span.arg1 + + +def test_populate_extra_span_attributes(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None) + base_span._populate_extra_span_attributes(span) + + assert not hasattr(base_span, "tp") + assert not hasattr(base_span, "tp") + assert not hasattr(base_span, "ia") + assert not hasattr(base_span, "lt") + assert not hasattr(base_span, "crtp") + assert not hasattr(base_span, "crid") + + +def test_populate_extra_span_attributes_with_values( + trace_id: int, + span_id: int, + span_processor: StanRecorder, +) -> None: + long_id = 1512366075204170929049582354406559215 + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + synthetic=True, + trace_parent=True, + instana_ancestor="IDK", + long_trace_id=long_id, + correlation_type="IDK", + correlation_id=long_id, + ) + span = InstanaSpan("test-base-span", span_context, span_processor) + base_span = BaseSpan(span, None) + base_span._populate_extra_span_attributes(span) + + assert trace_id == base_span.t + assert span_id == base_span.s + assert base_span.sy + assert base_span.tp + assert "IDK" == base_span.ia + assert long_id == base_span.lt + assert "IDK" == base_span.crtp + assert long_id == base_span.crid + + +def test_validate_attributes(base_span: BaseSpan) -> None: + attributes = { + "field1": 1, + "field2": "two", + } + filtered_attributes = base_span._validate_attributes(attributes) + + assert isinstance(filtered_attributes, dict) + assert len(attributes) == len(filtered_attributes) + for key, value in attributes.items(): + assert key in filtered_attributes.keys() + assert value in filtered_attributes.values() + + +def test_validate_attribute_with_invalid_key_type(base_span: BaseSpan) -> None: + key = 1 + value = "one" + + (validated_key, validated_value) = base_span._validate_attribute(key, value) + + assert not validated_key + assert not validated_value + + +def test_validate_attribute_exception(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None) + key = "field1" + value = span + + with patch( + "instana.span.base_span.BaseSpan._convert_attribute_value", + side_effect=Exception("mocked error"), + ): + (validated_key, validated_value) = base_span._validate_attribute(key, value) + assert key == validated_key + assert not validated_value + + +def test_convert_attribute_value(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None) + value = span + + converted_value = base_span._convert_attribute_value(value) + assert " None: + mock = Mock() + mock.__repr__ = Mock(side_effect=Exception("mocked error")) + + converted_value = base_span._convert_attribute_value(mock) + assert not converted_value diff --git a/tests/span/test_event.py b/tests/span/test_event.py new file mode 100644 index 00000000..baa7521b --- /dev/null +++ b/tests/span/test_event.py @@ -0,0 +1,36 @@ +# (c) Copyright IBM Corp. 2024 + +import time + +from instana.span.readable_span import Event + + +def test_span_event_defaults(): + event_name = "test-span-event" + event = Event(event_name) + + assert event + assert isinstance(event, Event) + assert event.name == event_name + assert not event.attributes + assert isinstance(event.timestamp, int) + + +def test_span_event(): + event_name = "test-span-event" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp = time.time_ns() + + event = Event(event_name, attributes, timestamp) + + assert event + assert isinstance(event, Event) + assert event.name == event_name + assert event.attributes + assert len(event.attributes) == 2 + assert "field1" in event.attributes.keys() + assert "two" == event.attributes.get("field2") + assert event.timestamp == timestamp diff --git a/tests/span/test_readable_span.py b/tests/span/test_readable_span.py new file mode 100644 index 00000000..4c4717f2 --- /dev/null +++ b/tests/span/test_readable_span.py @@ -0,0 +1,91 @@ +import time +from instana.span.readable_span import Event, ReadableSpan +from instana.span_context import SpanContext +from opentelemetry.trace.status import Status, StatusCode + + +def test_event() -> None: + name = "sample-event" + test_event = Event(name) + + assert test_event.name == name + assert not test_event.attributes + assert test_event.timestamp < time.time_ns() + + +def test_event_with_params() -> None: + name = "sample-event" + attributes = ["attribute"] + timestamp = time.time_ns() + test_event = Event(name, attributes, timestamp) + + assert test_event.name == name + assert test_event.attributes == attributes + assert test_event.timestamp == timestamp + + +def test_readablespan( + span_context: SpanContext, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + timestamp = time.time_ns() + span = ReadableSpan(span_name, span_context) + + assert span is not None + assert isinstance(span, ReadableSpan) + assert span.name == span_name + + span_context = span.context + assert isinstance(span_context, SpanContext) + assert span_context.trace_id == trace_id + assert span_context.span_id == span_id + + assert span.start_time + assert isinstance(span.start_time, int) + assert span.start_time > timestamp + assert not span.end_time + assert not span.attributes + assert not span.events + assert not span.parent_id + assert not span.duration + assert span.status + + assert not span.stack + assert span.synthetic is False + + +def test_readablespan_with_params( + span_context: SpanContext, +) -> None: + span_name = "test-span" + parent_id = "123456789" + start_time = time.time_ns() + end_time = time.time_ns() + attributes = {"key": "value"} + event_name = "event" + events = [Event(event_name, attributes, start_time)] + status = Status(StatusCode.OK) + stack = ["span-1", "span-2"] + span = ReadableSpan( + span_name, + span_context, + parent_id, + start_time, + end_time, + attributes, + events, + status, + stack, + ) + + assert span.name == span_name + assert span.parent_id == parent_id + assert span.start_time == start_time + assert span.end_time == end_time + assert span.attributes == attributes + assert span.events == events + assert span.status == status + assert span.duration == end_time - start_time + assert span.stack == stack diff --git a/tests/span/test_registered_span.py b/tests/span/test_registered_span.py new file mode 100644 index 00000000..f381b1f3 --- /dev/null +++ b/tests/span/test_registered_span.py @@ -0,0 +1,433 @@ +# (c) Copyright IBM Corp. 2024 + +import time +from typing import Any, Dict, Tuple + +import pytest +from opentelemetry.trace import SpanKind + +from instana.recorder import StanRecorder +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext + + +@pytest.mark.parametrize( + "span_name, expected_result, attributes", + [ + ("wsgi", ("wsgi", SpanKind.SERVER, "http"), {}), + ("rabbitmq", ("rabbitmq", SpanKind.SERVER, "rabbitmq"), {}), + ("gcps-producer", ("gcps", SpanKind.CLIENT, "gcps"), {}), + ("urllib3", ("urllib3", SpanKind.CLIENT, "http"), {}), + ("rabbitmq", ("rabbitmq", SpanKind.CLIENT, "rabbitmq"), {"sort": "publish"}), + ("render", ("render", SpanKind.INTERNAL, "render"), {"arguments": "--quiet"}), + ], +) +def test_registered_span( + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + expected_result: Tuple[str, int, str], + attributes: Dict[str, Any], +) -> None: + service_name = "test-registered-service" + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + reg_span = RegisteredSpan(span, None, service_name) + + assert expected_result[0] == reg_span.n + assert expected_result[1] == reg_span.k + assert service_name == reg_span.data["service"] + assert expected_result[2] in reg_span.data.keys() + + +def test_collect_http_attributes_with_attributes( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-registered-span" + attributes = { + "span.kind": "entry", + "http.host": "localhost", + "http.url": "https://www.instana.com", + "http.header.test": "one more test", + } + service_name = "test-registered-service" + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + reg_span = RegisteredSpan(span, None, service_name) + + excepted_result = { + "http.host": attributes["http.host"], + "http.url": attributes["http.url"], + "http.header.test": attributes["http.header.test"], + } + + reg_span._collect_http_attributes(span) + + assert excepted_result["http.host"] == reg_span.data["http"]["host"] + assert excepted_result["http.url"] == reg_span.data["http"]["url"] + assert ( + excepted_result["http.header.test"] == reg_span.data["http"]["header"]["test"] + ) + + +def test_populate_local_span_data_with_other_name( + span_context: SpanContext, caplog +) -> None: + # span_name = "test-registered-span" + # service_name = "test-registered-service" + # span = InstanaSpan(span_name, span_context) + # reg_span = RegisteredSpan(span, None, service_name) + + # expected_msg = f"SpanRecorder: Unknown local span: {span_name}" + + # reg_span._populate_local_span_data(span) + + # assert expected_msg == caplog.record_tuples[0][2] + pass + + +@pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "aws.lambda.entry", + "lambda", + { + "lambda.arn": "test", + "lambda.trigger": None, + }, + ), + ( + "celery-worker", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "gcps-consumer", + "gcps", + { + "gcps.op": "consume", + "gcps.projid": "MY_PROJECT", + "gcps.sub": "MY_SUBSCRIPTION_NAME", + }, + ), + ( + "rpc-server", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ], +) +def test_populate_entry_span_data( + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + service_name: str, + attributes: Dict[str, Any], +) -> None: + span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_entry_span_data(span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + +@pytest.mark.parametrize( + "attributes", + [ + { + "lambda.arn": "test", + "lambda.trigger": "aws:api.gateway", + "http.host": "localhost", + "http.url": "https://www.instana.com", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.events", + "lambda.cw.events.resources": "Resource 1", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.logs", + "lambda.cw.logs.group": "My Group", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:s3", + "lambda.s3.events": "Event 1", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:sqs", + "lambda.sqs.messages": "Message 1", + }, + ], +) +def test_populate_entry_span_data_AWSlambda( + span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] +) -> None: + span_name = "aws.lambda.entry" + service_name = "lambda" + expected_result = attributes.copy() + + span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(span, None, service_name) + + span.set_attributes(attributes) + reg_span._populate_entry_span_data(span) + + assert "python" == reg_span.data["lambda"]["runtime"] + assert "Unknown" == reg_span.data["lambda"]["functionName"] + assert "test" == reg_span.data["lambda"]["arn"] + assert expected_result["lambda.trigger"] == reg_span.data["lambda"]["trigger"] + + if expected_result["lambda.trigger"] == "aws:api.gateway": + assert expected_result["http.host"] == reg_span.data["http"]["host"] + assert expected_result["http.url"] == reg_span.data["http"]["url"] + + elif expected_result["lambda.trigger"] == "aws:cloudwatch.events": + assert ( + expected_result["lambda.cw.events.resources"] + == reg_span.data["lambda"]["cw"]["events"]["resources"] + ) + elif expected_result["lambda.trigger"] == "aws:cloudwatch.logs": + assert ( + expected_result["lambda.cw.logs.group"] + == reg_span.data["lambda"]["cw"]["logs"]["group"] + ) + elif expected_result["lambda.trigger"] == "aws:s3": + assert ( + expected_result["lambda.s3.events"] + == reg_span.data["lambda"]["s3"]["events"] + ) + elif expected_result["lambda.trigger"] == "aws:sqs": + assert ( + expected_result["lambda.sqs.messages"] + == reg_span.data["lambda"]["sqs"]["messages"] + ) + + +@pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "cassandra", + "cassandra", + { + "cassandra.cluster": "my_cluster", + "cassandra.error": "minor error", + }, + ), + ( + "celery-client", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "couchbase", + "couchbase", + { + "couchbase.hostname": "localhost", + "couchbase.error_type": 1234, + }, + ), + ( + "rabbitmq", + "rabbitmq", + { + "address": "localhost", + "key": 1234, + }, + ), + ( + "redis", + "redis", + { + "command": "ls -l", + "redis.error": "minor error", + }, + ), + ( + "rpc-client", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ( + "sqlalchemy", + "sqlalchemy", + { + "sqlalchemy.sql": "SELECT * FROM everything;", + "sqlalchemy.err": "Impossible select everything from everything!", + }, + ), + ( + "mysql", + "mysql", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "postgres", + "pg", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "mongo", + "mongo", + { + "command": "IDK", + "error": "minor error", + }, + ), + ( + "gcs", + "gcs", + { + "gcs.op": "produce", + "gcs.projectId": "MY_PROJECT", + "gcs.accessId": "Can not tell you!", + }, + ), + ( + "gcps-producer", + "gcps", + { + "gcps.op": "produce", + "gcps.projid": "MY_PROJECT", + "gcps.top": "MY_SUBSCRIPTION_NAME", + }, + ), + ], +) +def test_populate_exit_span_data( + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + service_name: str, + attributes: Dict[str, Any], +) -> None: + span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_exit_span_data(span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + +@pytest.mark.parametrize( + "attributes", + [ + { + "op": "test", + "http.host": "localhost", + "http.url": "https://www.instana.com", + }, + { + "payload": { + "blah": "bleh", + "blih": "bloh", + }, + "http.host": "localhost", + "http.url": "https://www.instana.com", + }, + ], +) +def test_populate_exit_span_data_boto3( + span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] +) -> None: + span_name = service_name = "boto3" + expected_result = attributes.copy() + + span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(span, None, service_name) + + # expected_result = {} + # for attr, value in attributes.items(): + # attrl = attr.split(".") + # attrl = attrl[1] if len(attrl) > 1 else attrl[0] + # expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_exit_span_data(span) + + assert expected_result.pop("http.host", None) == reg_span.data["http"]["host"] + assert expected_result.pop("http.url", None) == reg_span.data["http"]["url"] + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + +def test_populate_exit_span_data_log( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = service_name = "log" + sample_span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(sample_span, None, service_name) + + excepted_text = "Houston, we have a problem!" + sample_events = [ + ( + "test_populate_exit_span_data_log_event_with_message", + { + "field1": 1, + "field2": "two", + "message": excepted_text, + }, + time.time_ns(), + ), + ( + "test_populate_exit_span_data_log_event_with_parameters", + { + "field1": 1, + "field2": "two", + "parameters": excepted_text, + }, + time.time_ns(), + ), + ] + + for event_name, attributes, timestamp in sample_events: + sample_span.add_event(event_name, attributes, timestamp) + + reg_span._populate_exit_span_data(sample_span) + + assert excepted_text == reg_span.data["log"]["message"] + assert excepted_text == reg_span.data["log"]["parameters"] + + while sample_span._events: + sample_span._events.pop() diff --git a/tests/span/test_span.py b/tests/span/test_span.py new file mode 100644 index 00000000..2b3778dc --- /dev/null +++ b/tests/span/test_span.py @@ -0,0 +1,807 @@ +# (c) Copyright IBM Corp. 2024 + +import time +from unittest.mock import patch + +import pytest +from opentelemetry.trace.status import Status, StatusCode + +from instana.recorder import StanRecorder +from instana.span.span import INVALID_SPAN, Event, InstanaSpan, get_current_span +from instana.span_context import SpanContext + + +def test_span_default( + span_context: SpanContext, + span_processor: StanRecorder, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + timestamp = time.time_ns() + span = InstanaSpan(span_name, span_context, span_processor) + + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + context = span.context + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + + assert span.start_time + assert isinstance(span.start_time, int) + assert span.start_time > timestamp + assert not span.end_time + assert not span.attributes + assert not span.events + assert span.is_recording() + assert span.status + assert span.status.is_unset + + +def test_span_get_span_context( + span_context: SpanContext, + span_processor: StanRecorder, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + context = span.get_span_context() + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + assert context == span.context + + +def test_span_set_attributes_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + span.set_attributes(attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + +def test_span_set_attributes( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + span.set_attributes(attributes) + + assert len(span.attributes) == 4 + assert "field3" in span.attributes.keys() + assert "vier" in span.attributes.get("field4") + + +def test_span_set_attribute_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + for key, value in attributes.items(): + span.set_attribute(key, value) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + +def test_span_set_attribute( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + for key, value in attributes.items(): + span.set_attribute(key, value) + + assert len(span.attributes) == 4 + assert "field3" in span.attributes.keys() + assert "vier" in span.attributes.get("field4") + + +def test_span_update_name( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span-1" + span = InstanaSpan(span_name, span_context, span_processor) + + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + new_span_name = "test-span-2" + span.update_name(new_span_name) + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == new_span_name + + +def test_span_set_status_with_Status_default( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_Status_and_desc( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + set_status_desc = "Test" + span.set_status(span_status, set_status_desc) + excepted_log = f"Description {set_status_desc} ignored. Use either `Status` or `(StatusCode, Description)`" + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert excepted_log == caplog.record_tuples[1][2] + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusUNSET_to_StatusERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is UNSET." + span_status = Status(status_code=StatusCode.UNSET, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert not span.status.is_ok + assert span.status.description == status_desc + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code == StatusCode.ERROR + + +def test_span_set_status_with_StatusOK_to_StatusERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusCode_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + span_status_code = StatusCode(StatusCode.OK) + + span.set_status(span_status_code) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusCode_and_desc( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status_code = StatusCode(StatusCode.OK) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + +def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is UNSET." + span_status = Status(status_code=StatusCode.UNSET, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert not span.status.is_ok + assert span.status.description == status_desc + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code == StatusCode.ERROR + + +def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_add_event_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.events + + event_name = "event1" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp = time.time_ns() + span.add_event(event_name, attributes, timestamp) + + assert span.events + assert len(span.events) == 1 + for event in span.events: + assert isinstance(event, Event) + assert event.name == event_name + assert event.timestamp == timestamp + assert len(event.attributes) == 2 + + +def test_span_add_event( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + event_name1 = "event1" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp1 = time.time_ns() + event = Event(event_name1, attributes, timestamp1) + span = InstanaSpan(span_name, span_context, span_processor, events=[event]) + + assert span.events + assert len(span.events) == 1 + for event in span.events: + assert isinstance(event, Event) + assert event.name == event_name1 + assert event.timestamp == timestamp1 + assert len(event.attributes) == 2 + + event_name2 = "event2" + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + timestamp2 = time.time_ns() + span.add_event(event_name2, attributes, timestamp2) + + assert len(span.events) == 2 + for event in span.events: + assert isinstance(event, Event) + assert event.name in [event_name1, event_name2] + assert event.timestamp in [timestamp1, timestamp2] + assert len(event.attributes) == 2 + + +@pytest.mark.parametrize( + "span_name, span_attribute", + [ + ("test-span", None), + ("rpc-server", "rpc.error"), + ("rpc-client", "rpc.error"), + ("mysql", "mysql.error"), + ("postgres", "pg.error"), + ("django", "http.error"), + ("http", "http.error"), + ("urllib3", "http.error"), + ("wsgi", "http.error"), + ("asgi", "http.error"), + ("celery-client", "error"), + ("celery-worker", "error"), + ("sqlalchemy", "sqlalchemy.err"), + ("aws.lambda.entry", "lambda.error"), + ], +) +def test_span_record_exception_default( + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + span_attribute: str, +) -> None: + exception_msg = "Test Exception" + + exception = Exception(exception_msg) + span = InstanaSpan(span_name, span_context, span_processor) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + if span_attribute: + assert span_attribute in span.attributes.keys() + assert exception_msg == span.attributes.get(span_attribute, None) + else: + event = span.events[-1] # always get the latest event + assert isinstance(event, Event) + assert "exception" == event.name + assert exception_msg == event.attributes.get("message", None) + + +def test_span_record_exception_with_attribute( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + exception_msg = "Test Exception" + attributes = { + "custom_attr": 0, + } + + exception = Exception(exception_msg) + span = InstanaSpan(span_name, span_context, span_processor) + + span.record_exception(exception, attributes) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + + event = span.events[-1] # always get the latest event + assert isinstance(event, Event) + assert 2 == len(event.attributes) + assert exception_msg == event.attributes.get("message", None) + assert 0 == event.attributes.get("custom_attr", None) + + +def test_span_record_exception_with_Exception_msg( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "wsgi" + span_attribute = "http.error" + exception_msg = "Test Exception" + + exception = Exception() + exception.message = exception_msg + span = InstanaSpan(span_name, span_context, span_processor) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + assert span_attribute in span.attributes.keys() + assert exception_msg == span.attributes.get(span_attribute, None) + + +def test_span_record_exception_with_Exception_none_msg( + span_context: SpanContext, + span_processor: StanRecorder, +) -> None: + span_name = "wsgi" + span_attribute = "http.error" + + exception = Exception() + exception.message = None + span = InstanaSpan(span_name, span_context, span_processor) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + assert span_attribute in span.attributes.keys() + assert "Exception()" == span.attributes.get(span_attribute, None) + + +def test_span_record_exception_with_Exception_raised( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + + exception = None + span = InstanaSpan(span_name, span_context, span_processor) + + with patch( + "instana.span.span.InstanaSpan.add_event", side_effect=Exception("mocked error") + ): + with pytest.raises(Exception): + span.record_exception(exception) + + +def test_span_end_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + + span.end() + + assert span.end_time + assert isinstance(span.end_time, int) + + +def test_span_end(span_context: SpanContext, span_processor: StanRecorder) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + + timestamp_end = time.time_ns() + span.end(timestamp_end) + + assert span.end_time + assert span.end_time == timestamp_end + + +def test_span_mark_as_errored_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + span.mark_as_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_mark_as_errored( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + attributes = { + "field1": 1, + "field2": "two", + } + span.mark_as_errored(attributes) + + assert span.attributes + assert len(span.attributes) == 3 + assert span.attributes.get("ec") == 1 + assert "field1" in span.attributes.keys() + assert span.attributes.get("field2") == "two" + + span.mark_as_errored() + + assert span.attributes + assert len(span.attributes) == 3 + assert span.attributes.get("ec") == 2 + assert "field1" in span.attributes.keys() + assert span.attributes.get("field2") == "two" + + +def test_span_mark_as_errored_exception( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + with patch( + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), + ): + span.mark_as_errored() + assert not span.attributes + + +def test_span_assure_errored_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + span.assure_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_assure_errored( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + span.assure_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_assure_errored_exception( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + with patch( + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), + ): + span.assure_errored() + assert not span.attributes + + +def test_get_current_span(context) -> None: + span = get_current_span(context) + assert isinstance(span, InstanaSpan) + + +def test_get_current_span_INVALID_SPAN() -> None: + span = get_current_span() + + assert span + assert span == INVALID_SPAN + + +def test_span_duration_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + assert not span.duration + + span.end() + + assert span.end_time + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + + +def test_span_duration(span_context: SpanContext, span_processor: StanRecorder) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + assert not span.duration + + timestamp_end = time.time_ns() + span.end(timestamp_end) + + assert span.end_time + assert span.end_time == timestamp_end + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + assert span.duration == (timestamp_end - span.start_time) diff --git a/tests/span/test_span_sdk.py b/tests/span/test_span_sdk.py new file mode 100644 index 00000000..175fc60e --- /dev/null +++ b/tests/span/test_span_sdk.py @@ -0,0 +1,88 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Tuple + +import pytest + +from instana.recorder import StanRecorder +from instana.span.sdk_span import SDKSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext + + +def test_sdkspan(span_context: SpanContext, span_processor: StanRecorder) -> None: + span_name = "test-sdk-span" + service_name = "test-sdk" + attributes = { + "span.kind": "entry", + "arguments": "--quiet", + "return": "True", + } + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) + sdk_span = SDKSpan(span, None, service_name) + + expected_result = { + "n": "sdk", + "k": 1, + "data": { + "service": service_name, + "sdk": { + "name": span_name, + "type": attributes["span.kind"], + "custom": { + "attributes": attributes, + }, + "arguments": attributes["arguments"], + "return": attributes["return"], + }, + }, + } + + assert expected_result["n"] == sdk_span.n + assert expected_result["k"] == sdk_span.k + assert len(expected_result["data"]) == len(sdk_span.data) + assert expected_result["data"]["service"] == sdk_span.data["service"] + assert len(expected_result["data"]["sdk"]) == len(sdk_span.data["sdk"]) + assert expected_result["data"]["sdk"]["name"] == sdk_span.data["sdk"]["name"] + assert expected_result["data"]["sdk"]["type"] == sdk_span.data["sdk"]["type"] + assert len(attributes) == len(sdk_span.data["sdk"]["custom"]["attributes"]) + assert attributes == sdk_span.data["sdk"]["custom"]["attributes"] + assert attributes["arguments"] == sdk_span.data["sdk"]["arguments"] + assert attributes["return"] == sdk_span.data["sdk"]["return"] + + +@pytest.mark.parametrize( + "span_kind, expected_result", + [ + (None, ("intermediate", 3)), + ("entry", ("entry", 1)), + ("server", ("entry", 1)), + ("consumer", ("entry", 1)), + ("exit", ("exit", 2)), + ("client", ("exit", 2)), + ("producer", ("exit", 2)), + ], +) +def test_sdkspan_get_span_kind( + span_context: SpanContext, + span_processor: StanRecorder, + span_kind: str, + expected_result: Tuple[str, int], +) -> None: + attributes = { + "span.kind": span_kind, + } + span = InstanaSpan( + "test-sdk-span", span_context, span_processor, attributes=attributes + ) + sdk_span = SDKSpan(span, None, "test") + + kind = sdk_span.get_span_kind(span) + + assert expected_result == kind + + +def test_sdkspan_get_span_kind_with_no_attributes(span: InstanaSpan) -> None: + sdk_span = SDKSpan(span, None, "test") + kind = sdk_span.get_span_kind(span) + assert ("intermediate", 3) == kind diff --git a/tests/test_id_management.py b/tests/test_id_management.py deleted file mode 100644 index fb3badbb..00000000 --- a/tests/test_id_management.py +++ /dev/null @@ -1,56 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2017 - -import unittest -import instana - - -class TestIdManagement(unittest.TestCase): - def test_id_generation(self): - count = 0 - while count <= 10000: - id = instana.util.ids.generate_id() - base10_id = int(id, 16) - self.assertGreaterEqual(base10_id, 0) - self.assertLessEqual(base10_id, 18446744073709551615) - count += 1 - - - def test_various_header_to_id_conversion(self): - # Get a hex string to test against & convert - header_id = instana.util.ids.generate_id() - converted_id = instana.util.ids.header_to_long_id(header_id) - self.assertEqual(header_id, converted_id) - - # Hex value - result should be left padded - result = instana.util.ids.header_to_long_id('abcdef') - self.assertEqual('0000000000abcdef', result) - - # Hex value - result = instana.util.ids.header_to_long_id('0123456789abcdef') - self.assertEqual('0123456789abcdef', result) - - # Very long incoming header should just return the rightmost 16 bytes - result = instana.util.ids.header_to_long_id('0x0123456789abcdef0123456789abcdef') - self.assertEqual('0x0123456789abcdef0123456789abcdef', result) - - - def test_header_to_id_conversion_with_bogus_header(self): - # Bogus nil arg - bogus_result = instana.util.ids.header_to_long_id(None) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - # Bogus Integer arg - bogus_result = instana.util.ids.header_to_long_id(1234) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - # Bogus Array arg - bogus_result = instana.util.ids.header_to_long_id([1234]) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - # Bogus Hex Values in String - bogus_result = instana.util.ids.header_to_long_id('0xZZZZZZ') - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - bogus_result = instana.util.ids.header_to_long_id('ZZZZZZ') - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) diff --git a/tests/test_sampling.py b/tests/test_sampling.py new file mode 100644 index 00000000..a3aa3136 --- /dev/null +++ b/tests/test_sampling.py @@ -0,0 +1,20 @@ +import pytest + +from typing import Generator +from instana.sampling import InstanaSampler, SamplingPolicy + + +class TestInstanaSampler: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.sampler = InstanaSampler() + yield + self.sampler = None + + def test_sampling_policy(self) -> None: + assert self.sampler._sampled == SamplingPolicy.DROP + assert self.sampler._sampled.name == "DROP" + assert self.sampler._sampled.value == 0 + + def test_sampler(self) -> None: + assert not self.sampler.sampled() diff --git a/tests/test_tracer.py b/tests/test_tracer.py new file mode 100644 index 00000000..06474447 --- /dev/null +++ b/tests/test_tracer.py @@ -0,0 +1,206 @@ +# (c) Copyright IBM Corp. 2024 + +import pytest +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE + +from instana.agent.host import HostAgent +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.span.span import ( + INVALID_SPAN, + INVALID_SPAN_ID, + InstanaSpan, + get_current_span, +) +from instana.span_context import SpanContext +from instana.tracer import InstanaTracer, InstanaTracerProvider + + +def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + assert isinstance(tracer._sampler, InstanaSampler) + assert isinstance(tracer.span_processor, StanRecorder) + assert isinstance(tracer.exporter, HostAgent) + assert len(tracer._propagators) == 3 + + +def test_tracer_start_span( + tracer_provider: InstanaTracerProvider, span_context: SpanContext +) -> None: + span_name = "test-span" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + span = tracer.start_span(name=span_name, span_context=span_context) + + assert span + assert isinstance(span, InstanaSpan) + assert span.name == span_name + assert not span.stack + + +def test_tracer_start_span_with_stack(tracer_provider: InstanaTracerProvider) -> None: + span_name = "log" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + span = tracer.start_span(name=span_name) + + assert span + assert isinstance(span, InstanaSpan) + assert span.name == span_name + assert span.stack + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() + + +def test_tracer_start_span_Exception( + mocker, tracer_provider: InstanaTracerProvider, span_context: SpanContext +) -> None: + span_name = "test-span" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + + mocker.patch( + "instana.tracer.InstanaTracer._create_span_context", + return_value={"key": "value"}, + ) + with pytest.raises(AttributeError): + tracer.start_span(name=span_name, span_context=span_context) + + +def test_tracer_start_as_current_span(tracer_provider: InstanaTracerProvider) -> None: + span_name = "test-span" + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + with tracer.start_as_current_span(name=span_name) as span: + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + +def test_tracer_nested_span(tracer_provider: InstanaTracerProvider) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + parent_span_name = "parent-span" + child_span_name = "child-span" + with tracer.start_as_current_span(name=parent_span_name) as pspan: + assert get_current_span() is pspan + with tracer.start_as_current_span(name=child_span_name) as cspan: + assert get_current_span() is cspan + assert cspan.parent_id == pspan.context.span_id + # child span goes out of scope + assert cspan.end_time is not None + assert get_current_span() is pspan + # parent span goes out of scope + assert pspan.end_time is not None + assert get_current_span() is INVALID_SPAN + + +def test_tracer_create_span_context( + span_context: SpanContext, tracer_provider: InstanaTracerProvider +) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + new_span_context = tracer._create_span_context(span_context) + + assert span_context.trace_id == new_span_context.trace_id + assert span_context.span_id != new_span_context.span_id + assert span_context.long_trace_id == new_span_context.long_trace_id + + assert span_context.trace_id > INVALID_SPAN_ID + assert span_context.trace_id <= _SPAN_ID_MAX_VALUE + + assert span_context.span_id > INVALID_SPAN_ID + assert span_context.span_id <= _SPAN_ID_MAX_VALUE + + +def test_tracer_create_span_context_root( + tracer_provider: InstanaTracerProvider, +) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + new_span_context = tracer._create_span_context(parent_context=None) + + assert new_span_context.trace_id > INVALID_SPAN_ID + assert new_span_context.trace_id <= _SPAN_ID_MAX_VALUE + + assert new_span_context.trace_id == new_span_context.span_id + + +def test_tracer_add_stack_high_limit( + span: InstanaSpan, tracer_provider: InstanaTracerProvider +) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + tracer._add_stack(span, 50) + + assert span.stack + assert 40 >= len(span.stack) + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() + + +def test_tracer_add_stack_low_limit( + span: InstanaSpan, tracer_provider: InstanaTracerProvider +) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + tracer._add_stack(span, 5) + + assert span.stack + assert 5 >= len(span.stack) + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py new file mode 100644 index 00000000..5a1ffd5b --- /dev/null +++ b/tests/test_tracer_provider.py @@ -0,0 +1,53 @@ +# (c) Copyright IBM Corp. 2024 + +from pytest import LogCaptureFixture + +from instana.agent.base import BaseAgent +from instana.agent.host import HostAgent +from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.format import Format +from instana.propagators.http_propagator import HTTPPropagator +from instana.propagators.text_propagator import TextPropagator +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.tracer import InstanaTracer, InstanaTracerProvider + + +def test_tracer_provider_defaults() -> None: + provider = InstanaTracerProvider() + assert isinstance(provider.sampler, InstanaSampler) + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._exporter, HostAgent) + assert len(provider._propagators) == 3 + assert isinstance(provider._propagators[Format.HTTP_HEADERS], HTTPPropagator) + assert isinstance(provider._propagators[Format.TEXT_MAP], TextPropagator) + assert isinstance(provider._propagators[Format.BINARY], BinaryPropagator) + + +def test_tracer_provider_get_tracer() -> None: + provider = InstanaTracerProvider() + tracer = provider.get_tracer("instana.test.tracer") + + assert isinstance(tracer, InstanaTracer) + + +def test_tracer_provider_get_tracer_empty_instrumenting_module_name( + caplog: LogCaptureFixture, +) -> None: + provider = InstanaTracerProvider() + tracer = provider.get_tracer("") + + assert "get_tracer called with missing module name." in caplog.messages + assert isinstance(tracer, InstanaTracer) + + +def test_tracer_provider_add_span_processor(span_processor: StanRecorder) -> None: + provider = InstanaTracerProvider() + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._span_processor.agent, HostAgent) + assert provider._span_processor.THREAD_NAME == "InstanaSpan Recorder" + + provider.add_span_processor(span_processor) + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._span_processor.agent, BaseAgent) + assert provider._span_processor.THREAD_NAME == "InstanaSpan Recorder Test" diff --git a/tests/test_utils.py b/tests/test_utils.py index 57fc5cf6..2be9a228 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,10 +3,10 @@ class _TraceContextMixin: def assertTraceContextPropagated(self, parent_span, child_span): - self.assertEqual(parent_span.t, child_span.t) - self.assertEqual(parent_span.s, child_span.p) - self.assertNotEqual(parent_span.s, child_span.s) + assert parent_span.t == child_span.t + assert parent_span.s == child_span.p + assert parent_span.s != child_span.s def assertErrorLogging(self, spans): for span in spans: - self.assertIsNone(span.ec) + assert not span.ec diff --git a/tests/util/test_id_management.py b/tests/util/test_id_management.py new file mode 100644 index 00000000..c10d2b51 --- /dev/null +++ b/tests/util/test_id_management.py @@ -0,0 +1,63 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2017 + +import pytest +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID + +import instana + + +def test_id_generation(): + count = 0 + while count <= 10000: + id = instana.util.ids.generate_id() + assert id >= 0 + assert id > INVALID_SPAN_ID + assert id <= _SPAN_ID_MAX_VALUE + count += 1 + + +@pytest.mark.parametrize( + "str_id, id", + [ + ("BADCAFFE", 3135025150), + ("abcdef", 11259375), + ("0123456789abcdef", 81985529216486895), + ("0x0123456789abcdef0123456789abcdef", 1512366075204170929049582354406559215), + (None, INVALID_SPAN_ID), + (1234, INVALID_SPAN_ID), + ([1234], INVALID_SPAN_ID), + ("0xZZZZZZ", INVALID_SPAN_ID), + ("ZZZZZZ", INVALID_SPAN_ID), + (b"BADCAFFE", 3135025150), + (b"abcdef", 11259375), + (b"0123456789abcdef", 81985529216486895), + (b"0x0123456789abcdef0123456789abcdef", 1512366075204170929049582354406559215), + ], +) +def test_header_to_long_id(str_id, id): + result = instana.util.ids.header_to_long_id(str_id) + assert result == id + + +@pytest.mark.parametrize( + "str_id, id", + [ + ("BADCAFFE", 3135025150), + ("abcdef", 11259375), + ("0123456789abcdef", 81985529216486895), + ("0x0123456789abcdef0123456789abcdef", 81985529216486895), + (None, INVALID_SPAN_ID), + (1234, INVALID_SPAN_ID), + ([1234], INVALID_SPAN_ID), + ("0xZZZZZZ", INVALID_SPAN_ID), + ("ZZZZZZ", INVALID_SPAN_ID), + (b"BADCAFFE", 3135025150), + (b"abcdef", 11259375), + (b"0123456789abcdef", 81985529216486895), + (b"0x0123456789abcdef0123456789abcdef", 81985529216486895), + ], +) +def test_header_to_id(str_id, id): + result = instana.util.ids.header_to_id(str_id) + assert result == id diff --git a/tests/test_secrets.py b/tests/util/test_secrets.py similarity index 100% rename from tests/test_secrets.py rename to tests/util/test_secrets.py diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py new file mode 100644 index 00000000..f1817029 --- /dev/null +++ b/tests/util/test_traceutils.py @@ -0,0 +1,65 @@ +from unittest.mock import patch +import pytest + +from instana.tracer import InstanaTracer +from instana.util.traceutils import ( + extract_custom_headers, + get_active_tracer, + get_tracer_tuple, + tracing_is_off, +) +from instana.singletons import agent, tracer + + +class TestTraceUtils: + @pytest.fixture(autouse=True) + def _resource(self): + pass + + def test_extract_custom_headers(self, span) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + extract_custom_headers(span, request_headers) + assert len(span.attributes) == 2 + assert span.attributes["http.header.X-Capture-This-Too"] == "this too" + assert span.attributes["http.header.X-Capture-That-Too"] == "that too" + + def test_get_activate_tracer(self) -> None: + assert not get_active_tracer() + + with tracer.start_as_current_span("test"): + response = get_active_tracer() + assert isinstance(response, InstanaTracer) + assert response == tracer + with patch( + "instana.span.span.InstanaSpan.is_recording", return_value=False + ): + assert not get_active_tracer() + + def test_get_tracer_tuple(self) -> None: + response = get_tracer_tuple() + assert response == (None, None, None) + + agent.options.allow_exit_as_root = True + response = get_tracer_tuple() + assert response == (tracer, None, None) + agent.options.allow_exit_as_root = False + + with tracer.start_as_current_span("test") as span: + response = get_tracer_tuple() + assert response == (tracer, span, span.name) + + def test_tracing_is_off(self) -> None: + response = tracing_is_off() + assert response + with tracer.start_as_current_span("test"): + response = tracing_is_off() + assert not response + + agent.options.allow_exit_as_root = True + response = tracing_is_off() + assert not response + agent.options.allow_exit_as_root = False diff --git a/tests/test_util.py b/tests/util/test_util.py similarity index 100% rename from tests/test_util.py rename to tests/util/test_util.py