diff --git a/.circleci/config.yml b/.circleci/config.yml index a3027569..d93a36a8 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 @@ -269,10 +269,23 @@ jobs: - store-pytest-results - store-coverage-report + py312aws: + docker: + - image: cimg/python:3.12 + working_directory: ~/repo + steps: + - checkout + - pip-install-deps: + requirements: "tests/requirements-312.txt" + - run-tests-with-coverage-report: + tests: "tests_aws" + - store-pytest-results + - store-coverage-report + 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 +309,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 +325,7 @@ jobs: py39cassandra: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: cassandra:3.11 environment: MAX_HEAP_SIZE: 2048m @@ -365,9 +378,10 @@ workflows: - python313 - py39cassandra - py39couchbase - - py39gevent_starlette + # - py39gevent_starlette - py311googlecloud - py312googlecloud + - py312aws - final_job: requires: - python38 @@ -378,6 +392,7 @@ workflows: - python313 - py39cassandra - py39couchbase - - py39gevent_starlette + # - py39gevent_starlette - py311googlecloud - py312googlecloud + - py312aws 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/pipeline.yaml b/.tekton/pipeline.yaml index 9a7678ff..1f99421e 100644 --- a/.tekton/pipeline.yaml +++ b/.tekton/pipeline.yaml @@ -100,3 +100,17 @@ spec: workspaces: - name: task-pvc workspace: python-tracer-ci-pipeline-pvc + - name: unittest-aws + runAfter: + - clone + matrix: + params: + - name: imageDigest + value: + # 3.12.6-bookworm + - "sha256:af6fa5c329d6bd6dec52855ccb8bb37c30fb8f00819953a035d49499e43b2c9b" + taskRef: + name: python-tracer-unittest-googlecloud-task + workspaces: + - name: task-pvc + workspace: python-tracer-ci-pipeline-pvc diff --git a/.tekton/run_unittests.sh b/.tekton/run_unittests.sh index dfcc79eb..7229fcf3 100755 --- a/.tekton/run_unittests.sh +++ b/.tekton/run_unittests.sh @@ -44,6 +44,9 @@ googlecloud) export REQUIREMENTS='requirements-googlecloud.txt' export TESTS=('tests/clients/test_google-cloud-storage.py' 'tests/clients/test_google-cloud-pubsub.py') export GOOGLE_CLOUD_TEST='true' ;; +aws) + export REQUIREMENTS='requirements-312.txt' + export TESTS=('tests_aws') *) echo "ERROR \$TEST_CONFIGURATION='${TEST_CONFIGURATION}' is unsupported " \ "not in (default|cassandra|couchbase|gevent_starlette|googlecloud)" >&2 @@ -52,7 +55,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/.tekton/task.yaml b/.tekton/task.yaml index 96796bb8..253296ba 100644 --- a/.tekton/task.yaml +++ b/.tekton/task.yaml @@ -207,3 +207,24 @@ spec: workingDir: /workspace/python-sensor/ command: - /workspace/python-sensor/.tekton/run_unittests.sh +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: python-tracer-unittest-aws-task +spec: + params: + - name: imageDigest + type: string + workspaces: + - name: task-pvc + mountPath: /workspace + steps: + - name: unittest + image: python@$(params.imageDigest) + env: + - name: TEST_CONFIGURATION + value: aws + workingDir: /workspace/python-sensor/ + command: + - /workspace/python-sensor/.tekton/run_unittests.sh diff --git a/README.md b/README.md index 32adfebd..b8ccdeef 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,16 @@ The `instana` Python package collects key metrics and distributed traces for [Instana]. -This package supports Python 3.8 or greater. - Any feedback is welcome. Happy Python visibility. [![CircleCI](https://circleci.com/gh/instana/python-sensor/tree/master.svg?style=svg)](https://circleci.com/gh/instana/python-sensor/tree/master) -[![OpenTracing Badge](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) +[![OpenTracing Badge](https://img.shields.io/badge/OpenTracing-disabled-red.svg)](http://opentracing.io) +[![OpenTelemetry Badge](https://img.shields.io/badge/OpenTelemetry-enabled-blue.svg)](http://opentelemetry.io) +![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Finstana%2Fpython-sensor%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml) +![GitHub Release](https://img.shields.io/github/v/release/instana/python-sensor) + +> [!NOTE] +> Support for OpenTracing is deprecated starting on version 3.0.0. If you still want to use it, rely on any version up to 2.5.3 or use the `legacy_2.x` branch. ## Installation @@ -53,7 +57,7 @@ Want to instrument other languages? See our [Node.js], [Go], [Ruby] instrumenta [Instana]: https://www.instana.com/ "IBM Instana Observability" -[Instana AutoTrace™️]: https://www.instana.com/supported-technologies/instana-autotrace/ "Instana AutoTrace" +[Instana AutoTrace™️]: https://www.ibm.com/docs/en/instana-observability/current?topic=kubernetes-instana-autotrace-webhook "Instana AutoTrace" [configuration page]: https://www.ibm.com/docs/en/instana-observability/current?topic=package-python-configuration-configuring-instana#general "Instana Python package configuration" [PyPI]: https://pypi.python.org/pypi/instana "Instana package at PyPI" [installation document]: https://www.ibm.com/docs/en/instana-observability/current?topic=technologies-monitoring-python-instana-python-package#installing "Instana Python package installation" diff --git a/pyproject.toml b/pyproject.toml index 2703ace0..11c63a87 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,14 @@ 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.27.0", + "opentelemetry-semantic-conventions>=0.48b0", + "typing_extensions>=4.12.2", ] [project.entry-points."instana"] @@ -59,6 +60,8 @@ string = "instana:load" [project.optional-dependencies] dev = [ "pytest", + "pytest-cov", + "pytest-mock", ] [project.urls] @@ -77,3 +80,12 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/instana"] + +[tool.coverage.report] +exclude_also = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "except ImportError:", + "except Exception:", + "except Exception as exc:", + ] diff --git a/pytest.ini b/pytest.ini index 52835b1d..30d6f4d7 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,7 @@ 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 +testpaths = + tests + tests_aws diff --git a/src/instana/__init__.py b/src/instana/__init__.py index e6ae417b..b9bc30ec 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -1,38 +1,37 @@ # 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 typing import Tuple + +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,49 +41,75 @@ # 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"] - - -def load(_): +do_not_load_list = [ + "pip", + "pip2", + "pip3", + "pipenv", + "docker-compose", + "easy_install", + "easy_install-2.7", + "smtpd.py", + "twine", + "ufw", + "unattended-upgrade", +] + + +def load(_: object) -> None: """ Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP environment variable. """ # Work around https://bugs.python.org/issue32573 if not hasattr(sys, "argv"): - sys.argv = [''] + sys.argv = [""] return None -def apply_gevent_monkey_patch(): + +def apply_gevent_monkey_patch() -> None: 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 - - def key_to_bool(k): - return not k.startswith('no-') + + def short_key(k: str) -> str: + return k[3:] if k.startswith("no-") else k + + def key_to_bool(k: str) -> bool: + 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() -def get_lambda_handler_or_default(): +def get_aws_lambda_handler() -> Tuple[str, str]: """ - For instrumenting AWS Lambda, users specify their original lambda handler in the LAMBDA_HANDLER environment - variable. This function searches for and parses that environment variable or returns the defaults. - - The default handler value for AWS Lambda is 'lambda_function.lambda_handler' which - equates to the function "lambda_handler in a file named "lambda_function.py" or in Python - terms "from lambda_function import lambda_handler" + For instrumenting AWS Lambda, users specify their original lambda handler + in the LAMBDA_HANDLER environment variable. This function searches for and + parses that environment variable or returns the defaults. + + The default handler value for AWS Lambda is 'lambda_function.lambda_handler' + which equates to the function "lambda_handler in a file named + lambda_function.py" or in Python terms + "from lambda_function import lambda_handler" """ handler_module = "lambda_function" handler_function = "lambda_handler" @@ -96,104 +121,117 @@ def get_lambda_handler_or_default(): parts = handler.split(".") handler_function = parts.pop().strip() handler_module = ".".join(parts).strip() - except Exception: - pass + except Exception as exc: + print(f"get_aws_lambda_handler error: {exc}") return handler_module, handler_function -def lambda_handler(event, context): +def lambda_handler(event: str, context: str) -> None: """ Entry point for AWS Lambda monitoring. This function will trigger the initialization of Instana monitoring and then call the original user specified lambda handler function. """ - module_name, function_name = get_lambda_handler_or_default() + module_name, function_name = get_aws_lambda_handler() try: # 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(): +def boot_agent() -> None: """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 import celery # 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: - from .singletons import get_profiler + from instana.singletons import get_profiler profiler = get_profiler() if profiler: diff --git a/src/instana/agent/aws_lambda.py b/src/instana/agent/aws_lambda.py index 66145e15..140275ab 100644 --- a/src/instana/agent/aws_lambda.py +++ b/src/instana/agent/aws_lambda.py @@ -2,21 +2,23 @@ # (c) Copyright Instana Inc. 2020 """ -The Instana agent (for AWS Lambda functions) that manages +The Instana Agent for AWS Lambda functions that manages monitoring state and reporting that data. """ -import time -from ..log import logger -from ..util import to_json -from .base import BaseAgent -from ..version import VERSION -from ..collector.aws_lambda import AWSLambdaCollector -from ..options import AWSLambdaOptions + +from typing import Any, Dict +from instana.agent.base import BaseAgent +from instana.collector.aws_lambda import AWSLambdaCollector +from instana.log import logger +from instana.options import AWSLambdaOptions +from instana.util import to_json +from instana.version import VERSION class AWSLambdaAgent(BaseAgent): - """ In-process agent for AWS Lambda """ - def __init__(self): + """In-process Agent for AWS Lambda""" + + def __init__(self) -> None: super(AWSLambdaAgent, self).__init__() self.collector = None @@ -27,29 +29,33 @@ def __init__(self): # Update log level from what Options detected self.update_log_level() - logger.info("Stan is on the AWS Lambda scene. Starting Instana instrumentation version: %s", VERSION) + logger.info( + f"Stan is on the AWS Lambda scene. Starting Instana instrumentation version: {VERSION}", + ) if self._validate_options(): self._can_send = True self.collector = AWSLambdaCollector(self) self.collector.start() else: - logger.warning("Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. " - "We will not be able monitor this function.") + logger.warning( + "Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. " + "We will not be able monitor this function." + ) - def can_send(self): + def can_send(self) -> bool: """ Are we in a state where we can send data? @return: Boolean """ return self._can_send - def get_from_structure(self): + def get_from_structure(self) -> Dict[str, Any]: """ Retrieves the From data that is reported alongside monitoring data. @return: dict() """ - return {'hl': True, 'cp': 'aws', 'e': self.collector.get_fq_arn()} + return {"hl": True, "cp": "aws", "e": self.collector.get_fq_arn()} def report_data_payload(self, payload): """ @@ -64,30 +70,38 @@ def report_data_payload(self, payload): self.report_headers["X-Instana-Host"] = self.collector.get_fq_arn() self.report_headers["X-Instana-Key"] = self.options.agent_key - response = self.client.post(self.__data_bundle_url(), - data=to_json(payload), - headers=self.report_headers, - timeout=self.options.timeout, - verify=self.options.ssl_verify, - proxies=self.options.endpoint_proxy) + response = self.client.post( + self.__data_bundle_url(), + data=to_json(payload), + headers=self.report_headers, + timeout=self.options.timeout, + verify=self.options.ssl_verify, + proxies=self.options.endpoint_proxy, + ) if 200 <= response.status_code < 300: - logger.debug("report_data_payload: Instana responded with status code %s", response.status_code) + logger.debug( + "report_data_payload: Instana responded with status code %s", + response.status_code, + ) else: - logger.info("report_data_payload: Instana responded with status code %s", response.status_code) + logger.info( + "report_data_payload: Instana responded with status code %s", + response.status_code, + ) except Exception as exc: logger.debug("report_data_payload: connection error (%s)", type(exc)) return response - def _validate_options(self): + def _validate_options(self) -> bool: """ Validate that the options used by this Agent are valid. e.g. can we report data? """ - return self.options.endpoint_url is not None and self.options.agent_key is not None + return self.options.endpoint_url and self.options.agent_key - def __data_bundle_url(self): + def __data_bundle_url(self) -> str: """ URL for posting metrics to the host agent. Only valid when announced. """ - return "%s/bundle" % self.options.endpoint_url + return f"{self.options.endpoint_url}/bundle" 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/autoprofile/frame_cache.py b/src/instana/autoprofile/frame_cache.py index f59a7f6d..166c2de6 100644 --- a/src/instana/autoprofile/frame_cache.py +++ b/src/instana/autoprofile/frame_cache.py @@ -2,33 +2,32 @@ # (c) Copyright Instana Inc. 2020 -import threading import os -import re -import importlib +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from instana.autoprofile.profile import Profile -from .runtime import runtime_info class FrameCache(object): MAX_CACHE_SIZE = 2500 - def __init__(self, profiler): + def __init__(self, profiler: "Profile") -> None: self.profiler = profiler self.profiler_frame_cache = None - self.include_profiler_frames = None - self.profiler_dir = os.path.dirname(os.path.realpath(__file__)) - def start(self): + def start(self) -> None: self.profiler_frame_cache = dict() + self.include_profiler_frames = self.profiler.get_option( + "include_profiler_frames", False + ) - self.include_profiler_frames = self.profiler.get_option('include_profiler_frames', False) - - def stop(self): + def stop(self) -> None: pass - def is_profiler_frame(self, filename): + def is_profiler_frame(self, filename: str) -> bool: if filename in self.profiler_frame_cache: return self.profiler_frame_cache[filename] diff --git a/src/instana/autoprofile/profile.py b/src/instana/autoprofile/profile.py index 52ee393b..38d31eb4 100644 --- a/src/instana/autoprofile/profile.py +++ b/src/instana/autoprofile/profile.py @@ -3,28 +3,37 @@ import math import os -import uuid import time +import uuid +from typing import Any, Dict, Optional class Profile(object): - CATEGORY_CPU = 'cpu' - CATEGORY_MEMORY = 'memory' - CATEGORY_TIME = 'time' - TYPE_CPU_USAGE = 'cpu-usage' - TYPE_MEMORY_ALLOCATION_RATE = 'memory-allocation-rate' - TYPE_BLOCKING_CALLS = 'blocking-calls' - UNIT_NONE = '' - UNIT_MILLISECOND = 'millisecond' - UNIT_MICROSECOND = 'microsecond' - UNIT_NANOSECOND = 'nanosecond' - UNIT_BYTE = 'byte' - UNIT_KILOBYTE = 'kilobyte' - UNIT_PERCENT = 'percent' - UNIT_SAMPLE = 'sample' - RUNTIME_PYTHON = 'python' - - def __init__(self, category, typ, unit, roots, duration, timespan): + CATEGORY_CPU = "cpu" + CATEGORY_MEMORY = "memory" + CATEGORY_TIME = "time" + TYPE_CPU_USAGE = "cpu-usage" + TYPE_MEMORY_ALLOCATION_RATE = "memory-allocation-rate" + TYPE_BLOCKING_CALLS = "blocking-calls" + UNIT_NONE = "" + UNIT_MILLISECOND = "millisecond" + UNIT_MICROSECOND = "microsecond" + UNIT_NANOSECOND = "nanosecond" + UNIT_BYTE = "byte" + UNIT_KILOBYTE = "kilobyte" + UNIT_PERCENT = "percent" + UNIT_SAMPLE = "sample" + RUNTIME_PYTHON = "python" + + def __init__( + self, + category: str, + typ: str, + unit: str, + roots: object, + duration: int, + timespan: int, + ) -> None: self.process_id = str(os.getpid()) self.id = generate_uuid() self.runtime = Profile.RUNTIME_PYTHON @@ -36,18 +45,18 @@ def __init__(self, category, typ, unit, roots, duration, timespan): self.timespan = timespan self.timestamp = millis() - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: profile_dict = { - 'pid': self.process_id, - 'id': self.id, - 'runtime': self.runtime, - 'category': self.category, - 'type': self.type, - 'unit': self.unit, - 'roots': [root.to_dict() for root in self.roots], - 'duration': self.duration, - 'timespan': self.timespan, - 'timestamp': self.timestamp + "pid": self.process_id, + "id": self.id, + "runtime": self.runtime, + "category": self.category, + "type": self.type, + "unit": self.unit, + "roots": [root.to_dict() for root in self.roots], + "duration": self.duration, + "timespan": self.timespan, + "timestamp": self.timestamp, } return profile_dict @@ -55,83 +64,97 @@ def to_dict(self): class CallSite: __slots__ = [ - 'method_name', - 'file_name', - 'file_line', - 'measurement', - 'num_samples', - 'children' + "method_name", + "file_name", + "file_line", + "measurement", + "num_samples", + "children", ] - def __init__(self, method_name, file_name, file_line): + def __init__(self, method_name: str, file_name: str, file_line: int) -> None: self.method_name = method_name self.file_name = file_name self.file_line = file_line - self.measurement = 0 - self.num_samples = 0 + self.measurement: int = 0 + self.num_samples: int = 0 self.children = dict() - def create_key(self, method_name, file_name, file_line): - return '{0} ({1}:{2})'.format(method_name, file_name, file_line) + def create_key(self, method_name: str, file_name: str, file_line: int) -> str: + return f"{method_name} ({file_name}:{file_line})" - def find_child(self, method_name, file_name, file_line): + def find_child( + self, method_name: str, file_name: str, file_line: int + ) -> Optional[object]: key = self.create_key(method_name, file_name, file_line) if key in self.children: return self.children[key] return None - def add_child(self, child): - self.children[self.create_key(child.method_name, child.file_name, child.file_line)] = child + def add_child(self, child: object) -> None: + self.children[ + self.create_key(child.method_name, child.file_name, child.file_line) + ] = child - def remove_child(self, child): - del self.children[self.create_key(child.method_name, child.file_name, child.file_line)] + def remove_child(self, child: object) -> None: + del self.children[ + self.create_key(child.method_name, child.file_name, child.file_line) + ] - def find_or_add_child(self, method_name, file_name, file_line): + def find_or_add_child( + self, method_name: str, file_name: str, file_line: int + ) -> object: child = self.find_child(method_name, file_name, file_line) - if child == None: + if not child: child = CallSite(method_name, file_name, file_line) self.add_child(child) return child - def increment(self, value, count): + def increment(self, value: int, count: int) -> None: self.measurement += value self.num_samples += count - def normalize(self, factor): + def normalize(self, factor: int) -> None: self.measurement = self.measurement / factor self.num_samples = int(math.ceil(self.num_samples / factor)) for child in self.children.values(): child.normalize(factor) - def floor(self): + def floor(self) -> None: self.measurement = int(self.measurement) for child in self.children.values(): child.floor() - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: children_dicts = [] for child in self.children.values(): children_dicts.append(child.to_dict()) call_site_dict = { - 'method_name': self.method_name, - 'file_name': self.file_name, - 'file_line': self.file_line, - 'measurement': self.measurement, - 'num_samples': self.num_samples, - 'children': children_dicts + "method_name": self.method_name, + "file_name": self.file_name, + "file_line": self.file_line, + "measurement": self.measurement, + "num_samples": self.num_samples, + "children": children_dicts, } return call_site_dict -def millis(): +def millis() -> int: + """ + Returns the current time in milliseconds since the Unix epoch (January 1, 1970). + """ return int(round(time.time() * 1000)) -def generate_uuid(): +def generate_uuid() -> str: + """ + Generates a UUID as string. + """ return str(uuid.uuid4()) diff --git a/src/instana/autoprofile/profiler.py b/src/instana/autoprofile/profiler.py index 4cdca87e..2e685a0e 100644 --- a/src/instana/autoprofile/profiler.py +++ b/src/instana/autoprofile/profiler.py @@ -1,106 +1,86 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import threading import os -import signal -import atexit import platform +import signal +import threading +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union -from ..log import logger -from .runtime import min_version, runtime_info, register_signal -from .frame_cache import FrameCache -from .sampler_scheduler import SamplerScheduler, SamplerConfig -from .samplers.cpu_sampler import CPUSampler -from .samplers.allocation_sampler import AllocationSampler -from .samplers.block_sampler import BlockSampler +from instana.autoprofile.frame_cache import FrameCache +from instana.autoprofile.runtime import RuntimeInfo, min_version, register_signal +from instana.autoprofile.sampler_scheduler import SamplerConfig, SamplerScheduler +from instana.autoprofile.samplers.allocation_sampler import AllocationSampler +from instana.autoprofile.samplers.block_sampler import BlockSampler +from instana.autoprofile.samplers.cpu_sampler import CPUSampler +from instana.log import logger +if TYPE_CHECKING: + from types import FrameType + from instana.agent.host import HostAgent -class Profiler(object): - def __init__(self, agent): +class Profiler(object): + def __init__(self, agent: "HostAgent") -> None: self.agent = agent - self.profiler_started = False self.profiler_destroyed = False - self.sampler_active = False - self.main_thread_func = None - self.frame_cache = FrameCache(self) - - config = SamplerConfig() - config.log_prefix = 'CPU sampler' - config.max_profile_duration = 20 - config.max_span_duration = 5 - config.max_span_count = 30 - config.span_interval = 20 - config.report_interval = 120 - self.cpu_sampler_scheduler = SamplerScheduler(self, CPUSampler(self), config) - - config = SamplerConfig() - config.log_prefix = 'Allocation sampler' - config.max_profile_duration = 20 - config.max_span_duration = 5 - config.max_span_count = 30 - config.span_interval = 20 - config.report_interval = 120 - self.allocation_sampler_scheduler = SamplerScheduler(self, AllocationSampler(self), config) - - config = SamplerConfig() - config.log_prefix = 'Block sampler' - config.max_profile_duration = 20 - config.max_span_duration = 5 - config.max_span_count = 30 - config.span_interval = 20 - config.report_interval = 120 - self.block_sampler_scheduler = SamplerScheduler(self, BlockSampler(self), config) - self.options = None - - def get_option(self, name, default_val=None): + self.cpu_sampler_scheduler = self._create_sampler_scheduler( + CPUSampler(self), "CPU sampler", 20, 5, 30, 20, 120 + ) + self.allocation_sampler_scheduler = self._create_sampler_scheduler( + AllocationSampler(self), "Allocation sampler", 20, 5, 30, 20, 120 + ) + self.block_sampler_scheduler = self._create_sampler_scheduler( + BlockSampler(self), "Block sampler", 20, 5, 30, 20, 120 + ) + + def get_option( + self, name: str, default_val: Optional[object] = None + ) -> Optional[object]: if name not in self.options: return default_val else: return self.options[name] - def start(self, **kwargs): + def start(self, **kwargs: Dict[str, Any]) -> None: if self.profiler_started: return try: - if not min_version(2, 7) and not min_version(3, 4): - raise Exception('Supported Python versions 2.6 or higher and 3.4 or higher') + if not min_version(3, 8): + raise Exception("Supported Python versions 3.8 or higher.") - if platform.python_implementation() != 'CPython': - raise Exception('Supported Python interpreter is CPython') + if platform.python_implementation() != "CPython": + raise Exception("Supported Python interpreter is CPython.") if self.profiler_destroyed: - logger.warning('Destroyed profiler cannot be started') + logger.warning("Destroyed profiler cannot be started.") return self.options = kwargs - self.frame_cache.start() - self.cpu_sampler_scheduler.setup() self.allocation_sampler_scheduler.setup() self.block_sampler_scheduler.setup() # execute main_thread_func in main thread on signal - def _signal_handler(signum, frame): - if(self.main_thread_func): + def _signal_handler(signum: signal.Signals, frame: "FrameType") -> bool: + if self.main_thread_func: func = self.main_thread_func self.main_thread_func = None try: func() except Exception: - logger.error('Error in signal handler function', exc_info=True) + logger.error("Error in signal handler function", exc_info=True) return True - if not runtime_info.OS_WIN: + if not RuntimeInfo.OS_WIN: register_signal(signal.SIGUSR2, _signal_handler) self.cpu_sampler_scheduler.start() @@ -108,13 +88,13 @@ def _signal_handler(signum, frame): self.block_sampler_scheduler.start() self.profiler_started = True - logger.debug('Profiler started') + logger.debug("Profiler started.") except Exception: - logger.error('Error starting profiler', exc_info=True) + logger.error("Error starting profiler", exc_info=True) - def destroy(self): + def destroy(self) -> None: if not self.profiler_started: - logger.warning('Profiler has not been started') + logger.warning("Profiler has not been started.") return if self.profiler_destroyed: @@ -130,20 +110,20 @@ def destroy(self): self.block_sampler_scheduler.destroy() self.profiler_destroyed = True - logger.debug('Profiler destroyed') + logger.debug("Profiler destroyed.") - def run_in_thread(self, func): - def func_wrapper(): + def run_in_thread(self, func: Callable[..., object]) -> threading.Thread: + def func_wrapper() -> None: try: func() except Exception: - logger.error('Error in thread function', exc_info=True) + logger.error("Error in thread function", exc_info=True) t = threading.Thread(target=func_wrapper) t.start() return t - def run_in_main_thread(self, func): + def run_in_main_thread(self, func: Callable[..., object]) -> bool: if self.main_thread_func: return False @@ -151,3 +131,23 @@ def run_in_main_thread(self, func): os.kill(os.getpid(), signal.SIGUSR2) return True + + def _create_sampler_scheduler( + self, + sampler: Union["AllocationSampler", "BlockSampler", "CPUSampler"], + log_prefix: str, + max_profile_duration: int, + max_span_duration: int, + max_span_count: int, + span_interval: int, + report_interval: int, + ) -> SamplerScheduler: + config = SamplerConfig() + config.log_prefix = log_prefix + config.max_profile_duration = max_profile_duration + config.max_span_duration = max_span_duration + config.max_span_count = max_span_count + config.span_interval = span_interval + config.report_interval = report_interval + + return SamplerScheduler(self, sampler, config) diff --git a/src/instana/autoprofile/runtime.py b/src/instana/autoprofile/runtime.py index b2cb9976..e296e103 100644 --- a/src/instana/autoprofile/runtime.py +++ b/src/instana/autoprofile/runtime.py @@ -1,33 +1,42 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import sys -import signal import os +import signal +import sys +from typing import TYPE_CHECKING, Callable, Optional +if TYPE_CHECKING: + from types import FrameType -class runtime_info(object): - OS_LINUX = (sys.platform.startswith('linux')) - OS_DARWIN = (sys.platform == 'darwin') - OS_WIN = (sys.platform == 'win32') +class RuntimeInfo(object): + OS_LINUX = sys.platform.startswith("linux") + OS_DARWIN = sys.platform == "darwin" + OS_WIN = sys.platform == "win32" GEVENT = False + try: import gevent - if hasattr(gevent, '_threading'): - runtime_info.GEVENT = True + + if hasattr(gevent, "_threading"): + RuntimeInfo.GEVENT = True except ImportError: pass -def min_version(major, minor=0): - return (sys.version_info.major == major and sys.version_info.minor >= minor) +def min_version(major: int, minor: Optional[int] = 0) -> bool: + return sys.version_info.major == major and sys.version_info.minor >= minor -def register_signal(signal_number, handler_func, once=False): +def register_signal( + signal_number: signal.Signals, + handler_func: Callable[..., object], + once: Optional[bool] = False, +) -> None: prev_handler = None - def _handler(signum, frame): + def _handler(signum: signal.Signals, frame: "FrameType") -> None: skip_prev = handler_func(signum, frame) if not skip_prev: diff --git a/src/instana/autoprofile/sampler_scheduler.py b/src/instana/autoprofile/sampler_scheduler.py index ac4788d0..6513ec02 100644 --- a/src/instana/autoprofile/sampler_scheduler.py +++ b/src/instana/autoprofile/sampler_scheduler.py @@ -1,17 +1,22 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time import random +import time +from typing import TYPE_CHECKING, Union -from ..log import logger -from .profile import Profile -from .profile import CallSite -from .schedule import schedule, delay +from instana.autoprofile.schedule import delay, schedule +from instana.log import logger + +if TYPE_CHECKING: + from instana.autoprofile.profiler import Profiler + from instana.autoprofile.samplers.allocation_sampler import AllocationSampler + from instana.autoprofile.samplers.block_sampler import BlockSampler + from instana.autoprofile.samplers.cpu_sampler import CPUSampler class SamplerConfig(object): - def __init__(self): + def __init__(self) -> None: self.log_prefix = None self.max_profile_duration = None self.max_span_duration = None @@ -20,7 +25,12 @@ def __init__(self): class SamplerScheduler: - def __init__(self, profiler, sampler, config): + def __init__( + self, + profiler: "Profiler", + sampler: Union["AllocationSampler", "BlockSampler", "CPUSampler"], + config: SamplerConfig, + ) -> None: self.profiler = profiler self.sampler = sampler self.config = config @@ -35,28 +45,32 @@ def __init__(self, profiler, sampler, config): self.span_start_ts = None self.span_count = 0 - def setup(self): + def setup(self) -> None: self.sampler.setup() - def start(self): + def start(self) -> None: if not self.sampler.ready: return if self.started: return - self.started = True + self.started = True self.reset() - def random_delay(): - timeout = random.randint(0, round(self.config.span_interval - self.config.max_span_duration)) + def random_delay() -> None: + timeout = random.randint( + 0, round(self.config.span_interval - self.config.max_span_duration) + ) self.random_timer = delay(timeout, self.start_profiling) - if not self.profiler.get_option('disable_timers'): + if not self.profiler.get_option("disable_timers"): self.span_timer = schedule(0, self.config.span_interval, random_delay) - self.report_timer = schedule(self.config.report_interval, self.config.report_interval, self.report) + self.report_timer = schedule( + self.config.report_interval, self.config.report_interval, self.report + ) - def stop(self): + def stop(self) -> None: if not self.started: return @@ -76,67 +90,71 @@ def stop(self): self.stop_profiling() - def destroy(self): + def destroy(self) -> None: self.sampler.destroy() - def reset(self): + def reset(self) -> None: self.sampler.reset() self.profile_start_ts = time.time() self.profile_duration = 0 self.span_count = 0 - def start_profiling(self): + def start_profiling(self) -> bool: if not self.started: return False if self.profile_duration > self.config.max_profile_duration: - logger.debug(self.config.log_prefix + ': max profiling duration reached.') + logger.debug(f"{self.config.log_prefix}: max profiling duration reached.") return False if self.span_count > self.config.max_span_count: - logger.debug(self.config.log_prefix + ': max recording count reached.') + logger.debug(f"{self.config.log_prefix}: max recording count reached.") return False if self.profiler.sampler_active: - logger.debug(self.config.log_prefix + ': sampler lock exists.') + logger.debug(f"{self.config.log_prefix}: sampler lock exists.") return False + self.profiler.sampler_active = True - logger.debug(self.config.log_prefix + ': started.') + logger.debug(f"{self.config.log_prefix}: started.") try: self.sampler.start_sampler() except Exception: self.profiler.sampler_active = False - logger.error('Error starting profiling', exc_info=True) + logger.error("Error starting profiling", exc_info=True) return False self.span_timeout = delay(self.config.max_span_duration, self.stop_profiling) - + self.span_active = True self.span_start_ts = time.time() self.span_count += 1 return True - def stop_profiling(self): + def stop_profiling(self) -> None: if not self.span_active: return + self.span_active = False try: - self.profile_duration = self.profile_duration + time.time() - self.span_start_ts + self.profile_duration = ( + self.profile_duration + time.time() - self.span_start_ts + ) self.sampler.stop_sampler() except Exception: - logger.error('Error stopping profiling', exc_info=True) + logger.error("Error stopping profiling", exc_info=True) self.profiler.sampler_active = False if self.span_timeout: self.span_timeout.cancel() - logger.debug(self.config.log_prefix + ': stopped.') + logger.debug(f"{self.config.log_prefix}: stopped.") - def report(self): + def report(self) -> None: if not self.started: return @@ -150,8 +168,9 @@ def report(self): return profile = self.sampler.build_profile( - to_millis(self.profile_duration), - to_millis(time.time() - self.profile_start_ts)) + to_millis(self.profile_duration), + to_millis(time.time() - self.profile_start_ts), + ) if self.profiler.agent.can_send(): if self.profiler.agent.announce_data.pid: @@ -159,12 +178,14 @@ def report(self): self.profiler.agent.collector.profile_queue.put(profile.to_dict()) - logger.debug(self.config.log_prefix + ': reporting profile:') + logger.debug(f"{self.config.log_prefix}: reporting profile:") else: - logger.debug(self.config.log_prefix + ': not reporting profile, agent not ready') + logger.debug( + f"{self.config.log_prefix}: not reporting profile, agent not ready" + ) self.reset() -def to_millis(t): +def to_millis(t: int) -> int: return int(round(t * 1000)) diff --git a/src/instana/autoprofile/samplers/allocation_sampler.py b/src/instana/autoprofile/samplers/allocation_sampler.py index 82d9be0e..d65f329b 100644 --- a/src/instana/autoprofile/samplers/allocation_sampler.py +++ b/src/instana/autoprofile/samplers/allocation_sampler.py @@ -3,62 +3,69 @@ import threading -from ...log import logger -from ..runtime import min_version, runtime_info -from ..profile import Profile -from ..profile import CallSite -from ..schedule import schedule, delay +from instana.autoprofile.profile import CallSite, Profile +from instana.autoprofile.runtime import RuntimeInfo, min_version +from instana.autoprofile.schedule import schedule +from instana.log import logger if min_version(3, 4): import tracemalloc class AllocationSampler(object): - MAX_TRACEBACK_SIZE = 25 # number of frames - MAX_MEMORY_OVERHEAD = 10 * 1e6 # 10MB + MAX_TRACEBACK_SIZE = 25 # number of frames + MAX_MEMORY_OVERHEAD = 10 * 1e6 # 10MB MAX_PROFILED_ALLOCATIONS = 25 - def __init__(self, profiler): + def __init__(self, profiler: Profile) -> None: self.profiler = profiler self.ready = False self.top = None self.top_lock = threading.Lock() self.overhead_monitor = None - def setup(self): - if self.profiler.get_option('allocation_sampler_disabled'): + def setup(self) -> None: + if self.profiler.get_option("allocation_sampler_disabled"): return - if not runtime_info.OS_LINUX and not runtime_info.OS_DARWIN: - logger.debug('Allocation sampler is only supported on Linux and OS X.') + if not RuntimeInfo.OS_LINUX and not RuntimeInfo.OS_DARWIN: + logger.debug("Allocation sampler is only supported on Linux and OS X.") return if not min_version(3, 4): - logger.debug('Memory allocation profiling is available for Python 3.4 or higher') + logger.debug( + "Memory allocation profiling is available for Python 3.4 or higher." + ) return self.ready = True - def reset(self): - self.top = CallSite('', '', 0) + def reset(self) -> None: + self.top = CallSite("", "", 0) - def start_sampler(self): - logger.debug('Activating memory allocation sampler.') + def start_sampler(self) -> None: + logger.debug("Activating memory allocation sampler.") - def start(): + def start() -> None: tracemalloc.start(self.MAX_TRACEBACK_SIZE) + self.profiler.run_in_main_thread(start) - def monitor_overhead(): - if tracemalloc.is_tracing() and tracemalloc.get_tracemalloc_memory() > self.MAX_MEMORY_OVERHEAD: - logger.debug('Allocation sampler memory overhead limit exceeded: %s bytes', tracemalloc.get_tracemalloc_memory()) + def monitor_overhead() -> None: + if ( + tracemalloc.is_tracing() + and tracemalloc.get_tracemalloc_memory() > self.MAX_MEMORY_OVERHEAD + ): + logger.debug( + f"Allocation sampler memory overhead limit exceeded: {tracemalloc.get_tracemalloc_memory()} bytes." + ) self.stop_sampler() - if not self.profiler.get_option('disable_timers'): + if not self.profiler.get_option("disable_timers"): self.overhead_monitor = schedule(0.5, 0.5, monitor_overhead) - def stop_sampler(self): - logger.debug('Deactivating memory allocation sampler.') + def stop_sampler(self) -> None: + logger.debug("Deactivating memory allocation sampler.") with self.top_lock: if self.overhead_monitor: @@ -67,11 +74,13 @@ def stop_sampler(self): if tracemalloc.is_tracing(): snapshot = tracemalloc.take_snapshot() - logger.debug('Allocation sampler memory overhead %s bytes', tracemalloc.get_tracemalloc_memory()) + logger.debug( + f"Allocation sampler memory overhead {tracemalloc.get_tracemalloc_memory()} bytes.", + ) tracemalloc.stop() self.process_snapshot(snapshot) - def build_profile(self, duration, timespan): + def build_profile(self, duration: int, timespan: int) -> Profile: with self.top_lock: self.top.normalize(duration) self.top.floor() @@ -82,22 +91,24 @@ def build_profile(self, duration, timespan): Profile.UNIT_BYTE, self.top.children.values(), duration, - timespan + timespan, ) return profile - def destroy(self): + def destroy(self) -> None: pass - def process_snapshot(self, snapshot): - stats = snapshot.statistics('traceback') + def process_snapshot(self, snapshot: tracemalloc.Snapshot) -> None: + stats = snapshot.statistics("traceback") - for stat in stats[:self.MAX_PROFILED_ALLOCATIONS]: + for stat in stats[: self.MAX_PROFILED_ALLOCATIONS]: if stat.traceback: skip_stack = False for frame in stat.traceback: - if frame.filename and self.profiler.frame_cache.is_profiler_frame(frame.filename): + if frame.filename and self.profiler.frame_cache.is_profiler_frame( + frame.filename + ): skip_stack = True break if skip_stack: @@ -105,8 +116,10 @@ def process_snapshot(self, snapshot): current_node = self.top for frame in reversed(stat.traceback): - if frame.filename == '': + if frame.filename == "": continue - current_node = current_node.find_or_add_child('', frame.filename, frame.lineno) + current_node = current_node.find_or_add_child( + "", frame.filename, frame.lineno + ) current_node.increment(stat.size, stat.count) diff --git a/src/instana/autoprofile/samplers/block_sampler.py b/src/instana/autoprofile/samplers/block_sampler.py index a604d79a..0fc6018b 100644 --- a/src/instana/autoprofile/samplers/block_sampler.py +++ b/src/instana/autoprofile/samplers/block_sampler.py @@ -1,24 +1,28 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import signal import sys import threading -import signal +from typing import TYPE_CHECKING, List, Optional, Tuple + +from instana.autoprofile.profile import CallSite, Profile +from instana.autoprofile.runtime import RuntimeInfo +from instana.log import logger -from ...log import logger -from ..runtime import runtime_info -from ..profile import Profile -from ..profile import CallSite +if TYPE_CHECKING: + from types import FrameType -if runtime_info.GEVENT: + +if RuntimeInfo.GEVENT: import gevent class BlockSampler(object): SAMPLING_RATE = 0.05 - MAX_TRACEBACK_SIZE = 25 # number of frames + MAX_TRACEBACK_SIZE = 25 # number of frames - def __init__(self, profiler): + def __init__(self, profiler: Profile) -> None: self.profiler = profiler self.ready = False self.top = None @@ -26,23 +30,23 @@ def __init__(self, profiler): self.prev_signal_handler = None self.sampler_active = False - def setup(self): - if self.profiler.get_option('block_sampler_disabled'): + def setup(self) -> None: + if self.profiler.get_option("block_sampler_disabled"): return - if not runtime_info.OS_LINUX and not runtime_info.OS_DARWIN: - logger.debug('CPU profiler is only supported on Linux and OS X.') + if not RuntimeInfo.OS_LINUX and not RuntimeInfo.OS_DARWIN: + logger.debug("CPU profiler is only supported on Linux and OS X.") return sample_time = self.SAMPLING_RATE * 1000 - main_thread_id = None - if runtime_info.GEVENT: - main_thread_id = gevent._threading.get_ident() - else: - main_thread_id = threading.current_thread().ident + main_thread_id = ( + gevent._threading.get_ident() + if RuntimeInfo.GEVENT + else threading.current_thread().ident + ) - def _sample(signum, signal_frame): + def _sample(signum: object, signal_frame: "FrameType") -> None: if self.sampler_active: return self.sampler_active = True @@ -52,7 +56,7 @@ def _sample(signum, signal_frame): self.process_sample(signal_frame, sample_time, main_thread_id) signal_frame = None except Exception: - logger.error('Error processing sample', exc_info=True) + logger.error("Error processing sample", exc_info=True) self.sampler_active = False @@ -60,26 +64,26 @@ def _sample(signum, signal_frame): self.ready = True - def destroy(self): + def destroy(self) -> None: if not self.ready: return signal.signal(signal.SIGALRM, self.prev_signal_handler) - def reset(self): - self.top = CallSite('', '', 0) + def reset(self) -> None: + self.top = CallSite("", "", 0) - def start_sampler(self): - logger.debug('Activating block sampler.') + def start_sampler(self) -> None: + logger.debug("Activating block sampler.") signal.setitimer(signal.ITIMER_REAL, self.SAMPLING_RATE, self.SAMPLING_RATE) - def stop_sampler(self): + def stop_sampler(self) -> None: signal.setitimer(signal.ITIMER_REAL, 0) - logger.debug('Deactivating block sampler.') + logger.debug("Deactivating block sampler.") - def build_profile(self, duration, timespan): + def build_profile(self, duration: int, timespan: int) -> Profile: with self.top_lock: self.top.normalize(duration) self.top.floor() @@ -90,12 +94,14 @@ def build_profile(self, duration, timespan): Profile.UNIT_MILLISECOND, self.top.children.values(), duration, - timespan + timespan, ) return profile - def process_sample(self, signal_frame, sample_time, main_thread_id): + def process_sample( + self, signal_frame: "FrameType", sample_time: int, main_thread_id: int + ) -> None: if self.top: current_frames = sys._current_frames() items = current_frames.items() @@ -107,7 +113,9 @@ def process_sample(self, signal_frame, sample_time, main_thread_id): if stack: current_node = self.top for func_name, filename, lineno in reversed(stack): - current_node = current_node.find_or_add_child(func_name, filename, lineno) + current_node = current_node.find_or_add_child( + func_name, filename, lineno + ) current_node.increment(sample_time, 1) thread_id, thread_frame, stack = None, None, None @@ -115,13 +123,18 @@ def process_sample(self, signal_frame, sample_time, main_thread_id): items = None current_frames = None - - def recover_stack(self, thread_frame): + def recover_stack( + self, thread_frame: "FrameType" + ) -> Optional[List[Tuple[str, str, int]]]: stack = [] depth = 0 while thread_frame is not None and depth <= self.MAX_TRACEBACK_SIZE: - if thread_frame.f_code and thread_frame.f_code.co_name and thread_frame.f_code.co_filename: + if ( + thread_frame.f_code + and thread_frame.f_code.co_name + and thread_frame.f_code.co_filename + ): func_name = thread_frame.f_code.co_name filename = thread_frame.f_code.co_filename lineno = thread_frame.f_lineno diff --git a/src/instana/autoprofile/samplers/cpu_sampler.py b/src/instana/autoprofile/samplers/cpu_sampler.py index 98a4fd2c..2743a475 100644 --- a/src/instana/autoprofile/samplers/cpu_sampler.py +++ b/src/instana/autoprofile/samplers/cpu_sampler.py @@ -1,20 +1,23 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import threading import signal +import threading +from typing import TYPE_CHECKING, List, Optional, Tuple + +from instana.autoprofile.profile import CallSite, Profile +from instana.autoprofile.runtime import RuntimeInfo +from instana.log import logger -from ...log import logger -from ..runtime import runtime_info -from ..profile import Profile -from ..profile import CallSite +if TYPE_CHECKING: + from types import FrameType class CPUSampler(object): SAMPLING_RATE = 0.01 - MAX_TRACEBACK_SIZE = 25 # number of frames + MAX_TRACEBACK_SIZE = 25 # number of frames - def __init__(self, profiler): + def __init__(self, profiler: Profile) -> None: self.profiler = profiler self.ready = False self.top = None @@ -22,15 +25,15 @@ def __init__(self, profiler): self.prev_signal_handler = None self.sampler_active = False - def setup(self): - if self.profiler.get_option('cpu_sampler_disabled'): + def setup(self) -> None: + if self.profiler.get_option("cpu_sampler_disabled"): return - if not runtime_info.OS_LINUX and not runtime_info.OS_DARWIN: - logger.debug('CPU sampler is only supported on Linux and OS X.') + if not RuntimeInfo.OS_LINUX and not RuntimeInfo.OS_DARWIN: + logger.debug("CPU sampler is only supported on Linux and OS X.") return - def _sample(signum, signal_frame): + def _sample(signum: object, signal_frame: "FrameType") -> None: if self.sampler_active: return self.sampler_active = True @@ -40,32 +43,32 @@ def _sample(signum, signal_frame): self.process_sample(signal_frame) signal_frame = None except Exception: - logger.error('Error in signal handler', exc_info=True) - + logger.error("Error in signal handler", exc_info=True) + self.sampler_active = False self.prev_signal_handler = signal.signal(signal.SIGPROF, _sample) self.ready = True - def reset(self): - self.top = CallSite('', '', 0) + def reset(self) -> None: + self.top = CallSite("", "", 0) - def start_sampler(self): - logger.debug('Activating CPU sampler.') + def start_sampler(self) -> None: + logger.debug("Activating CPU sampler.") signal.setitimer(signal.ITIMER_PROF, self.SAMPLING_RATE, self.SAMPLING_RATE) - def stop_sampler(self): + def stop_sampler(self) -> None: signal.setitimer(signal.ITIMER_PROF, 0) - def destroy(self): + def destroy(self) -> None: if not self.ready: return signal.signal(signal.SIGPROF, self.prev_signal_handler) - def build_profile(self, duration, timespan): + def build_profile(self, duration: int, timespan: int) -> Profile: with self.top_lock: profile = Profile( Profile.CATEGORY_CPU, @@ -73,12 +76,12 @@ def build_profile(self, duration, timespan): Profile.UNIT_SAMPLE, self.top.children.values(), duration, - timespan + timespan, ) return profile - def process_sample(self, signal_frame): + def process_sample(self, signal_frame: "FrameType") -> None: if self.top: if signal_frame: stack = self.recover_stack(signal_frame) @@ -87,12 +90,18 @@ def process_sample(self, signal_frame): stack = None - def recover_stack(self, signal_frame): + def recover_stack( + self, signal_frame: "FrameType" + ) -> Optional[List[Tuple[str, str, int]]]: stack = [] depth = 0 while signal_frame is not None and depth <= self.MAX_TRACEBACK_SIZE: - if signal_frame.f_code and signal_frame.f_code.co_name and signal_frame.f_code.co_filename: + if ( + signal_frame.f_code + and signal_frame.f_code.co_name + and signal_frame.f_code.co_filename + ): func_name = signal_frame.f_code.co_name filename = signal_frame.f_code.co_filename lineno = signal_frame.f_lineno @@ -100,11 +109,11 @@ def recover_stack(self, signal_frame): if filename and self.profiler.frame_cache.is_profiler_frame(filename): return None - #frame = Frame(func_name, filename, lineno) + # frame = Frame(func_name, filename, lineno) stack.append((func_name, filename, lineno)) signal_frame = signal_frame.f_back - + depth += 1 if len(stack) == 0: @@ -112,10 +121,10 @@ def recover_stack(self, signal_frame): else: return stack - def update_profile(self, profile, stack): + def update_profile(self, profile: Profile, stack: List[Tuple[str, str, int]]): current_node = profile for func_name, filename, lineno in reversed(stack): current_node = current_node.find_or_add_child(func_name, filename, lineno) - + current_node.increment(1, 1) diff --git a/src/instana/autoprofile/schedule.py b/src/instana/autoprofile/schedule.py index 1c8a8a6d..c74dd2b8 100644 --- a/src/instana/autoprofile/schedule.py +++ b/src/instana/autoprofile/schedule.py @@ -3,28 +3,31 @@ import threading import time +from typing import Callable, Tuple -from ..log import logger +from instana.log import logger class TimerWraper(object): - def __init__(self): + def __init__(self) -> None: self.timer = None self.cancel_lock = threading.Lock() self.canceled = False - def cancel(self): + def cancel(self) -> None: with self.cancel_lock: self.canceled = True self.timer.cancel() -def delay(timeout, func, *args): - def func_wrapper(): +def delay( + timeout: float, func: Callable[..., object], *args: Tuple[object] +) -> threading.Timer: + def func_wrapper() -> None: try: func(*args) except Exception: - logger.error('Error in delayed function', exc_info=True) + logger.error("Error in delayed function", exc_info=True) t = threading.Timer(timeout, func_wrapper, ()) t.start() @@ -32,23 +35,27 @@ def func_wrapper(): return t -def schedule( timeout, interval, func, *args): +def schedule( + timeout: float, interval: float, func: Callable[..., object], *args: Tuple[object] +) -> TimerWraper: tw = TimerWraper() - def func_wrapper(): + def func_wrapper() -> None: start = time.time() try: func(*args) except Exception: - logger.error('Error in scheduled function', exc_info=True) + logger.error("Error in scheduled function", exc_info=True) with tw.cancel_lock: if not tw.canceled: - tw.timer = threading.Timer(abs(interval - (time.time() - start)), func_wrapper, ()) + tw.timer = threading.Timer( + abs(interval - (time.time() - start)), func_wrapper, () + ) tw.timer.start() tw.timer = threading.Timer(timeout, func_wrapper, ()) tw.timer.start() - return tw \ No newline at end of file + return tw 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..67008e34 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,13 +130,17 @@ 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") + logger.debug( + "Thread shutdown signal is active: Shutting down reporting thread" + ) return False return True @@ -147,8 +150,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 +189,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..7292cca4 --- /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 not span.k is 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/aws/lambda_inst.py b/src/instana/instrumentation/aws/lambda_inst.py index 926c0967..9cb37dff 100644 --- a/src/instana/instrumentation/aws/lambda_inst.py +++ b/src/instana/instrumentation/aws/lambda_inst.py @@ -4,66 +4,89 @@ """ Instrumentation for AWS Lambda functions """ + import sys +import traceback +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple + import wrapt -import opentracing.ext.tags as ext +from opentelemetry.semconv.trace import SpanAttributes -from ...log import logger -from ...singletons import env_is_aws_lambda -from ... import get_lambda_handler_or_default -from ...singletons import get_agent, get_tracer -from .triggers import enrich_lambda_span, get_context -import traceback +from instana import get_aws_lambda_handler +from instana.instrumentation.aws.triggers import enrich_lambda_span, get_context +from instana.log import logger +from instana.singletons import env_is_aws_lambda, get_agent, get_tracer + +if TYPE_CHECKING: + from instana.agent.aws_lambda import AWSLambdaAgent -def lambda_handler_with_instana(wrapped, instance, args, kwargs): +def lambda_handler_with_instana( + wrapped: Callable[..., object], + instance: object, + args: Tuple[object, ...], + kwargs: Dict[str, Any], +) -> object: event = args[0] - agent = get_agent() + agent: "AWSLambdaAgent" = get_agent() tracer = get_tracer() agent.collector.collect_snapshot(*args) incoming_ctx = get_context(tracer, event) result = None - with tracer.start_active_span("aws.lambda.entry", child_of=incoming_ctx) as scope: - enrich_lambda_span(agent, scope.span, *args) + with tracer.start_as_current_span( + "aws.lambda.entry", span_context=incoming_ctx + ) as span: + enrich_lambda_span(agent, span, *args) try: result = wrapped(*args, **kwargs) if isinstance(result, dict): - server_timing_value = "intid;desc=%s" % scope.span.context.trace_id - if 'headers' in result: - result['headers']['Server-Timing'] = server_timing_value - elif 'multiValueHeaders' in result: - result['multiValueHeaders']['Server-Timing'] = [server_timing_value] - if 'statusCode' in result and result.get('statusCode'): - status_code = int(result['statusCode']) - scope.span.set_tag(ext.HTTP_STATUS_CODE, status_code) + server_timing_value = f"intid;desc={span.context.trace_id}" + if "headers" in result: + result["headers"]["Server-Timing"] = server_timing_value + elif "multiValueHeaders" in result: + result["multiValueHeaders"]["Server-Timing"] = [server_timing_value] + if "statusCode" in result and result.get("statusCode"): + status_code = int(result["statusCode"]) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) if 500 <= status_code: - scope.span.log_exception(f'HTTP status {status_code}') + span.record_exception(f"HTTP status {status_code}") except Exception as exc: - if scope.span: + logger.debug(f"AWS Lambda lambda_handler_with_instana error: {exc}") + if span: exc = traceback.format_exc() - scope.span.log_exception(exc) + span.record_exception(exc) raise finally: - scope.close() agent.collector.shutdown() - agent.collector.shutdown() + if agent.collector.started: + agent.collector.shutdown() + return result -if env_is_aws_lambda is True: - handler_module, handler_function = get_lambda_handler_or_default() +if env_is_aws_lambda: + handler_module, handler_function = get_aws_lambda_handler() - if handler_module is not None and handler_function is not None: + if handler_module and handler_function: try: - logger.debug("Instrumenting AWS Lambda handler (%s.%s)" % (handler_module, handler_function)) - sys.path.insert(0, '/var/runtime') - sys.path.insert(0, '/var/task') - wrapt.wrap_function_wrapper(handler_module, handler_function, lambda_handler_with_instana) + logger.debug( + f"Instrumenting AWS Lambda handler ({handler_module}.{handler_function})" + ) + sys.path.insert(0, "/var/runtime") + sys.path.insert(0, "/var/task") + wrapt.wrap_function_wrapper( + handler_module, handler_function, lambda_handler_with_instana + ) except (ModuleNotFoundError, ImportError) as exc: - logger.warning("Instana: Couldn't instrument AWS Lambda handler. Not monitoring.") + logger.debug(f"AWS Lambda error: {exc}") + logger.warning( + "Instana: Couldn't instrument AWS Lambda handler. Not monitoring." + ) else: - logger.warning("Instana: Couldn't determine AWS Lambda Handler. Not monitoring.") + logger.warning( + "Instana: Couldn't determine AWS Lambda Handler. Not monitoring." + ) diff --git a/src/instana/instrumentation/aws/triggers.py b/src/instana/instrumentation/aws/triggers.py index c91aac33..67ebcd39 100644 --- a/src/instana/instrumentation/aws/triggers.py +++ b/src/instana/instrumentation/aws/triggers.py @@ -4,37 +4,54 @@ """ Module to handle the work related to the many AWS Lambda Triggers. """ + +import base64 import gzip import json -import base64 from io import BytesIO -import opentracing as ot +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format -from ...log import logger +if TYPE_CHECKING: + from opentelemetry.context import Context -STR_LAMBDA_TRIGGER = 'lambda.trigger' + from instana.agent.aws_lambda import AWSLambdaAgent + from instana.span.span import InstanaSpan + from instana.tracer import InstanaTracer +STR_LAMBDA_TRIGGER = "lambda.trigger" -def get_context(tracer, event): + +def get_context(tracer: "InstanaTracer", event: Dict[str, Any]) -> Optional["Context"]: # TODO: Search for more types of trigger context - is_proxy_event = is_api_gateway_proxy_trigger(event) or \ - is_api_gateway_v2_proxy_trigger(event) or \ - is_application_load_balancer_trigger(event) + is_proxy_event = ( + is_api_gateway_proxy_trigger(event) + or is_api_gateway_v2_proxy_trigger(event) + or is_application_load_balancer_trigger(event) + ) if is_proxy_event: - return tracer.extract(ot.Format.HTTP_HEADERS, event.get('headers', {}), disable_w3c_trace_context=True) + return tracer.extract( + Format.HTTP_HEADERS, + event.get("headers", {}), + disable_w3c_trace_context=True, + ) - return tracer.extract(ot.Format.HTTP_HEADERS, event, disable_w3c_trace_context=True) + return tracer.extract(Format.HTTP_HEADERS, event, disable_w3c_trace_context=True) -def is_api_gateway_proxy_trigger(event): +def is_api_gateway_proxy_trigger(event: Dict[str, Any]) -> bool: for key in ["resource", "path", "httpMethod"]: if key not in event: return False return True -def is_api_gateway_v2_proxy_trigger(event): +def is_api_gateway_v2_proxy_trigger(event: Dict[str, Any]) -> bool: for key in ["version", "requestContext"]: if key not in event: return False @@ -48,41 +65,48 @@ def is_api_gateway_v2_proxy_trigger(event): return True -def is_application_load_balancer_trigger(event): - if 'requestContext' in event and 'elb' in event['requestContext']: + +def is_application_load_balancer_trigger(event: Dict[str, Any]) -> bool: + if "requestContext" in event and "elb" in event["requestContext"]: return True return False -def is_cloudwatch_trigger(event): - if "source" in event and 'detail-type' in event: - if event["source"] == 'aws.events' and event['detail-type'] == 'Scheduled Event': +def is_cloudwatch_trigger(event: Dict[str, Any]) -> bool: + if "source" in event and "detail-type" in event: + if ( + event["source"] == "aws.events" + and event["detail-type"] == "Scheduled Event" + ): return True return False -def is_cloudwatch_logs_trigger(event): - if hasattr(event, 'get') and event.get("awslogs", False) is not False: +def is_cloudwatch_logs_trigger(event: Dict[str, Any]) -> bool: + if hasattr(event, "get") and event.get("awslogs", "\b") != "\b": return True else: return False -def is_s3_trigger(event): +def is_s3_trigger(event: Dict[str, Any]) -> bool: if "Records" in event: - if len(event["Records"]) > 0 and event["Records"][0]["eventSource"] == 'aws:s3': + if len(event["Records"]) > 0 and event["Records"][0]["eventSource"] == "aws:s3": return True return False -def is_sqs_trigger(event): +def is_sqs_trigger(event: Dict[str, Any]) -> bool: if "Records" in event: - if len(event["Records"]) > 0 and event["Records"][0]["eventSource"] == 'aws:sqs': + if ( + len(event["Records"]) > 0 + and event["Records"][0]["eventSource"] == "aws:sqs" + ): return True return False -def read_http_query_params(event): +def read_http_query_params(event: Dict[str, Any]) -> str: """ Used to parse the Lambda QueryString formats. @@ -94,25 +118,27 @@ def read_http_query_params(event): if event is None or type(event) is not dict: return "" - mvqsp = event.get('multiValueQueryStringParameters', None) - qsp = event.get('queryStringParameters', None) + mvqsp = event.get("multiValueQueryStringParameters", None) + qsp = event.get("queryStringParameters", None) if mvqsp is not None and type(mvqsp) is dict: for key in mvqsp: - params.append("%s=%s" % (key, mvqsp[key])) + params.append(f"{key}={mvqsp[key]}") return "&".join(params) elif qsp is not None and type(qsp) is dict: for key in qsp: - params.append("%s=%s" % (key, qsp[key])) + params.append(f"{key}={qsp[key]}") return "&".join(params) else: return "" except Exception: - logger.debug("read_http_query_params: ", exc_info=True) + logger.debug("AWS Lambda read_http_query_params error: ", exc_info=True) return "" -def capture_extra_headers(event, span, extra_headers): +def capture_extra_headers( + event: Dict[str, Any], span: "InstanaSpan", extra_headers: List[Dict[str, Any]] +) -> None: """ Capture the headers specified in `extra_headers` from `event` and log them as a tag in the span. @@ -125,16 +151,23 @@ def capture_extra_headers(event, span, extra_headers): try: event_headers = event.get("headers", None) - if event_headers is not None: + if event_headers: for custom_header in extra_headers: for key in event_headers: if key.lower() == custom_header.lower(): - span.set_tag("http.header.%s" % custom_header, event_headers[key]) + span.set_attribute( + "http.header.%s" % custom_header, event_headers[key] + ) except Exception: - logger.debug("capture_extra_headers: ", exc_info=True) + logger.debug("AWS Lambda capture_extra_headers error: ", exc_info=True) -def enrich_lambda_span(agent, span, event, context): +def enrich_lambda_span( + agent: "AWSLambdaAgent", + span: "InstanaSpan", + event: Optional[Dict[str, Any]], + context: "Context", +) -> None: """ Extract the required information about this Lambda run (and the trigger) and store the data on `span`. @@ -146,23 +179,23 @@ def enrich_lambda_span(agent, span, event, context): @return: None """ try: - span.set_tag('lambda.arn', agent.collector.get_fq_arn()) - span.set_tag('lambda.name', context.function_name) - span.set_tag('lambda.version', context.function_version) + span.set_attribute("lambda.arn", agent.collector.get_fq_arn()) + span.set_attribute("lambda.name", context.function_name) + span.set_attribute("lambda.version", context.function_version) - if event is None or type(event) is not dict: - logger.debug("enrich_lambda_span: bad event %s", type(event)) + if not event or not isinstance(event, dict): + logger.debug(f"AWS Lambda enrich_lambda_span: bad event {type(event)}") return if is_api_gateway_proxy_trigger(event): logger.debug("Detected as API Gateway Proxy Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:api.gateway') - span.set_tag('http.method', event["httpMethod"]) - span.set_tag('http.url', event["path"]) - span.set_tag('http.path_tpl', event["resource"]) - span.set_tag('http.params', read_http_query_params(event)) + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:api.gateway") + span.set_attribute(SpanAttributes.HTTP_METHOD, event["httpMethod"]) + span.set_attribute(SpanAttributes.HTTP_URL, event["path"]) + span.set_attribute("http.path_tpl", event["resource"]) + span.set_attribute("http.params", read_http_query_params(event)) - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: capture_extra_headers(event, span, agent.options.extra_http_headers) elif is_api_gateway_v2_proxy_trigger(event): @@ -173,76 +206,81 @@ def enrich_lambda_span(agent, span, event, context): # trim optional HTTP method prefix route_path = event["routeKey"].split(" ", 2)[-1] - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:api.gateway') - span.set_tag('http.method', reqCtx["http"]["method"]) - span.set_tag('http.url', reqCtx["http"]["path"]) - span.set_tag('http.path_tpl', route_path) - span.set_tag('http.params', read_http_query_params(event)) + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:api.gateway") + span.set_attribute(SpanAttributes.HTTP_METHOD, reqCtx["http"]["method"]) + span.set_attribute(SpanAttributes.HTTP_URL, reqCtx["http"]["path"]) + span.set_attribute("http.path_tpl", route_path) + span.set_attribute("http.params", read_http_query_params(event)) - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: capture_extra_headers(event, span, agent.options.extra_http_headers) elif is_application_load_balancer_trigger(event): logger.debug("Detected as Application Load Balancer Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:application.load.balancer') - span.set_tag('http.method', event["httpMethod"]) - span.set_tag('http.url', event["path"]) - span.set_tag('http.params', read_http_query_params(event)) + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:application.load.balancer") + span.set_attribute(SpanAttributes.HTTP_METHOD, event["httpMethod"]) + span.set_attribute(SpanAttributes.HTTP_URL, event["path"]) + span.set_attribute("http.params", read_http_query_params(event)) - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: capture_extra_headers(event, span, agent.options.extra_http_headers) elif is_cloudwatch_trigger(event): logger.debug("Detected as Cloudwatch Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:cloudwatch.events') - span.set_tag('data.lambda.cw.events.id', event['id']) - - resources = event['resources'] - resource_count = len(event['resources']) - if resource_count > 3: - resources = event['resources'][:3] - span.set_tag('lambda.cw.events.more', True) + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:cloudwatch.events") + span.set_attribute("data.lambda.cw.events.id", event["id"]) + + resources = event["resources"] + if len(event["resources"]) > 3: + resources = event["resources"][:3] + span.set_attribute("lambda.cw.events.more", True) else: - span.set_tag('lambda.cw.events.more', False) + span.set_attribute("lambda.cw.events.more", False) report = [] for item in resources: if len(item) > 200: item = item[:200] report.append(item) - span.set_tag('lambda.cw.events.resources', report) + span.set_attribute("lambda.cw.events.resources", report) elif is_cloudwatch_logs_trigger(event): logger.debug("Detected as Cloudwatch Logs Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:cloudwatch.logs') + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:cloudwatch.logs") try: - if 'awslogs' in event and 'data' in event['awslogs']: - data = event['awslogs']['data'] + if "awslogs" in event and "data" in event["awslogs"]: + data = event["awslogs"]["data"] decoded_data = base64.b64decode(data) - decompressed_data = gzip.GzipFile(fileobj=BytesIO(decoded_data)).read() - log_data = json.loads(decompressed_data.decode('utf-8')) - - span.set_tag('lambda.cw.logs.group', log_data.get('logGroup', None)) - span.set_tag('lambda.cw.logs.stream', log_data.get('logStream', None)) - if len(log_data['logEvents']) > 3: - span.set_tag('lambda.cw.logs.more', True) - events = log_data['logEvents'][:3] + decompressed_data = gzip.GzipFile( + fileobj=BytesIO(decoded_data) + ).read() + log_data = json.loads(decompressed_data.decode("utf-8")) + + span.set_attribute( + "lambda.cw.logs.group", log_data.get("logGroup", None) + ) + span.set_attribute( + "lambda.cw.logs.stream", log_data.get("logStream", None) + ) + if len(log_data["logEvents"]) > 3: + span.set_attribute("lambda.cw.logs.more", True) + events = log_data["logEvents"][:3] else: - events = log_data['logEvents'] + events = log_data["logEvents"] event_data = [] for item in events: - msg = item.get('message', None) + msg = item.get("message", None) if len(msg) > 200: msg = msg[:200] event_data.append(msg) - span.set_tag('lambda.cw.logs.events', event_data) + span.set_attribute("lambda.cw.logs.events", event_data) except Exception as e: - span.set_tag('lambda.cw.logs.decodingError', repr(e)) + span.set_attribute("lambda.cw.logs.decodingError", repr(e)) elif is_s3_trigger(event): logger.debug("Detected as S3 Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:s3') + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:s3") if "Records" in event: events = [] @@ -258,23 +296,27 @@ def enrich_lambda_span(agent, span, event, context): if len(object_name) > 200: object_name = object_name[:200] - events.append({"event": item['eventName'], - "bucket": bucket_name, - "object": object_name}) - span.set_tag('lambda.s3.events', events) + events.append( + { + "event": item["eventName"], + "bucket": bucket_name, + "object": object_name, + } + ) + span.set_attribute("lambda.s3.events", events) elif is_sqs_trigger(event): logger.debug("Detected as SQS Trigger") - span.set_tag(STR_LAMBDA_TRIGGER, 'aws:sqs') + span.set_attribute(STR_LAMBDA_TRIGGER, "aws:sqs") if "Records" in event: events = [] for item in event["Records"][:3]: - events.append({'queue': item['eventSourceARN']}) - span.set_tag('lambda.sqs.messages', events) + events.append({"queue": item["eventSourceARN"]}) + span.set_attribute("lambda.sqs.messages", events) else: - logger.debug("Detected as Unknown Trigger: %s" % event) - span.set_tag(STR_LAMBDA_TRIGGER, 'unknown') + logger.debug(f"Detected as Unknown Trigger: {event}") + span.set_attribute(STR_LAMBDA_TRIGGER, "unknown") except Exception: - logger.debug("enrich_lambda_span: ", exc_info=True) + logger.debug("AWS Lambda enrich_lambda_span error: ", exc_info=True) 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/celery.py b/src/instana/instrumentation/celery.py new file mode 100644 index 00000000..c69131aa --- /dev/null +++ b/src/instana/instrumentation/celery.py @@ -0,0 +1,202 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + + +import contextvars +from typing import Any, Dict, Tuple +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import tracer +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple +from opentelemetry import trace, context + +try: + import celery + from celery import registry, signals + + from urllib import parse + + client_token: Dict[str, Any] = {} + worker_token: Dict[str, Any] = {} + client_span = contextvars.ContextVar("client_span") + worker_span = contextvars.ContextVar("worker_span") + + def _get_task_id( + headers: Dict[str, Any], + body: Tuple[str, Any], + ) -> str: + """ + Across Celery versions, the task id can exist in a couple of places. + """ + id = headers.get("id", None) + if id is None: + id = body.get("id", None) + return id + + def add_broker_attributes( + span: InstanaSpan, + broker_url: str, + ) -> None: + try: + url = parse.urlparse(broker_url) + + # Add safety for edge case where scheme may not be a string + url_scheme = str(url.scheme) + span.set_attribute("scheme", url_scheme) + + span.set_attribute("host", url.hostname if url.hostname else "localhost") + + if not url.port: + # Set default port if not specified + if url_scheme == "redis": + span.set_attribute("port", "6379") + elif "amqp" in url_scheme: + span.set_attribute("port", "5672") + elif "sqs" in url_scheme: + span.set_attribute("port", "443") + else: + span.set_attribute("port", str(url.port)) + except Exception: + logger.debug(f"Error parsing broker URL: {broker_url}", exc_info=True) + + @signals.task_prerun.connect + def task_prerun( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + ctx = None + + task = kwargs.get("sender", None) + task_id = kwargs.get("task_id", None) + task = registry.tasks.get(task.name) + + headers = task.request.get("headers", {}) + if headers is not None: + ctx = tracer.extract( + Format.HTTP_HEADERS, headers, disable_w3c_trace_context=True + ) + + span = tracer.start_span("celery-worker", span_context=ctx) + span.set_attribute("task", task.name) + span.set_attribute("task_id", task_id) + add_broker_attributes(span, task.app.conf["broker_url"]) + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + worker_token["token"] = token + worker_span.set(span) + except Exception: + logger.debug("celery-worker task_prerun: ", exc_info=True) + + @signals.task_postrun.connect + def task_postrun( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + span = worker_span.get() + + if span.is_recording(): + span.end() + worker_span.set(None) + if "token" in worker_token: + context.detach(worker_token.pop("token", None)) + except Exception: + logger.debug("celery-worker after_task_publish: ", exc_info=True) + + @signals.task_failure.connect + def task_failure( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + span = worker_span.get() + if span.is_recording(): + span.set_attribute("success", False) + exc = kwargs.get("exception", None) + if exc: + span.record_exception(exc) + else: + span.mark_as_errored() + except Exception: + logger.debug("celery-worker task_failure: ", exc_info=True) + + @signals.task_retry.connect + def task_retry( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + span = worker_span.get() + if span.is_recording(): + reason = kwargs.get("reason", None) + if reason: + span.set_attribute("retry-reason", reason) + except Exception: + logger.debug("celery-worker task_failure: ", exc_info=True) + + @signals.before_task_publish.connect + def before_task_publish( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None + + if tracer: + body = kwargs["body"] + headers = kwargs["headers"] + task_name = kwargs["sender"] + task = registry.tasks.get(task_name) + task_id = _get_task_id(headers, body) + + span = tracer.start_span("celery-client", span_context=parent_context) + span.set_attribute("task", task_name) + span.set_attribute("task_id", task_id) + add_broker_attributes(span, task.app.conf["broker_url"]) + + # Context propagation + context_headers = {} + tracer.inject( + span.context, + Format.HTTP_HEADERS, + context_headers, + disable_w3c_trace_context=True, + ) + + # Fix for broken header propagation + # https://github.com/celery/celery/issues/4875 + task_headers = kwargs.get("headers") or {} + task_headers.setdefault("headers", {}) + task_headers["headers"].update(context_headers) + kwargs["headers"] = task_headers + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + client_token["token"] = token + client_span.set(span) + except Exception: + logger.debug("celery-client before_task_publish: ", exc_info=True) + + @signals.after_task_publish.connect + def after_task_publish( + *args: Tuple[object, ...], + **kwargs: Dict[str, Any], + ) -> None: + try: + span = client_span.get() + if span.is_recording(): + span.end() + client_span.set(None) + if "token" in client_token: + context.detach(client_token.pop("token", None)) + + except Exception: + logger.debug("celery-client after_task_publish: ", exc_info=True) + + logger.debug("Instrumenting celery") +except ImportError: + pass diff --git a/src/instana/instrumentation/celery/catalog.py b/src/instana/instrumentation/celery/catalog.py deleted file mode 100644 index 2ba395ac..00000000 --- a/src/instana/instrumentation/celery/catalog.py +++ /dev/null @@ -1,76 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -""" -Celery Signals are disjointed and don't allow us to pass the scope object along -with the Job message so we instead store all scopes in a dictionary on the -registered Task job. - -These methods allow pushing and pop'ing of scopes on Task objects. - -WeakValueDictionary allows for lost scopes to be garbage collected. -""" - -from weakref import WeakValueDictionary - - -def get_task_id(headers, body): - """ - Across Celery versions, the task id can exist in a couple of places. - """ - id = headers.get('id', None) - if id is None: - id = body.get('id', None) - return id - - -def task_catalog_push(task, task_id, scope, is_consumer): - """ - Push (adds) an object to the task catalog - @param task: The Celery Task - @param task_id: The Celery Task ID - @param is_consumer: Boolean - @return: scope - """ - catalog = None - if not hasattr(task, '_instana_scopes'): - catalog = WeakValueDictionary() - setattr(task, '_instana_scopes', catalog) - else: - catalog = getattr(task, '_instana_scopes') - - key = (task_id, is_consumer) - catalog[key] = scope - - -def task_catalog_pop(task, task_id, is_consumer): - """ - Pop (removes) an object from the task catalog - @param task: The Celery Task - @param task_id: The Celery Task ID - @param is_consumer: Boolean - @return: scope - """ - catalog = getattr(task, '_instana_scopes', None) - if catalog is None: - return None - - key = (task_id, is_consumer) - return catalog.pop(key, None) - - -def task_catalog_get(task, task_id, is_consumer): - """ - Get an object from the task catalog - @param task: The Celery Task - @param task_id: The Celery Task ID - @param is_consumer: Boolean - @return: scope - """ - catalog = getattr(task, '_instana_scopes', None) - if catalog is None: - return None - - key = (task_id, is_consumer) - return catalog.get(key, None) - diff --git a/src/instana/instrumentation/celery/hooks.py b/src/instana/instrumentation/celery/hooks.py deleted file mode 100644 index eb2a180c..00000000 --- a/src/instana/instrumentation/celery/hooks.py +++ /dev/null @@ -1,162 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - - -import opentracing - -from ...log import logger -from ...singletons import tracer -from ...util.traceutils import get_active_tracer - -try: - import celery - from celery import registry, signals - from .catalog import task_catalog_get, task_catalog_pop, task_catalog_push, get_task_id - - from urllib import parse - - - def add_broker_tags(span, broker_url): - try: - url = parse.urlparse(broker_url) - - # Add safety for edge case where scheme may not be a string - url_scheme = str(url.scheme) - span.set_tag("scheme", url_scheme) - - if url.hostname is None: - span.set_tag("host", 'localhost') - else: - span.set_tag("host", url.hostname) - - if url.port is None: - # Set default port if not specified - if url_scheme == 'redis': - span.set_tag("port", "6379") - elif 'amqp' in url_scheme: - span.set_tag("port", "5672") - elif 'sqs' in url_scheme: - span.set_tag("port", "443") - else: - span.set_tag("port", str(url.port)) - except Exception: - logger.debug("Error parsing broker URL: %s" % broker_url, exc_info=True) - - - @signals.task_prerun.connect - def task_prerun(*args, **kwargs): - try: - ctx = None - task = kwargs.get('sender', None) - task_id = kwargs.get('task_id', None) - task = registry.tasks.get(task.name) - - headers = task.request.get('headers', {}) - if headers is not None: - ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, headers, disable_w3c_trace_context=True) - - scope = tracer.start_active_span("celery-worker", child_of=ctx) - scope.span.set_tag("task", task.name) - scope.span.set_tag("task_id", task_id) - add_broker_tags(scope.span, task.app.conf['broker_url']) - - # Store the scope on the task to eventually close it out on the "after" signal - task_catalog_push(task, task_id, scope, True) - except: - logger.debug("task_prerun: ", exc_info=True) - - - @signals.task_postrun.connect - def task_postrun(*args, **kwargs): - try: - task = kwargs.get('sender', None) - task_id = kwargs.get('task_id', None) - scope = task_catalog_pop(task, task_id, True) - if scope is not None: - scope.close() - except: - logger.debug("after_task_publish: ", exc_info=True) - - - @signals.task_failure.connect - def task_failure(*args, **kwargs): - try: - task_id = kwargs.get('task_id', None) - task = kwargs['sender'] - scope = task_catalog_get(task, task_id, True) - - if scope is not None: - scope.span.set_tag("success", False) - exc = kwargs.get('exception', None) - if exc is None: - scope.span.mark_as_errored() - else: - scope.span.log_exception(kwargs['exception']) - except: - logger.debug("task_failure: ", exc_info=True) - - - @signals.task_retry.connect - def task_retry(*args, **kwargs): - try: - task_id = kwargs.get('task_id', None) - task = kwargs['sender'] - scope = task_catalog_get(task, task_id, True) - - if scope is not None: - reason = kwargs.get('reason', None) - if reason is not None: - scope.span.set_tag('retry-reason', reason) - except: - logger.debug("task_failure: ", exc_info=True) - - - @signals.before_task_publish.connect - def before_task_publish(*args, **kwargs): - try: - active_tracer = get_active_tracer() - if active_tracer is not None: - body = kwargs['body'] - headers = kwargs['headers'] - task_name = kwargs['sender'] - task = registry.tasks.get(task_name) - task_id = get_task_id(headers, body) - - scope = active_tracer.start_active_span("celery-client", child_of=active_tracer.active_span) - scope.span.set_tag("task", task_name) - scope.span.set_tag("task_id", task_id) - add_broker_tags(scope.span, task.app.conf['broker_url']) - - # Context propagation - context_headers = {} - active_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, context_headers, - disable_w3c_trace_context=True) - - # Fix for broken header propagation - # https://github.com/celery/celery/issues/4875 - task_headers = kwargs.get('headers') or {} - task_headers.setdefault('headers', {}) - task_headers['headers'].update(context_headers) - kwargs['headers'] = task_headers - - # Store the scope on the task to eventually close it out on the "after" signal - task_catalog_push(task, task_id, scope, False) - except: - logger.debug("before_task_publish: ", exc_info=True) - - - @signals.after_task_publish.connect - def after_task_publish(*args, **kwargs): - try: - task_id = get_task_id(kwargs['headers'], kwargs['body']) - task = registry.tasks.get(kwargs['sender']) - scope = task_catalog_pop(task, task_id, False) - if scope is not None: - scope.close() - except: - logger.debug("after_task_publish: ", exc_info=True) - - - logger.debug("Instrumenting celery") -except ImportError: - pass diff --git a/src/instana/instrumentation/couchbase_inst.py b/src/instana/instrumentation/couchbase_inst.py index f65c639a..d9678230 100644 --- a/src/instana/instrumentation/couchbase_inst.py +++ b/src/instana/instrumentation/couchbase_inst.py @@ -6,90 +6,144 @@ https://docs.couchbase.com/python-sdk/2.5/start-using-sdk.html """ -import wrapt - -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off - try: import couchbase + from instana.log import logger - 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" and couchbase.__version__ < "3.0.0") + ): logger.debug("Instana supports 2.3.4 <= couchbase_versions < 3.0.0. Skipping.") raise ImportError + from couchbase.bucket import Bucket from couchbase.n1ql import N1QLQuery - # 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', + from typing import Any, Callable, Dict, Tuple, Union - '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'] + import wrapt - def capture_kvs(scope, instance, query_arg, op): + from instana.span.span import InstanaSpan + from instana.util.traceutils import get_tracer_tuple, tracing_is_off + + # 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 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/google/cloud/pubsub.py b/src/instana/instrumentation/google/cloud/pubsub.py index 712ec515..fe4b5424 100644 --- a/src/instana/instrumentation/google/cloud/pubsub.py +++ b/src/instana/instrumentation/google/cloud/pubsub.py @@ -2,38 +2,50 @@ # (c) Copyright Instana Inc. 2021 -import json +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple + import wrapt -from opentracing import Format -from ....log import logger -from ....singletons import tracer -from ....util.traceutils import get_tracer_tuple, tracing_is_off +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: + from instana.span.span import InstanaSpan try: from google.cloud import pubsub_v1 - - def _set_publisher_tags(span, topic_path): - span.set_tag('gcps.op', 'publish') + def _set_publisher_attributes( + span: "InstanaSpan", + topic_path: str, + ) -> None: + span.set_attribute("gcps.op", "publish") # Fully qualified identifier is in the form of # `projects/{project_id}/topic/{topic_name}` - project_id, topic_name = topic_path.split('/')[1::2] - span.set_tag('gcps.projid', project_id) - span.set_tag('gcps.top', topic_name) - - - def _set_consumer_tags(span, subscription_path): - span.set_tag('gcps.op', 'consume') + project_id, topic_name = topic_path.split("/")[1::2] + span.set_attribute("gcps.projid", project_id) + span.set_attribute("gcps.top", topic_name) + + def _set_consumer_attributes( + span: "InstanaSpan", + subscription_path: str, + ) -> None: + span.set_attribute("gcps.op", "consume") # Fully qualified identifier is in the form of # `projects/{project_id}/subscriptions/{subscription_name}` - project_id, subscription_id = subscription_path.split('/')[1::2] - span.set_tag('gcps.projid', project_id) - span.set_tag('gcps.sub', subscription_id) - - - @wrapt.patch_function_wrapper('google.cloud.pubsub_v1', 'PublisherClient.publish') - def publish_with_instana(wrapped, instance, args, kwargs): + project_id, subscription_id = subscription_path.split("/")[1::2] + span.set_attribute("gcps.projid", project_id) + span.set_attribute("gcps.sub", subscription_id) + + @wrapt.patch_function_wrapper("google.cloud.pubsub_v1", "PublisherClient.publish") + def publish_with_instana( + wrapped: Callable[..., object], + instance: pubsub_v1.PublisherClient, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: """References: - PublisherClient.publish(topic_path, messages, metadata) """ @@ -42,29 +54,43 @@ def publish_with_instana(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs) tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span('gcps-producer', child_of=parent_span) as scope: + with tracer.start_as_current_span( + "gcps-producer", span_context=parent_context + ) as span: # trace continuity, inject to the span context - headers = dict() - tracer.inject(scope.span.context, Format.TEXT_MAP, headers, disable_w3c_trace_context=True) + headers = {} + tracer.inject( + span.context, + Format.TEXT_MAP, + headers, + disable_w3c_trace_context=True, + ) + + headers = {key: str(value) for key, value in headers.items()} # update the metadata dict with instana trace attributes kwargs.update(headers) - _set_publisher_tags(scope.span, topic_path=args[0]) + _set_publisher_attributes(span, topic_path=args[0]) 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 - - @wrapt.patch_function_wrapper('google.cloud.pubsub_v1', 'SubscriberClient.subscribe') - def subscribe_with_instana(wrapped, instance, args, kwargs): - + @wrapt.patch_function_wrapper( + "google.cloud.pubsub_v1", "SubscriberClient.subscribe" + ) + def subscribe_with_instana( + wrapped: Callable[..., object], + instance: pubsub_v1.SubscriberClient, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: """References: - SubscriberClient.subscribe(subscription_path, callback) - callback(message) is called from the subscription future @@ -72,29 +98,31 @@ def subscribe_with_instana(wrapped, instance, args, kwargs): def callback_with_instana(message): if message.attributes: - parent_span = tracer.extract(Format.TEXT_MAP, message.attributes, disable_w3c_trace_context=True) + parent_context = tracer.extract( + Format.TEXT_MAP, message.attributes, disable_w3c_trace_context=True + ) else: - parent_span = None + parent_context = None - with tracer.start_active_span('gcps-consumer', child_of=parent_span) as scope: - _set_consumer_tags(scope.span, subscription_path=args[0]) + with tracer.start_as_current_span( + "gcps-consumer", span_context=parent_context + ) as span: + _set_consumer_attributes(span, subscription_path=args[0]) try: callback(message) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) # Handle callback appropriately from args or kwargs - if 'callback' in kwargs: - callback = kwargs.get('callback') - kwargs['callback'] = callback_with_instana + if "callback" in kwargs: + callback = kwargs.get("callback") + kwargs["callback"] = callback_with_instana return wrapped(*args, **kwargs) else: subscription, callback, *args = args args = (subscription, callback_with_instana, *args) return wrapped(*args, **kwargs) - - logger.debug('Instrumenting Google Cloud Pub/Sub') + logger.debug("Instrumenting Google Cloud Pub/Sub") except ImportError: pass diff --git a/src/instana/instrumentation/google/cloud/storage.py b/src/instana/instrumentation/google/cloud/storage.py index 45f6607f..a1ccb6d9 100644 --- a/src/instana/instrumentation/google/cloud/storage.py +++ b/src/instana/instrumentation/google/cloud/storage.py @@ -5,16 +5,19 @@ import wrapt import re -from ....log import logger -from .collectors import _storage_api -from ....util.traceutils import get_tracer_tuple, tracing_is_off +from typing import Any, Callable, Dict, Tuple, Union +from instana.log import logger +from instana.instrumentation.google.cloud.collectors import _storage_api +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: from google.cloud import storage - logger.debug('Instrumenting google-cloud-storage') + logger.debug("Instrumenting google-cloud-storage") - def _collect_tags(api_request): + def _collect_attributes( + api_request: Dict[str, Any], + ) -> Dict[str, Any]: """ Extract span tags from Google Cloud Storage API request. Returns None if the request is not supported. @@ -22,21 +25,21 @@ def _collect_tags(api_request): :param: dict :return: dict or None """ - method, path = api_request.get('method', None), api_request.get('path', None) + method, path = api_request.get("method", None), api_request.get("path", None) if method not in _storage_api: return try: - params = api_request.get('query_params', {}) - data = api_request.get('data', {}) + params = api_request.get("query_params", {}) + data = api_request.get("data", {}) if path in _storage_api[method]: # check is any of string keys matches the path exactly return _storage_api[method][path](params, data) else: # look for a regex that matches the string - for (matcher, collect) in _storage_api[method].items(): + for matcher, collect in _storage_api[method].items(): if not isinstance(matcher, re.Pattern): continue @@ -46,108 +49,139 @@ def _collect_tags(api_request): return collect(params, data, m) except Exception: - logger.debug("instana.instrumentation.google.cloud.storage._collect_tags: ", exc_info=True) - - def execute_with_instana(wrapped, instance, args, kwargs): + logger.debug( + "instana.instrumentation.google.cloud.storage._collect_attributes: ", + exc_info=True, + ) + + def execute_with_instana( + wrapped: Callable[..., object], + instance: Union[storage.Batch, storage._http.Connection], + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: # batch requests are traced with finish_batch_with_instana() # also return early if we're not tracing if isinstance(instance, storage.Batch) or tracing_is_off(): return wrapped(*args, **kwargs) tracer, parent_span, _ = get_tracer_tuple() - tags = _collect_tags(kwargs) - - # don't trace if the call is not instrumented - if tags is None: - logger.debug('uninstrumented Google Cloud Storage API request: %s' % kwargs) - return wrapped(*args, **kwargs) - - with tracer.start_active_span('gcs', child_of=parent_span) as scope: - for (k, v) in tags.items(): - scope.span.set_tag(k, v) + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span("gcs", span_context=parent_context) as span: try: + attributes = _collect_attributes(kwargs) + + # don't trace if the call is not instrumented + if attributes is None: + logger.debug( + f"uninstrumented Google Cloud Storage API request: {kwargs}" + ) + return wrapped(*args, **kwargs) + span.set_attributes(attributes) kv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return kv - def download_with_instana(wrapped, instance, args, kwargs): + def download_with_instana( + wrapped: Callable[..., object], + instance: storage.Blob, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: # return early if we're not tracing if tracing_is_off(): return wrapped(*args, **kwargs) tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span('gcs', child_of=parent_span) as scope: - scope.span.set_tag('gcs.op', 'objects.get') - scope.span.set_tag('gcs.bucket', instance.bucket.name) - scope.span.set_tag('gcs.object', instance.name) + with tracer.start_as_current_span("gcs", span_context=parent_context) as span: + span.set_attribute("gcs.op", "objects.get") + span.set_attribute("gcs.bucket", instance.bucket.name) + span.set_attribute("gcs.object", instance.name) - start = len(args) > 4 and args[4] or kwargs.get('start', None) + start = len(args) > 4 and args[4] or kwargs.get("start", None) if start is None: - start = '' + start = "" - end = len(args) > 5 and args[5] or kwargs.get('end', None) + end = len(args) > 5 and args[5] or kwargs.get("end", None) if end is None: - end = '' + end = "" - if start != '' or end != '': - scope.span.set_tag('gcs.range', '-'.join((start, end))) + if start != "" or end != "": + span.set_attribute("gcs.range", "-".join((start, end))) try: kv = wrapped(*args, **kwargs) except Exception as e: - scope.span.log_exception(e) - raise + span.record_exception(e) else: return kv - def upload_with_instana(wrapped, instance, args, kwargs): + def upload_with_instana( + wrapped: Callable[..., object], + instance: storage.Blob, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: # return early if we're not tracing if tracing_is_off(): return wrapped(*args, **kwargs) tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span('gcs', child_of=parent_span) as scope: - scope.span.set_tag('gcs.op', 'objects.insert') - scope.span.set_tag('gcs.bucket', instance.bucket.name) - scope.span.set_tag('gcs.object', instance.name) + with tracer.start_as_current_span("gcs", span_context=parent_context) as span: + span.set_attribute("gcs.op", "objects.insert") + span.set_attribute("gcs.bucket", instance.bucket.name) + span.set_attribute("gcs.object", instance.name) try: kv = wrapped(*args, **kwargs) except Exception as e: - scope.span.log_exception(e) - raise + span.record_exception(e) else: return kv - def finish_batch_with_instana(wrapped, instance, args, kwargs): + def finish_batch_with_instana( + wrapped: Callable[..., object], + instance: storage.Batch, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: # return early if we're not tracing if tracing_is_off(): return wrapped(*args, **kwargs) tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span('gcs', child_of=parent_span) as scope: - scope.span.set_tag('gcs.op', 'batch') - scope.span.set_tag('gcs.projectId', instance._client.project) - scope.span.set_tag('gcs.numberOfOperations', len(instance._requests)) + with tracer.start_as_current_span("gcs", span_context=parent_context) as span: + span.set_attribute("gcs.op", "batch") + span.set_attribute("gcs.projectId", instance._client.project) + span.set_attribute("gcs.numberOfOperations", len(instance._requests)) try: kv = wrapped(*args, **kwargs) except Exception as e: - scope.span.log_exception(e) - raise + span.record_exception(e) else: return kv - wrapt.wrap_function_wrapper('google.cloud.storage._http', 'Connection.api_request', execute_with_instana) - wrapt.wrap_function_wrapper('google.cloud.storage.blob', 'Blob._do_download', download_with_instana) - wrapt.wrap_function_wrapper('google.cloud.storage.blob', 'Blob._do_upload', upload_with_instana) - wrapt.wrap_function_wrapper('google.cloud.storage.batch', 'Batch.finish', finish_batch_with_instana) + wrapt.wrap_function_wrapper( + "google.cloud.storage._http", "Connection.api_request", execute_with_instana + ) + wrapt.wrap_function_wrapper( + "google.cloud.storage.blob", "Blob._do_download", download_with_instana + ) + wrapt.wrap_function_wrapper( + "google.cloud.storage.blob", "Blob._do_upload", upload_with_instana + ) + wrapt.wrap_function_wrapper( + "google.cloud.storage.batch", "Batch.finish", finish_batch_with_instana + ) except ImportError: pass diff --git a/src/instana/instrumentation/grpcio.py b/src/instana/instrumentation/grpcio.py index 18b799b7..ec73faa0 100644 --- a/src/instana/instrumentation/grpcio.py +++ b/src/instana/instrumentation/grpcio.py @@ -1,27 +1,32 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 - -import wrapt -import opentracing - -from ..log import logger -from ..singletons import tracer - try: import grpc - from grpc._channel import _UnaryUnaryMultiCallable, _StreamUnaryMultiCallable, \ - _UnaryStreamMultiCallable, _StreamStreamMultiCallable - - SUPPORTED_TYPES = [_UnaryUnaryMultiCallable, - _StreamUnaryMultiCallable, - _UnaryStreamMultiCallable, - _StreamStreamMultiCallable] - - - def collect_tags(span, instance, argv, kwargs): + from grpc._channel import ( + _UnaryUnaryMultiCallable, + _StreamUnaryMultiCallable, + _UnaryStreamMultiCallable, + _StreamStreamMultiCallable, + ) + + import wrapt + + from instana.log import logger + from instana.singletons import tracer + from instana.propagators.format import Format + from instana.span.span import get_current_span + + SUPPORTED_TYPES = [ + _UnaryUnaryMultiCallable, + _StreamUnaryMultiCallable, + _UnaryStreamMultiCallable, + _StreamStreamMultiCallable, + ] + + def collect_attributes(span, instance, argv, kwargs): try: - span.set_tag('rpc.flavor', 'grpc') + span.set_attribute("rpc.flavor", "grpc") if type(instance) in SUPPORTED_TYPES: method = instance._method.decode() @@ -33,228 +38,279 @@ def collect_tags(span, instance, argv, kwargs): method = argv[2][2][1]._method.decode() target = argv[2][2][1]._channel.target().decode() - span.set_tag('rpc.call', method) + span.set_attribute("rpc.call", method) - if ':///' in target: - _, target, *_ = target.split(':///') - parts = target.split(':') + if ":///" in target: + _, target, *_ = target.split(":///") + parts = target.split(":") if len(parts) == 2: - span.set_tag('rpc.host', parts[0]) - span.set_tag('rpc.port', parts[1]) - except: - logger.debug("grpc.collect_tags non-fatal error", exc_info=True) + span.set_attribute("rpc.host", parts[0]) + span.set_attribute("rpc.port", parts[1]) + except Exception: + logger.debug("grpc.collect_attributes non-fatal error", exc_info=True) return span - - @wrapt.patch_function_wrapper('grpc._channel', '_UnaryUnaryMultiCallable.with_call') + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.with_call") def unary_unary_with_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'unary') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "unary") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_UnaryUnaryMultiCallable.future') + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.future") def unary_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'unary') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "unary") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_UnaryUnaryMultiCallable.__call__') + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable.__call__") def unary_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context, record_exception=False + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'unary') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "unary") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_StreamUnaryMultiCallable.__call__') + @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.__call__") def stream_unary_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'stream') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "stream") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_StreamUnaryMultiCallable.with_call') + @wrapt.patch_function_wrapper( + "grpc._channel", "_StreamUnaryMultiCallable.with_call" + ) def stream_unary_with_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'stream') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "stream") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_StreamUnaryMultiCallable.future') + @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable.future") def stream_unary_future_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'stream') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "stream") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_UnaryStreamMultiCallable.__call__') + @wrapt.patch_function_wrapper("grpc._channel", "_UnaryStreamMultiCallable.__call__") def unary_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'stream') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "stream") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._channel', '_StreamStreamMultiCallable.__call__') + @wrapt.patch_function_wrapper( + "grpc._channel", "_StreamStreamMultiCallable.__call__" + ) def stream_stream_call_with_instana(wrapped, instance, argv, kwargs): - parent_span = tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if parent_span is None: + if not parent_span.is_recording(): return wrapped(*argv, **kwargs) - with tracer.start_active_span("rpc-client", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span( + "rpc-client", span_context=parent_context + ) as span: try: - if not "metadata" in kwargs: + if "metadata" not in kwargs: kwargs["metadata"] = [] - kwargs["metadata"] = tracer.inject(scope.span.context, opentracing.Format.BINARY, kwargs['metadata'], - disable_w3c_trace_context=True) - collect_tags(scope.span, instance, argv, kwargs) - scope.span.set_tag('rpc.call_type', 'stream') + kwargs["metadata"] = tracer.inject( + span.context, + Format.BINARY, + kwargs["metadata"], + disable_w3c_trace_context=True, + ) + collect_attributes(span, instance, argv, kwargs) + span.set_attribute("rpc.call_type", "stream") rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - @wrapt.patch_function_wrapper('grpc._server', '_call_behavior') + @wrapt.patch_function_wrapper("grpc._server", "_call_behavior") def call_behavior_with_instana(wrapped, instance, argv, kwargs): # Prep any incoming context headers metadata = argv[0].invocation_metadata @@ -262,19 +318,19 @@ def call_behavior_with_instana(wrapped, instance, argv, kwargs): for c in metadata: metadata_dict[c.key] = c.value - ctx = tracer.extract(opentracing.Format.BINARY, metadata_dict, disable_w3c_trace_context=True) + ctx = tracer.extract( + Format.BINARY, metadata_dict, disable_w3c_trace_context=True + ) - with tracer.start_active_span("rpc-server", child_of=ctx) as scope: + with tracer.start_as_current_span("rpc-server", span_context=ctx) as span: try: - collect_tags(scope.span, instance, argv, kwargs) + collect_attributes(span, instance, argv, kwargs) rv = wrapped(*argv, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - logger.debug("Instrumenting grpcio") except ImportError: pass 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..621bca26 100644 --- a/src/instana/instrumentation/redis.py +++ b/src/instana/instrumentation/redis.py @@ -2,94 +2,114 @@ # (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) - raise + except Exception as exc: + span.record_exception(exc) 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..f0be6252 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -5,140 +5,130 @@ Instrumentation for Sanic https://sanicframework.org/en/ """ + try: import sanic + from instana.log import logger + + if not (hasattr(sanic, "__version__") and sanic.__version__ >= "19.9.0"): + logger.debug( + "Instana supports Sanic package versions 19.9.0 and newer. Skipping." + ) + raise ImportError + 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.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"): + if agent.options.extra_http_headers: + 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}" + ) + + 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/tornado/client.py b/src/instana/instrumentation/tornado/client.py index 24e37809..e937db68 100644 --- a/src/instana/instrumentation/tornado/client.py +++ b/src/instana/instrumentation/tornado/client.py @@ -1,34 +1,27 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 - -import opentracing -import wrapt -import functools - -from ...log import logger -from ...singletons import agent, setup_tornado_tracer, tornado_tracer -from ...util.secrets import strip_secrets_from_query - try: import tornado - # Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing - # scope managers which we will tackle soon. - # Limit Tornado version for the time being. - if not (hasattr(tornado, 'version') and tornado.version[0] < '6'): - logger.debug('Instana supports Tornado package versions < 6.0. Skipping.') - raise ImportError + import wrapt + import functools - setup_tornado_tracer() + from opentelemetry.semconv.trace import SpanAttributes + + 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 + from instana.span.span import get_current_span @wrapt.patch_function_wrapper('tornado.httpclient', 'AsyncHTTPClient.fetch') def fetch_with_instana(wrapped, instance, argv, kwargs): try: - parent_span = tornado_tracer.active_span + parent_span = get_current_span() # If we're not tracing, just return - if (parent_span is None) or (parent_span.operation_name == "tornado-client"): + if (not parent_span.is_recording()) or (parent_span.name == "tornado-client"): return wrapped(*argv, **kwargs) request = argv[0] @@ -45,41 +38,43 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): new_kwargs[param] = kwargs.pop(param) kwargs = new_kwargs - scope = tornado_tracer.start_active_span('tornado-client', child_of=parent_span) - tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, request.headers) + parent_context = parent_span.get_span_context() if parent_span else None + + span = tracer.start_span("tornado-client", span_context=parent_context) + tracer.inject(span.context, Format.HTTP_HEADERS, request.headers) # Query param scrubbing parts = request.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) + 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) future = wrapped(request, **kwargs) if future is not None: - cb = functools.partial(finish_tracing, scope=scope) + cb = functools.partial(finish_tracing, span=span) future.add_done_callback(cb) return future except Exception: - logger.debug("tornado fetch", exc_info=True) - raise + logger.debug("Tornado fetch_with_instana: ", exc_info=True) - def finish_tracing(future, scope): + def finish_tracing(future, span): try: response = future.result() - scope.span.set_tag("http.status_code", response.code) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.code) except tornado.httpclient.HTTPClientError as e: - scope.span.set_tag("http.status_code", e.code) - scope.span.log_exception(e) - raise + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, e.code) + span.record_exception(e) + logger.debug("Tornado finish_tracing HTTPClientError: ", exc_info=True) finally: - scope.close() + if span.is_recording(): + span.end() logger.debug("Instrumenting tornado client") diff --git a/src/instana/instrumentation/tornado/server.py b/src/instana/instrumentation/tornado/server.py index 8fe8822c..7c928500 100644 --- a/src/instana/instrumentation/tornado/server.py +++ b/src/instana/instrumentation/tornado/server.py @@ -2,26 +2,17 @@ # (c) Copyright Instana Inc. 2019 -import opentracing -import wrapt - -from ...log import logger -from ...singletons import agent, setup_tornado_tracer, tornado_tracer -from ...util.secrets import strip_secrets_from_query - try: import tornado - # Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing - # scope managers which we will tackle soon. - # Limit Tornado version for the time being. - if not (hasattr(tornado, 'version') and tornado.version[0] < '6'): - logger.debug('Instana supports Tornado package versions < 6.0. Skipping.') - raise ImportError + import wrapt - from opentracing.scope_managers.tornado import tracer_stack_context + from opentelemetry.semconv.trace import SpanAttributes - setup_tornado_tracer() + 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 def extract_custom_headers(span, headers): if not agent.options.extra_http_headers or not headers: @@ -29,7 +20,7 @@ def extract_custom_headers(span, headers): 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) @@ -38,36 +29,36 @@ def extract_custom_headers(span, headers): @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler._execute') def execute_with_instana(wrapped, instance, argv, kwargs): try: - with tracer_stack_context(): - ctx = None - if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__: - ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, - instance.request.headers.__dict__['_dict']) - scope = tornado_tracer.start_active_span('tornado-server', child_of=ctx) + span_context = None + if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__: + span_context = tracer.extract(Format.HTTP_HEADERS, + instance.request.headers.__dict__['_dict']) - # Query param scrubbing - if instance.request.query is not None and len(instance.request.query) > 0: - cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", cleaned_qp) + span = tracer.start_span("tornado-server", span_context=span_context) - url = "%s://%s%s" % (instance.request.protocol, instance.request.host, instance.request.path) - scope.span.set_tag("http.url", url) - scope.span.set_tag("http.method", instance.request.method) + # Query param scrubbing + if instance.request.query is not None and len(instance.request.query) > 0: + cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher, + agent.options.secrets_list) + span.set_attribute("http.params", cleaned_qp) + + url = f"{instance.request.protocol}://{instance.request.host}{instance.request.path}" + span.set_attribute(SpanAttributes.HTTP_URL, url) + span.set_attribute(SpanAttributes.HTTP_METHOD, instance.request.method) - scope.span.set_tag("handler", instance.__class__.__name__) + span.set_attribute("handler", instance.__class__.__name__) - # Request header tracking support - extract_custom_headers(scope.span, instance.request.headers) + # Request header tracking support + extract_custom_headers(span, instance.request.headers) - setattr(instance.request, "_instana", scope) + setattr(instance.request, "_instana", span) - # Set the context response headers now because tornado doesn't give us a better option to do so - # later for this request. - tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers) - instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id) + # Set the context response headers now because tornado doesn't give us a better option to do so + # later for this request. + tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers) + instance.set_header(name='Server-Timing', value=f"intid;desc={span.context.trace_id}") - return wrapped(*argv, **kwargs) + return wrapped(*argv, **kwargs) except Exception: logger.debug("tornado execute", exc_info=True) @@ -77,9 +68,9 @@ def set_default_headers_with_instana(wrapped, instance, argv, kwargs): if not hasattr(instance.request, '_instana'): return wrapped(*argv, **kwargs) - scope = instance.request._instana - tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers) - instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id) + span = instance.request._instana + tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers) + instance.set_header(name='Server-Timing', value=f"intid;desc={span.context.trace_id}") @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.on_finish') @@ -88,17 +79,19 @@ def on_finish_with_instana(wrapped, instance, argv, kwargs): if not hasattr(instance.request, '_instana'): return wrapped(*argv, **kwargs) - with instance.request._instana as scope: - # Response header tracking support - extract_custom_headers(scope.span, instance._headers) + span = instance.request._instana + # Response header tracking support + extract_custom_headers(span, instance._headers) - status_code = instance.get_status() + status_code = instance.get_status() - # Mark 500 responses as errored - if 500 <= status_code: - scope.span.mark_as_errored() + # Mark 500 responses as errored + if 500 <= status_code: + span.mark_as_errored() - scope.span.set_tag("http.status_code", status_code) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + if span.is_recording(): + span.end() return wrapped(*argv, **kwargs) except Exception: @@ -112,8 +105,8 @@ def log_exception_with_instana(wrapped, instance, argv, kwargs): return wrapped(*argv, **kwargs) if not isinstance(argv[1], tornado.web.HTTPError): - scope = instance.request._instana - scope.span.log_exception(argv[0]) + span = instance.request._instana + span.record_exception(argv[0]) return wrapped(*argv, **kwargs) except Exception: 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..f92074f0 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -2,17 +2,21 @@ # (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,6 +27,7 @@ # # For injection, we only support the standard format: # X-Instana-T +CarrierT = TypeVar("CarrierT", Dict, List, Tuple) class BasePropagator(object): @@ -66,12 +71,14 @@ def __init__(self): 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 +87,17 @@ 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 +106,28 @@ 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) + + return correlation_type, correlation_id - def _get_participating_trace_context(self, span_context): + 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: @@ -136,83 +147,119 @@ def _get_participating_trace_context(self, span_context): 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", "") 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: + ctx_level = self._get_ctx_level(level) + ctx_trace_id = trace_id if ctx_level > 0 else None + ctx_span_id = span_id if ctx_level > 0 else None + + 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 + + hex_trace_id = hex(trace_id)[2:] + if len(hex_trace_id) > 16: + ctx_long_trace_id = hex_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 = tp_trace_id + ctx_span_id = 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 = instana_ancestor.t + ctx_span_id = 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 if ctx_trace_id else INVALID_TRACE_ID, + span_id=ctx_span_id if ctx_span_id else INVALID_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 @@ -221,12 +268,14 @@ def extract_instana_headers(self, dc): 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) + # 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) + # 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) @@ -268,10 +317,12 @@ 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 @@ -290,9 +341,21 @@ def extract(self, carrier, disable_w3c_trace_context=False): 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..7f84f786 --- /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 + + +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..0166bf29 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -2,22 +2,25 @@ # (c) Copyright Instana Inc. 2018 import os +from typing import TYPE_CHECKING, Type -import opentracing +from opentelemetry import trace -from .autoprofile.profiler import Profiler -from .log import logger -from .tracer import InstanaTracer +from instana.recorder import StanRecorder +from instana.tracer import InstanaTracerProvider +from instana.autoprofile.profiler import Profiler + +if TYPE_CHECKING: + from instana.agent.base import BaseAgent + from instana.tracer import InstanaTracer 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,51 +34,33 @@ (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 agent = AWSLambdaAgent() - span_recorder = StanRecorder(agent) - elif env_is_aws_fargate: - from .agent.aws_fargate import AWSFargateAgent - from .recorder import StanRecorder - + from instana.agent.aws_fargate import AWSFargateAgent agent = AWSFargateAgent() - span_recorder = StanRecorder(agent) elif env_is_google_cloud_run: from instana.agent.google_cloud_run import GCRAgent - from instana.recorder import StanRecorder - agent = GCRAgent( service=k_service, configuration=k_configuration, revision=k_revision ) - span_recorder = StanRecorder(agent) elif env_is_aws_eks_fargate: - from .agent.aws_eks_fargate import EKSFargateAgent - from .recorder import StanRecorder - + from instana.agent.aws_eks_fargate import EKSFargateAgent agent = EKSFargateAgent() - span_recorder = StanRecorder(agent) else: - from .agent.host import HostAgent - from .recorder import StanRecorder - + from instana.agent.host import HostAgent agent = HostAgent() - span_recorder = StanRecorder(agent) profiler = Profiler(agent) + + +if agent: + span_recorder = StanRecorder(agent) -def get_agent(): +def get_agent() -> Type["BaseAgent"]: """ Retrieve the globally configured agent @return: The Instana Agent singleton @@ -84,7 +69,7 @@ def get_agent(): return agent -def set_agent(new_agent): +def set_agent(new_agent: Type["BaseAgent"]) -> None: """ Set the global agent for the Instana package. This is used for the test suite only currently. @@ -96,37 +81,18 @@ 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(): +def get_tracer() -> "InstanaTracer": """ Retrieve the globally configured tracer @return: Tracer @@ -135,7 +101,7 @@ def get_tracer(): return tracer -def set_tracer(new_tracer): +def set_tracer(new_tracer: "InstanaTracer") -> None: """ Set the global tracer for the Instana package. This is used for the test suite only currently. @@ -146,7 +112,7 @@ def set_tracer(new_tracer): tracer = new_tracer -def get_profiler(): +def get_profiler() -> Profiler: """ Retrieve the globally configured profiler @return: Profiler @@ -155,7 +121,7 @@ def get_profiler(): return profiler -def set_profiler(new_profiler): +def set_profiler(new_profiler: Profiler): """ Set the global profiler for the Instana package. This is used for the test suite only currently. diff --git a/src/instana/span.py b/src/instana/span.py deleted file mode 100644 index c9896453..00000000 --- a/src/instana/span.py +++ /dev/null @@ -1,510 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2017 - -""" -This module contains the classes that represents spans. - -InstanaSpan - the OpenTracing 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 -""" -import six - -from basictracer.span import BasicSpan -import opentracing.ext.tags as ot_tags - -from .log import logger -from .util import DictionaryOfStan - - -class InstanaSpan(BasicSpan): - stack = None - synthetic = False - - def mark_as_errored(self, tags=None): - """ - Mark this span as errored. - - @param tags: optional tags to add to the span - """ - try: - ec = self.tags.get('ec', 0) - self.set_tag('ec', ec + 1) - - if tags is not None and isinstance(tags, dict): - for key in tags: - self.set_tag(key, tags[key]) - except Exception: - logger.debug('span.mark_as_errored', exc_info=True) - - def assure_errored(self): - """ - Make sure that this span is marked as errored. - @return: None - """ - try: - ec = self.tags.get('ec', None) - if ec is None or ec == 0: - self.set_tag('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 - - -class BaseSpan(object): - sy = None - - def __str__(self): - return "BaseSpan(%s)" % self.__dict__.__str__() - - def __repr__(self): - return self.__dict__.__str__() - - def __init__(self, span, source, service_name, **kwargs): - # pylint: disable=invalid-name - self.t = span.context.trace_id - self.p = span.parent_id - self.s = span.context.span_id - self.ts = int(round(span.start_time * 1000)) - self.d = int(round(span.duration * 1000)) - self.f = source - self.ec = span.tags.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): - 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_tags(self, tags): - """ - This method will loop through a set of tags to validate each key and value. - - :param tags: dict of tags - :return: dict - a filtered set of tags - """ - filtered_tags = DictionaryOfStan() - for key in tags.keys(): - validated_key, validated_value = self._validate_tag(key, tags[key]) - if validated_key is not None and validated_value is not None: - filtered_tags[validated_key] = validated_value - return filtered_tags - - def _validate_tag(self, key, value): - """ - This method will assure that and are valid to set as a tag. - 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 - not valid and could not be converted into something useful - - :param key: The tag key - :param value: The tag value - :return: Tuple (key, value) - """ - validated_key = None - validated_value = None - - try: - # Tag 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_tag_value(value) - else: - logger.debug("(non-fatal) tag names must be strings. tag discarded for %s", type(key)) - except Exception: - logger.debug("instana.span._validate_tag: ", exc_info=True) - - return (validated_key, validated_value) - - def _convert_tag_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" - logger.debug(final_value, exc_info=True) - return None - return final_value - - -class SDKSpan(BaseSpan): - ENTRY_KIND = ["entry", "server", "consumer"] - EXIT_KIND = ["exit", "client", "producer"] - - def __init__(self, span, source, service_name, **kwargs): - # pylint: disable=invalid-name - super(SDKSpan, self).__init__(span, source, service_name, **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.operation_name - self.data["sdk"]["type"] = span_kind[0] - self.data["sdk"]["custom"]["tags"] = self._validate_tags(span.tags) - - 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 "arguments" in span.tags: - self.data['sdk']['arguments'] = span.tags["arguments"] - - if "return" in span.tags: - self.data['sdk']['return'] = span.tags["return"] - - if len(span.context.baggage) > 0: - self.data["baggage"] = span.context.baggage - - def get_span_kind(self, span): - """ - Will retrieve the `span.kind` tag 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 - :return: Tuple (String, Int) - """ - kind = ("intermediate", 3) - if "span.kind" in span.tags: - if span.tags["span.kind"] in self.ENTRY_KIND: - kind = ("entry", 1) - elif span.tags["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): - # pylint: disable=invalid-name - super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) - self.n = span.operation_name - self.k = 1 - - self.data["service"] = service_name - if span.operation_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: - self.k = 2 # exit - self._populate_exit_span_data(span) - elif span.operation_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' - - # Store any leftover tags in the custom section - if len(span.tags) > 0: - self.data["custom"]["tags"] = self._validate_tags(span.tags) - - def _populate_entry_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) - - elif span.operation_name == "aws.lambda.entry": - self.data["lambda"]["arn"] = span.tags.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) - - 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) - 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) - else: - logger.debug("SpanRecorder: Unknown local span: %s" % span.operation_name) - - def _populate_exit_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) - - elif span.operation_name == "boto3": - # boto3 also sends http tags - self._collect_http_tags(span) - - for tag in ['op', 'ep', 'reg', 'payload', 'error']: - value = span.tags.pop(tag, None) - if value is not None: - if tag == 'payload': - self.data["boto3"][tag] = self._validate_tags(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["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": - # 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) - 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: - custom_headers = [] - for key in span.tags: - 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) diff --git a/src/instana/instrumentation/celery/__init__.py b/src/instana/span/__init__.py similarity index 100% rename from src/instana/instrumentation/celery/__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..fbdb4c42 --- /dev/null +++ b/src/instana/span/base_span.py @@ -0,0 +1,117 @@ +# (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.s = span.context.span_id + 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..9a4be35b --- /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"]["tags"] = 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..ac14e61d 100644 --- a/src/instana/span_context.py +++ b/src/instana/span_context.py @@ -2,102 +2,129 @@ # (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 +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 450ca9b1..e40079d2 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.3" +VERSION = "3.0.0" diff --git a/src/instana/w3c_trace_context/traceparent.py b/src/instana/w3c_trace_context/traceparent.py index 3175c6cf..edfd7066 100644 --- a/src/instana/w3c_trace_context/traceparent.py +++ b/src/instana/w3c_trace_context/traceparent.py @@ -1,15 +1,25 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -from ..log import logger import re +from typing import Optional, Tuple + +from opentelemetry.trace.span import ( + format_span_id, + format_trace_id, +) + +from instana.log import logger +from instana.util.ids import header_to_id # See https://www.w3.org/TR/trace-context-2/#trace-flags for details on the bitmasks. SAMPLED_BITMASK = 0b1; class Traceparent: SPECIFICATION_VERSION = "00" - TRACEPARENT_REGEX = re.compile("^[0-9a-f][0-9a-e]-(?!0{32})([0-9a-f]{32})-(?!0{16})([0-9a-f]{16})-[0-9a-f]{2}") + TRACEPARENT_REGEX = re.compile( + "^[0-9a-f][0-9a-e]-(?!0{32})([0-9a-f]{32})-(?!0{16})([0-9a-f]{16})-[0-9a-f]{2}" + ) def validate(self, traceparent): """ @@ -21,11 +31,15 @@ def validate(self, traceparent): if self.TRACEPARENT_REGEX.match(traceparent): return traceparent except Exception: - logger.debug("traceparent does not follow version {} specification".format(self.SPECIFICATION_VERSION)) + logger.debug( + "traceparent does not follow version {} specification".format( + self.SPECIFICATION_VERSION + ) + ) return None @staticmethod - def get_traceparent_fields(traceparent): + def get_traceparent_fields(traceparent: str) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[bool]]: """ Parses the validated traceparent header into its fields and returns the fields :param traceparent: the original validated traceparent header @@ -34,8 +48,8 @@ def get_traceparent_fields(traceparent): try: traceparent_properties = traceparent.split("-") version = traceparent_properties[0] - trace_id = traceparent_properties[1] - parent_id = traceparent_properties[2] + trace_id = header_to_id(traceparent_properties[1]) + parent_id = header_to_id(traceparent_properties[2]) flags = int(traceparent_properties[3], 16) sampled_flag = (flags & SAMPLED_BITMASK) == SAMPLED_BITMASK return version, trace_id, parent_id, sampled_flag @@ -45,7 +59,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 @@ -55,8 +75,14 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): :param level: instana level, used to determine the value of sampled flag of the traceparent header :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) + if ( + traceparent is None + ): # modify the trace_id part only when it was not present at all + 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 +93,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') + 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}-{format_trace_id(trace_id)}-{format_span_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/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/apps/tornado_server/app.py b/tests/apps/tornado_server/app.py index 01b8859e..cf71c677 100755 --- a/tests/apps/tornado_server/app.py +++ b/tests/apps/tornado_server/app.py @@ -14,7 +14,7 @@ import asyncio -from ...helpers import testenv +from tests.helpers import testenv class Application(tornado.web.Application): diff --git a/tests/autoprofile/samplers/test_allocation_sampler.py b/tests/autoprofile/samplers/test_allocation_sampler.py index 93ba817a..efa66bea 100644 --- a/tests/autoprofile/samplers/test_allocation_sampler.py +++ b/tests/autoprofile/samplers/test_allocation_sampler.py @@ -1,48 +1,59 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time -import unittest import random import threading +import time +from typing import Generator, Optional + +import pytest from instana.autoprofile.profiler import Profiler -from instana.autoprofile.runtime import min_version, runtime_info +from instana.autoprofile.runtime import min_version, RuntimeInfo from instana.autoprofile.samplers.allocation_sampler import AllocationSampler -class AllocationSamplerTestCase(unittest.TestCase): - - def test_allocation_profile(self): - if runtime_info.OS_WIN or not min_version(3, 4): +class TestAllocationSampler: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Create a new Profiler. + self.profiler = Profiler(None) + self.profiler.start(disable_timers=True) + yield + # teardown + self.profiler.destroy() + + def test_allocation_profile(self) -> None: + if RuntimeInfo.OS_WIN or not min_version(3, 4): return - profiler = Profiler(None) - profiler.start(disable_timers=True) - sampler = AllocationSampler(profiler) + sampler = AllocationSampler(self.profiler) sampler.setup() sampler.reset() mem1 = [] - def mem_leak(n = 100000): + + def mem_leak(n: Optional[int] = 100000) -> None: mem2 = [] for i in range(0, n): mem1.append(random.randint(0, 1000)) mem2.append(random.randint(0, 1000)) - def mem_leak2(): + def mem_leak2() -> None: mem_leak() - def mem_leak3(): + def mem_leak3() -> None: mem_leak2() - def mem_leak4(): + def mem_leak4() -> None: mem_leak3() - def mem_leak5(): + def mem_leak5() -> None: mem_leak4() - def record(): + def record() -> None: sampler.start_sampler() time.sleep(2) sampler.stop_sampler() @@ -56,10 +67,6 @@ def record(): t.join() profile = sampler.build_profile(2000, 120000).to_dict() - #print(profile) - - self.assertTrue('test_allocation_sampler.py' in str(profile)) + assert "test_allocation_sampler.py" in str(profile) -if __name__ == '__main__': - unittest.main() diff --git a/tests/autoprofile/samplers/test_block_sampler.py b/tests/autoprofile/samplers/test_block_sampler.py index 71f11a83..449d9729 100644 --- a/tests/autoprofile/samplers/test_block_sampler.py +++ b/tests/autoprofile/samplers/test_block_sampler.py @@ -1,50 +1,57 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os -import time -import unittest -import random import threading +import time +from typing import Generator + +import pytest from instana.autoprofile.profiler import Profiler -from instana.autoprofile.runtime import runtime_info +from instana.autoprofile.runtime import RuntimeInfo from instana.autoprofile.samplers.block_sampler import BlockSampler -class BlockSamplerTestCase(unittest.TestCase): - def test_block_profile(self): - if runtime_info.OS_WIN: +class TestBlockSampler: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Create a new Profiler. + self.profiler = Profiler(None) + self.profiler.start(disable_timers=True) + yield + # teardown + self.profiler.destroy() + + def test_block_profile(self) -> None: + if RuntimeInfo.OS_WIN: return - profiler = Profiler(None) - profiler.start(disable_timers=True) - sampler = BlockSampler(profiler) + sampler = BlockSampler(self.profiler) sampler.setup() sampler.reset() lock = threading.Lock() event = threading.Event() - def lock_lock(): + def lock_lock() -> None: lock.acquire() time.sleep(0.5) lock.release() - def lock_wait(): + def lock_wait() -> None: lock.acquire() lock.release() - - def event_lock(): + def event_lock() -> None: time.sleep(0.5) event.set() - - def event_wait(): + def event_wait() -> None: event.wait() - def record(): + def record() -> None: sampler.start_sampler() time.sleep(2) sampler.stop_sampler() @@ -69,11 +76,7 @@ def record(): record_t.join() profile = sampler.build_profile(2000, 120000).to_dict() - #print(profile) - - self.assertTrue('lock_wait' in str(profile)) - self.assertTrue('event_wait' in str(profile)) - + # print(profile) -if __name__ == '__main__': - unittest.main() + assert "lock_wait" in str(profile) + assert "event_wait" in str(profile) diff --git a/tests/autoprofile/samplers/test_cpu_sampler.py b/tests/autoprofile/samplers/test_cpu_sampler.py index 92bd6c0f..f0581d12 100644 --- a/tests/autoprofile/samplers/test_cpu_sampler.py +++ b/tests/autoprofile/samplers/test_cpu_sampler.py @@ -2,30 +2,38 @@ # (c) Copyright Instana Inc. 2020 import time -import unittest -import random import threading -import sys -import traceback +from typing import Generator + +import pytest from instana.autoprofile.profiler import Profiler -from instana.autoprofile.runtime import runtime_info +from instana.autoprofile.runtime import RuntimeInfo from instana.autoprofile.samplers.cpu_sampler import CPUSampler -class CPUSamplerTestCase(unittest.TestCase): +class TestCPUSampler: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Create a new Profiler. + self.profiler = Profiler(None) + self.profiler.start(disable_timers=True) + yield + # teardown + self.profiler.destroy() + - def test_cpu_profile(self): - if runtime_info.OS_WIN: + def test_cpu_profile(self) -> None: + if RuntimeInfo.OS_WIN: return - profiler = Profiler(None) - profiler.start(disable_timers=True) - sampler = CPUSampler(profiler) + sampler = CPUSampler(self.profiler) sampler.setup() sampler.reset() - def record(): + def record() -> None: sampler.start_sampler() time.sleep(2) sampler.stop_sampler() @@ -33,19 +41,15 @@ def record(): record_t = threading.Thread(target=record) record_t.start() - def cpu_work_main_thread(): + def cpu_work_main_thread() -> None: for i in range(0, 1000000): text = "text1" + str(i) text = text + "text2" + cpu_work_main_thread() record_t.join() profile = sampler.build_profile(2000, 120000).to_dict() - #print(profile) - - self.assertTrue('cpu_work_main_thread' in str(profile)) - -if __name__ == '__main__': - unittest.main() + assert 'cpu_work_main_thread' in str(profile) diff --git a/tests/autoprofile/test_frame_cache.py b/tests/autoprofile/test_frame_cache.py index 2bbdf675..34291097 100644 --- a/tests/autoprofile/test_frame_cache.py +++ b/tests/autoprofile/test_frame_cache.py @@ -1,23 +1,28 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest -import sys -import threading import os +from typing import Generator + +import pytest from instana import autoprofile from instana.autoprofile.profiler import Profiler -class FrameCacheTestCase(unittest.TestCase): +class TestFrameCache: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Create a new Profiler. + self.profiler = Profiler(None) + self.profiler.start(disable_timers=True) + yield + # teardown + self.profiler.destroy() - def test_skip_stack(self): - profiler = Profiler(None) - profiler.start(disable_timers=True) + def test_skip_stack(self) -> None: test_profiler_file = os.path.realpath(autoprofile.__file__) - self.assertTrue(profiler.frame_cache.is_profiler_frame(test_profiler_file)) - -if __name__ == '__main__': - unittest.main() + assert self.profiler.frame_cache.is_profiler_frame(test_profiler_file) diff --git a/tests/autoprofile/test_profiler.py b/tests/autoprofile/test_profiler.py index 33d2e3a8..6d1867a0 100644 --- a/tests/autoprofile/test_profiler.py +++ b/tests/autoprofile/test_profiler.py @@ -1,38 +1,41 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest import threading +from typing import Generator -from instana.autoprofile.profiler import Profiler -from instana.autoprofile.runtime import runtime_info, min_version - - -# python3 -m unittest discover -v -s tests -p *_test.py - -class ProfilerTestCase(unittest.TestCase): +import pytest - def test_run_in_main_thread(self): - if runtime_info.OS_WIN: +from instana.autoprofile.profiler import Profiler +from instana.autoprofile.runtime import RuntimeInfo + + +class TestProfiler: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Create a new Profiler. + self.profiler = Profiler(None) + self.profiler.start(disable_timers=True) + yield + # teardown + self.profiler.destroy() + + def test_run_in_main_thread(self) -> None: + if RuntimeInfo.OS_WIN: return - profiler = Profiler(None) - profiler.start(disable_timers=True) - result = {} def _run(): - result['thread_id'] = threading.current_thread().ident + result["thread_id"] = threading.current_thread().ident def _thread(): - profiler.run_in_main_thread(_run) + self.profiler.run_in_main_thread(_run) t = threading.Thread(target=_thread) t.start() t.join() - self.assertEqual(result['thread_id'], threading.current_thread().ident) - - -if __name__ == '__main__': - unittest.main() + assert threading.current_thread().ident == result["thread_id"] diff --git a/tests/autoprofile/test_runtime.py b/tests/autoprofile/test_runtime.py index 348484d9..0d1cf356 100644 --- a/tests/autoprofile/test_runtime.py +++ b/tests/autoprofile/test_runtime.py @@ -1,24 +1,25 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest -import signal import os +import signal +from typing import TYPE_CHECKING -from instana.autoprofile.profiler import Profiler -from instana.autoprofile.runtime import runtime_info, register_signal +from instana.autoprofile.runtime import RuntimeInfo, register_signal +if TYPE_CHECKING: + from types import FrameType -class RuntimeTestCase(unittest.TestCase): - def test_register_signal(self): - if runtime_info.OS_WIN: +class TestRuntime: + def test_register_signal(self) -> None: + if RuntimeInfo.OS_WIN: return - result = {'handler': 0} + result = {"handler": 0} - def _handler(signum, frame): - result['handler'] += 1 + def _handler(signum: signal.Signals, frame: "FrameType") -> None: + result["handler"] += 1 register_signal(signal.SIGUSR1, _handler) @@ -27,23 +28,4 @@ def _handler(signum, frame): signal.signal(signal.SIGUSR1, signal.SIG_DFL) - self.assertEqual(result['handler'], 2) - - - '''def test_register_signal_default(self): - result = {'handler': 0} - - def _handler(signum, frame): - result['handler'] += 1 - - register_signal(signal.SIGUSR1, _handler, once = True) - - os.kill(os.getpid(), signal.SIGUSR1) - os.kill(os.getpid(), signal.SIGUSR1) - - self.assertEqual(result['handler'], 1)''' - - -if __name__ == '__main__': - unittest.main() - + assert result["handler"] == 2 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..07945259 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, ResultSet 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 isinstance(res, ResultSet) 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 isinstance(res, ResultSet) 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 isinstance(res, ResultSet) 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 isinstance(res, ResultSet) 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 isinstance(res, ResultSet) 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_google-cloud-pubsub.py b/tests/clients/test_google-cloud-pubsub.py index 48b57eda..cbbafe6f 100644 --- a/tests/clients/test_google-cloud-pubsub.py +++ b/tests/clients/test_google-cloud-pubsub.py @@ -4,30 +4,33 @@ import os import threading import time -import six -import unittest +from typing import Generator -from google.cloud.pubsub_v1 import PublisherClient, SubscriberClient +import pytest +import six from google.api_core.exceptions import AlreadyExists +from google.cloud.pubsub_v1 import PublisherClient, SubscriberClient from google.cloud.pubsub_v1.publisher import exceptions +from opentelemetry.trace import SpanKind + from instana.singletons import agent, tracer +from instana.span.span import get_current_span from tests.test_utils import _TraceContextMixin # Use PubSub Emulator exposed at :8085 os.environ["PUBSUB_EMULATOR_HOST"] = "localhost:8085" -class TestPubSubPublish(unittest.TestCase, _TraceContextMixin): - @classmethod - def setUpClass(cls): - cls.publisher = PublisherClient() +class TestPubSubPublish(_TraceContextMixin): + publisher = PublisherClient() - def setUp(self): - self.recorder = tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor self.recorder.clear_spans() - self.project_id = 'test-project' - self.topic_name = 'test-topic' + self.project_id = "test-project" + self.topic_name = "test-topic" # setup topic_path & topic self.topic_path = self.publisher.topic_path(self.project_id, self.topic_name) @@ -36,31 +39,32 @@ def setUp(self): except AlreadyExists: self.publisher.delete_topic(request={"topic": self.topic_path}) self.publisher.create_topic(request={"name": self.topic_path}) - - def tearDown(self): + yield self.publisher.delete_topic(request={"topic": self.topic_path}) agent.options.allow_exit_as_root = False - def test_publish(self): + def test_publish(self) -> None: # publish a single message - with tracer.start_active_span('test'): - future = self.publisher.publish(self.topic_path, - b'Test Message', - origin="instana") + with tracer.start_as_current_span("test"): + future = self.publisher.publish( + self.topic_path, b"Test Message", origin="instana" + ) time.sleep(2.0) # for sanity result = future.result() - self.assertIsInstance(result, six.string_types) + assert isinstance(result, six.string_types) spans = self.recorder.queued_spans() gcps_span, test_span = spans[0], spans[1] - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) - self.assertEqual('gcps', gcps_span.n) - self.assertEqual(2, gcps_span.k) # EXIT + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() + assert gcps_span.n == "gcps" + assert gcps_span.k is SpanKind.CLIENT - self.assertEqual('publish', gcps_span.data['gcps']['op']) - self.assertEqual(self.topic_name, gcps_span.data['gcps']['top']) + assert gcps_span.data["gcps"]["op"] == "publish" + assert self.topic_name == gcps_span.data["gcps"]["top"] # Trace Context Propagation self.assertTraceContextPropagated(test_span, gcps_span) @@ -68,57 +72,58 @@ def test_publish(self): # Error logging self.assertErrorLogging(spans) - def test_publish_as_root_exit_span(self): + def test_publish_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True # publish a single message - future = self.publisher.publish(self.topic_path, - b'Test Message', - origin="instana") + future = self.publisher.publish( + self.topic_path, b"Test Message", origin="instana" + ) time.sleep(2.0) # for sanity result = future.result() - self.assertIsInstance(result, six.string_types) + assert isinstance(result, six.string_types) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 gcps_span = spans[0] - self.assertIsNone(tracer.active_span) - self.assertEqual('gcps', gcps_span.n) - self.assertEqual(2, gcps_span.k) # EXIT + current_span = get_current_span() + assert not current_span.is_recording() + assert gcps_span.n == "gcps" + assert gcps_span.k is SpanKind.CLIENT - self.assertEqual('publish', gcps_span.data['gcps']['op']) - self.assertEqual(self.topic_name, gcps_span.data['gcps']['top']) + assert gcps_span.data["gcps"]["op"] == "publish" + assert self.topic_name == gcps_span.data["gcps"]["top"] # Error logging self.assertErrorLogging(spans) class AckCallback(object): - def __init__(self): + def __init__(self) -> None: self.calls = 0 self.lock = threading.Lock() - def __call__(self, message): + def __call__(self, message) -> None: message.ack() # Only increment the number of calls **after** finishing. with self.lock: self.calls += 1 -class TestPubSubSubscribe(unittest.TestCase, _TraceContextMixin): +class TestPubSubSubscribe(_TraceContextMixin): @classmethod - def setUpClass(cls): + def setup_class(cls) -> None: cls.publisher = PublisherClient() cls.subscriber = SubscriberClient() - def setUp(self): - - self.recorder = tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor self.recorder.clear_spans() - self.project_id = 'test-project' - self.topic_name = 'test-topic' - self.subscription_name = 'test-subscription' + self.project_id = "test-project" + self.topic_name = "test-topic" + self.subscription_name = "test-subscription" # setup topic_path & topic self.topic_path = self.publisher.topic_path(self.project_id, self.topic_name) @@ -130,29 +135,33 @@ def setUp(self): # setup subscription path & attach subscription self.subscription_path = self.subscriber.subscription_path( - self.project_id, self.subscription_name) + self.project_id, + self.subscription_name, + ) try: self.subscriber.create_subscription( request={"name": self.subscription_path, "topic": self.topic_path} ) except AlreadyExists: - self.subscriber.delete_subscription(request={"subscription": self.subscription_path}) + self.subscriber.delete_subscription( + request={"subscription": self.subscription_path} + ) self.subscriber.create_subscription( request={"name": self.subscription_path, "topic": self.topic_path} ) - - def tearDown(self): + yield self.publisher.delete_topic(request={"topic": self.topic_path}) - self.subscriber.delete_subscription(request={"subscription": self.subscription_path}) - - def test_subscribe(self): + self.subscriber.delete_subscription( + request={"subscription": self.subscription_path} + ) - with tracer.start_active_span('test'): + def test_subscribe(self) -> None: + with tracer.start_as_current_span("test"): # Publish a message - future = self.publisher.publish(self.topic_path, - b"Test Message to PubSub", - origin="instana") - self.assertIsInstance(future.result(), six.string_types) + future = self.publisher.publish( + self.topic_path, b"Test Message to PubSub", origin="instana" + ) + assert isinstance(future.result(), six.string_types) time.sleep(2.0) # for sanity @@ -171,15 +180,16 @@ def test_subscribe(self): consumer_span = spans[1] test_span = spans[2] - self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) - self.assertEqual('publish', producer_span.data['gcps']['op']) - self.assertEqual('consume', consumer_span.data['gcps']['op']) - self.assertEqual(self.topic_name, producer_span.data['gcps']['top']) - self.assertEqual(self.subscription_name, consumer_span.data['gcps']['sub']) + assert len(spans) == 3 + current_span = get_current_span() + assert not current_span.is_recording() + assert producer_span.data["gcps"]["op"] == "publish" + assert consumer_span.data["gcps"]["op"] == "consume" + assert self.topic_name == producer_span.data["gcps"]["top"] + assert self.subscription_name == consumer_span.data["gcps"]["sub"] - self.assertEqual(2, producer_span.k) # EXIT - self.assertEqual(1, consumer_span.k) # ENTRY + assert producer_span.k is SpanKind.CLIENT + assert consumer_span.k is SpanKind.SERVER # Trace Context Propagation self.assertTraceContextPropagated(producer_span, consumer_span) diff --git a/tests/clients/test_google-cloud-storage.py b/tests/clients/test_google-cloud-storage.py index d8762584..3a069acc 100644 --- a/tests/clients/test_google-cloud-storage.py +++ b/tests/clients/test_google-cloud-storage.py @@ -1,37 +1,35 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import sys -import unittest +from typing import Generator import json +import pytest import requests import io from instana.singletons import agent, tracer +from instana.span.span import get_current_span from tests.test_utils import _TraceContextMixin +from opentelemetry.trace import SpanKind from mock import patch, Mock from six.moves import http_client from google.cloud import storage -from google.api_core import iam +from google.api_core import iam, page_iterator from google.auth.credentials import AnonymousCredentials -class TestGoogleCloudStorage(unittest.TestCase, _TraceContextMixin): - def setUp(self): - self.recorder = tracer.recorder +class TestGoogleCloudStorage(_TraceContextMixin): + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + 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 - @unittest.skipIf( - sys.platform == "darwin", reason="Raises not Implemented exception in OSX" - ) @patch("requests.Session.request") - def test_buckets_list(self, mock_requests): + def test_buckets_list(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#buckets", "items": []}, status_code=http_client.OK, @@ -41,40 +39,32 @@ def test_buckets_list(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): buckets = client.list_buckets() - self.assertEqual( - 0, - self.recorder.queue_size(), - msg="span has been created before the actual request", - ) - - # trigger the iterator - for b in buckets: + for _ in buckets: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) + assert gcs_span.data["gcs"]["op"] == "buckets.list" + assert gcs_span.data["gcs"]["projectId"] == "test-project" - @unittest.skipIf( - sys.platform == "darwin", reason="Raises not Implemented exception in OSX" - ) @patch("requests.Session.request") - def test_buckets_list_as_root_exit_span(self, mock_requests): + def test_buckets_list_as_root_exit_span(self, mock_requests: Mock) -> None: agent.options.allow_exit_as_root = True mock_requests.return_value = self._mock_response( json_content={"kind": "storage#buckets", "items": []}, @@ -86,32 +76,27 @@ def test_buckets_list_as_root_exit_span(self, mock_requests): ) buckets = client.list_buckets() - self.assertEqual( - 0, - self.recorder.queue_size(), - msg="span has been created before the actual request", - ) - - # trigger the iterator - for b in buckets: + for _ in buckets: pass spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 1 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) + assert gcs_span.data["gcs"]["op"] == "buckets.list" + assert gcs_span.data["gcs"]["projectId"] == "test-project" @patch("requests.Session.request") - def test_buckets_insert(self, mock_requests): + def test_buckets_insert(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#bucket"}, status_code=http_client.OK ) @@ -120,29 +105,31 @@ def test_buckets_insert(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.create_bucket("test bucket") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.insert", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.insert" + assert gcs_span.data["gcs"]["projectId"] == "test-project" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_get(self, mock_requests): + def test_buckets_get(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#bucket"}, status_code=http_client.OK ) @@ -151,29 +138,31 @@ def test_buckets_get(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.get_bucket("test bucket") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, gcs_span.t) - self.assertEqual(test_span.s, gcs_span.p) + assert gcs_span.t == test_span.t + assert gcs_span.p == test_span.s - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.get", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.get" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_patch(self, mock_requests): + def test_buckets_patch(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#bucket"}, status_code=http_client.OK ) @@ -182,28 +171,30 @@ def test_buckets_patch(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").patch() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.patch", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.patch" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_update(self, mock_requests): + def test_buckets_update(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#bucket"}, status_code=http_client.OK ) @@ -212,28 +203,30 @@ def test_buckets_update(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").update() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.update", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.update" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_get_iam_policy(self, mock_requests): + def test_buckets_get_iam_policy(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#policy"}, status_code=http_client.OK ) @@ -242,28 +235,30 @@ def test_buckets_get_iam_policy(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").get_iam_policy() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.getIamPolicy", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.getIamPolicy" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_set_iam_policy(self, mock_requests): + def test_buckets_set_iam_policy(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#policy"}, status_code=http_client.OK ) @@ -272,28 +267,30 @@ def test_buckets_set_iam_policy(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").set_iam_policy(iam.Policy()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.setIamPolicy", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.setIamPolicy" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_test_iam_permissions(self, mock_requests): + def test_buckets_test_iam_permissions(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#testIamPermissionsResponse"}, status_code=http_client.OK, @@ -303,28 +300,30 @@ def test_buckets_test_iam_permissions(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").test_iam_permissions("test-permission") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.testIamPermissions", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.testIamPermissions" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_lock_retention_policy(self, mock_requests): + def test_buckets_lock_retention_policy(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={ "kind": "storage#bucket", @@ -341,56 +340,60 @@ def test_buckets_lock_retention_policy(self, mock_requests): bucket = client.bucket("test bucket") bucket.reload() - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): bucket.lock_retention_policy() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.lockRetentionPolicy", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.lockRetentionPolicy" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_buckets_delete(self, mock_requests): + def test_buckets_delete(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response() client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").delete() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("buckets.delete", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "buckets.delete" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_objects_compose(self, mock_requests): + def test_objects_compose(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -399,7 +402,7 @@ def test_objects_compose(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("dest object").compose( [ storage.blob.Blob("object 1", "test bucket"), @@ -409,28 +412,30 @@ def test_objects_compose(self, mock_requests): spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.compose", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["destinationBucket"]) - self.assertEqual("dest object", gcs_span.data["gcs"]["destinationObject"]) - self.assertEqual( - "test bucket/object 1,test bucket/object 2", - gcs_span.data["gcs"]["sourceObjects"], + assert gcs_span.data["gcs"]["op"] == "objects.compose" + assert gcs_span.data["gcs"]["destinationBucket"] == "test bucket" + assert gcs_span.data["gcs"]["destinationObject"] == "dest object" + assert ( + gcs_span.data["gcs"]["sourceObjects"] + == "test bucket/object 1,test bucket/object 2" ) @patch("requests.Session.request") - def test_objects_copy(self, mock_requests): + def test_objects_copy(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -440,7 +445,7 @@ def test_objects_copy(self, mock_requests): ) bucket = client.bucket("src bucket") - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): bucket.copy_blob( bucket.blob("src object"), client.bucket("dest bucket"), @@ -449,55 +454,59 @@ def test_objects_copy(self, mock_requests): spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.copy", gcs_span.data["gcs"]["op"]) - self.assertEqual("dest bucket", gcs_span.data["gcs"]["destinationBucket"]) - self.assertEqual("dest object", gcs_span.data["gcs"]["destinationObject"]) - self.assertEqual("src bucket", gcs_span.data["gcs"]["sourceBucket"]) - self.assertEqual("src object", gcs_span.data["gcs"]["sourceObject"]) + assert gcs_span.data["gcs"]["op"] == "objects.copy" + assert gcs_span.data["gcs"]["destinationBucket"] == "dest bucket" + assert gcs_span.data["gcs"]["destinationObject"] == "dest object" + assert gcs_span.data["gcs"]["sourceBucket"] == "src bucket" + assert gcs_span.data["gcs"]["sourceObject"] == "src object" @patch("requests.Session.request") - def test_objects_delete(self, mock_requests): + def test_objects_delete(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response() client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").delete() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.delete", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.delete" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_objects_attrs(self, mock_requests): + def test_objects_attrs(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -506,29 +515,31 @@ def test_objects_attrs(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").exists() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.attrs", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.attrs" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_objects_get(self, mock_requests): + def test_objects_get(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( content=b"CONTENT", status_code=http_client.OK ) @@ -537,31 +548,33 @@ def test_objects_get(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").download_to_file( io.BytesIO(), raw_download=True ) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.get", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.get" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_objects_insert(self, mock_requests): + def test_objects_insert(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -570,34 +583,33 @@ def test_objects_insert(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").upload_from_string( "CONTENT" ) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.insert", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.insert" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" - @unittest.skipIf( - sys.platform == "darwin", reason="Raises not Implemented exception in OSX" - ) @patch("requests.Session.request") - def test_objects_list(self, mock_requests): + def test_objects_list(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -606,36 +618,33 @@ def test_objects_list(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): blobs = client.bucket("test bucket").list_blobs() - self.assertEqual( - 0, - self.recorder.queue_size(), - msg="span has been created before the actual request", - ) - for b in blobs: + for _ in blobs: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "objects.list" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_objects_patch(self, mock_requests): + def test_objects_patch(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -644,29 +653,31 @@ def test_objects_patch(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").patch() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.patch", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.patch" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_objects_rewrite(self, mock_requests): + def test_objects_rewrite(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={ "kind": "storage#rewriteResponse", @@ -682,33 +693,35 @@ def test_objects_rewrite(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("dest bucket").blob("dest object").rewrite( client.bucket("src bucket").blob("src object") ) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.rewrite", gcs_span.data["gcs"]["op"]) - self.assertEqual("dest bucket", gcs_span.data["gcs"]["destinationBucket"]) - self.assertEqual("dest object", gcs_span.data["gcs"]["destinationObject"]) - self.assertEqual("src bucket", gcs_span.data["gcs"]["sourceBucket"]) - self.assertEqual("src object", gcs_span.data["gcs"]["sourceObject"]) + assert gcs_span.data["gcs"]["op"] == "objects.rewrite" + assert gcs_span.data["gcs"]["destinationBucket"] == "dest bucket" + assert gcs_span.data["gcs"]["destinationObject"] == "dest object" + assert gcs_span.data["gcs"]["sourceBucket"] == "src bucket" + assert gcs_span.data["gcs"]["sourceObject"] == "src object" @patch("requests.Session.request") - def test_objects_update(self, mock_requests): + def test_objects_update(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#object"}, status_code=http_client.OK ) @@ -717,29 +730,31 @@ def test_objects_update(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").update() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objects.update", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objects.update" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_default_acls_list(self, mock_requests): + def test_default_acls_list(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#objectAccessControls", "items": []}, status_code=http_client.OK, @@ -749,28 +764,30 @@ def test_default_acls_list(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").default_object_acl.get_entities() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("defaultAcls.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) + assert gcs_span.data["gcs"]["op"] == "defaultAcls.list" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" @patch("requests.Session.request") - def test_object_acls_list(self, mock_requests): + def test_object_acls_list(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#objectAccessControls", "items": []}, status_code=http_client.OK, @@ -780,29 +797,31 @@ def test_object_acls_list(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.bucket("test bucket").blob("test object").acl.get_entities() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("objectAcls.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test bucket", gcs_span.data["gcs"]["bucket"]) - self.assertEqual("test object", gcs_span.data["gcs"]["object"]) + assert gcs_span.data["gcs"]["op"] == "objectAcls.list" + assert gcs_span.data["gcs"]["bucket"] == "test bucket" + assert gcs_span.data["gcs"]["object"] == "test object" @patch("requests.Session.request") - def test_object_hmac_keys_create(self, mock_requests): + def test_object_hmac_keys_create(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#hmacKey", "metadata": {}, "secret": ""}, status_code=http_client.OK, @@ -812,59 +831,63 @@ def test_object_hmac_keys_create(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.create_hmac_key("test@example.com") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("hmacKeys.create", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) + assert gcs_span.data["gcs"]["op"] == "hmacKeys.create" + assert gcs_span.data["gcs"]["projectId"] == "test-project" @patch("requests.Session.request") - def test_object_hmac_keys_delete(self, mock_requests): + def test_object_hmac_keys_delete(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response() client = self._client( credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): key = storage.hmac_key.HMACKeyMetadata(client, access_id="test key") key.state = storage.hmac_key.HMACKeyMetadata.INACTIVE_STATE key.delete() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("hmacKeys.delete", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) - self.assertEqual("test key", gcs_span.data["gcs"]["accessId"]) + assert gcs_span.data["gcs"]["op"] == "hmacKeys.delete" + assert gcs_span.data["gcs"]["projectId"] == "test-project" + assert gcs_span.data["gcs"]["accessId"] == "test key" @patch("requests.Session.request") - def test_object_hmac_keys_get(self, mock_requests): + def test_object_hmac_keys_get(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#hmacKey", "metadata": {}, "secret": ""}, status_code=http_client.OK, @@ -874,32 +897,31 @@ def test_object_hmac_keys_get(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): storage.hmac_key.HMACKeyMetadata(client, access_id="test key").exists() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("hmacKeys.get", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) - self.assertEqual("test key", gcs_span.data["gcs"]["accessId"]) + assert gcs_span.data["gcs"]["op"] == "hmacKeys.get" + assert gcs_span.data["gcs"]["projectId"] == "test-project" + assert gcs_span.data["gcs"]["accessId"] == "test key" - @unittest.skipIf( - sys.platform == "darwin", reason="Raises not Implemented exception in OSX" - ) @patch("requests.Session.request") - def test_object_hmac_keys_list(self, mock_requests): + def test_object_hmac_keys_list(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#hmacKeysMetadata", "items": []}, status_code=http_client.OK, @@ -909,36 +931,33 @@ def test_object_hmac_keys_list(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): keys = client.list_hmac_keys() - self.assertEqual( - 0, - self.recorder.queue_size(), - msg="span has been created before the actual request", - ) - for k in keys: + for _ in keys: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("hmacKeys.list", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) + assert gcs_span.data["gcs"]["op"] == "hmacKeys.list" + assert gcs_span.data["gcs"]["projectId"] == "test-project" @patch("requests.Session.request") - def test_object_hmac_keys_update(self, mock_requests): + def test_object_hmac_keys_update(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={"kind": "storage#hmacKey", "metadata": {}, "secret": ""}, status_code=http_client.OK, @@ -948,29 +967,31 @@ def test_object_hmac_keys_update(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): storage.hmac_key.HMACKeyMetadata(client, access_id="test key").update() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("hmacKeys.update", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) - self.assertEqual("test key", gcs_span.data["gcs"]["accessId"]) + assert gcs_span.data["gcs"]["op"] == "hmacKeys.update" + assert gcs_span.data["gcs"]["projectId"] == "test-project" + assert gcs_span.data["gcs"]["accessId"] == "test key" @patch("requests.Session.request") - def test_object_hmac_keys_update(self, mock_requests): + def test_object_get_service_account_email(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( json_content={ "email_address": "test@example.com", @@ -983,28 +1004,30 @@ def test_object_hmac_keys_update(self, mock_requests): credentials=AnonymousCredentials(), project="test-project" ) - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): client.get_service_account_email() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIsNone(tracer.active_span) + assert len(spans) == 2 + + current_span = get_current_span() + assert not current_span.is_recording() gcs_span = spans[0] test_span = spans[1] self.assertTraceContextPropagated(test_span, gcs_span) - self.assertEqual("gcs", gcs_span.n) - self.assertEqual(2, gcs_span.k) - self.assertIsNone(gcs_span.ec) + assert gcs_span.n == "gcs" + assert gcs_span.k is SpanKind.CLIENT + assert not gcs_span.ec - self.assertEqual("serviceAccount.get", gcs_span.data["gcs"]["op"]) - self.assertEqual("test-project", gcs_span.data["gcs"]["projectId"]) + assert gcs_span.data["gcs"]["op"] == "serviceAccount.get" + assert gcs_span.data["gcs"]["projectId"] == "test-project" @patch("requests.Session.request") - def test_batch_operation(self, mock_requests): + def test_batch_operation(self, mock_requests: Mock) -> None: mock_requests.return_value = self._mock_response( _TWO_PART_BATCH_RESPONSE, status_code=http_client.OK, @@ -1016,16 +1039,110 @@ def test_batch_operation(self, mock_requests): ) bucket = client.bucket("test-bucket") - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with client.batch(): for obj in ["obj1", "obj2"]: bucket.delete_blob(obj) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 + + @patch("requests.Session.request") + def test_execute_with_instana_without_tags(self, mock_requests: Mock) -> None: + mock_requests.return_value = self._mock_response( + json_content={"kind": "storage#buckets", "items": []}, + status_code=http_client.OK, + ) + client = self._client( + credentials=AnonymousCredentials(), project="test-project" + ) + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.google.cloud.storage._collect_attributes", + return_value=None, + ): + buckets = client.list_buckets() + for b in buckets: + pass + assert isinstance(buckets, page_iterator.HTTPIterator) + + def test_execute_with_instana_tracing_is_off(self) -> None: + client = self._client( + credentials=AnonymousCredentials(), project="test-project" + ) + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.google.cloud.storage.tracing_is_off", + return_value=True, + ): + response = client.list_buckets() + assert isinstance(response.client, storage.Client) + + @patch("requests.Session.request") + def test_download_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: + mock_requests.return_value = self._mock_response( + content=b"CONTENT", status_code=http_client.OK + ) + client = self._client( + credentials=AnonymousCredentials(), project="test-project" + ) + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.google.cloud.storage.tracing_is_off", + return_value=True, + ): + response = ( + client.bucket("test bucket") + .blob("test object") + .download_to_file( + io.BytesIO(), + raw_download=True, + ) + ) + assert not response + + @patch("requests.Session.request") + def test_upload_with_instana_tracing_is_off(self, mock_requests: Mock) -> None: + mock_requests.return_value = self._mock_response( + json_content={"kind": "storage#object"}, status_code=http_client.OK + ) + + client = self._client( + credentials=AnonymousCredentials(), project="test-project" + ) + + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.google.cloud.storage.tracing_is_off", + return_value=True, + ): + response = ( + client.bucket("test bucket") + .blob("test object") + .upload_from_string("CONTENT") + ) + assert not response + + @patch("requests.Session.request") + def test_finish_batch_operation_tracing_is_off(self, mock_requests: Mock) -> None: + mock_requests.return_value = self._mock_response( + _TWO_PART_BATCH_RESPONSE, + status_code=http_client.OK, + headers={"content-type": 'multipart/mixed; boundary="DEADBEEF="'}, + ) + + client = self._client( + credentials=AnonymousCredentials(), project="test-project" + ) + bucket = client.bucket("test-bucket") + + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.google.cloud.storage.tracing_is_off", + return_value=True, + ): + with client.batch() as batch_response: + for obj in ["obj1", "obj2"]: + bucket.delete_blob(obj) + assert batch_response - def _client(self, *args, **kwargs): + def _client(self, *args, **kwargs) -> storage.Client: # override the HTTP client to bypass the authorization kwargs["_http"] = kwargs.get("_http", requests.Session()) kwargs["_http"].is_mtls = False @@ -1038,7 +1155,7 @@ def _mock_response( status_code=http_client.NO_CONTENT, json_content=None, headers={}, - ): + ) -> Mock: resp = Mock() resp.status_code = status_code resp.headers = headers 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..069a4edd 100644 --- a/tests/clients/test_mysqlclient.py +++ b/tests/clients/test_mysqlclient.py @@ -1,22 +1,28 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging -import unittest - +import sys 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 + + +@pytest.mark.skipif( + sys.platform == "darwin", + reason="Avoiding errors with deprecated MySQL Client lib.", +) +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 +42,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/conftest.py b/tests/conftest.py index d63dea98..b2a81955 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,39 +4,41 @@ 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 +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 -collect_ignore_glob = [] +collect_ignore_glob = [ + "*test_gevent*", + "*platforms/test_gcr*", + "*platforms/test_google*", +] -# Cassandra and gevent tests are run in dedicated jobs on CircleCI and will -# be run explicitly. (So always exclude them here) +# # 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*") - -# Python 3.10 support is incomplete yet -# TODO: Remove this once we start supporting Tornado >= 6.0 -if sys.version_info >= (3, 10): - collect_ignore_glob.append("*test_tornado*") - # Furthermore on Python 3.11 the above TC is skipped: - # tests/opentracing/test_ot_span.py::TestOTSpan::test_stacks - # TODO: Remove that once we find a workaround or DROP opentracing! +# if not os.environ.get("GEVENT_STARLETTE_TEST"): +# collect_ignore_glob.append("*test_gevent*") +# collect_ignore_glob.append("*test_starlette*") if sys.version_info >= (3, 11): if not os.environ.get("GOOGLE_CLOUD_TEST"): @@ -46,6 +48,7 @@ # TODO: Test Case failures for unknown reason: collect_ignore_glob.append("*test_aiohttp_server*") collect_ignore_glob.append("*test_celery*") + collect_ignore_glob.append("*frameworks/test_tornado_server*") # Currently there is a runtime incompatibility caused by the library: # `undefined symbol: _PyErr_WriteUnraisableMsg` @@ -54,13 +57,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 +85,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_celery.py b/tests/frameworks/test_celery.py index bd08877f..126f0368 100644 --- a/tests/frameworks/test_celery.py +++ b/tests/frameworks/test_celery.py @@ -2,222 +2,280 @@ # (c) Copyright Instana Inc. 2020 import time +from typing import Generator, List from celery import shared_task +import celery +import celery.app +import celery.contrib +import celery.contrib.testing +import celery.contrib.testing.worker +import pytest from instana.singletons import tracer -from ..helpers import get_first_span_by_filter +from instana.span.span import InstanaSpan +from tests.helpers import get_first_span_by_filter # TODO: Refactor to class based tests + @shared_task -def add(x, y): +def add( + x: int, + y: int, +) -> int: return x + y @shared_task -def will_raise_error(): - raise Exception('This is a simulated error') +def will_raise_error() -> None: + raise Exception("This is a simulated error") -def filter_out_ping_tasks(spans): +def filter_out_ping_tasks( + spans: List[InstanaSpan], +) -> List[InstanaSpan]: filtered_spans = [] for span in spans: - is_ping_task = (span.n == 'celery-worker' and span.data['celery']['task'] == 'celery.ping') + is_ping_task = ( + span.n == "celery-worker" and span.data["celery"]["task"] == "celery.ping" + ) if not is_ping_task: filtered_spans.append(span) return filtered_spans -def setup_method(): - """ Clear all spans before a test run """ - tracer.recorder.clear_spans() - - -def test_apply_async(celery_app, celery_worker): - result = None - with tracer.start_active_span('test'): - result = add.apply_async(args=(4, 5)) - - # Wait for jobs to finish - time.sleep(0.5) - - spans = filter_out_ping_tasks(tracer.recorder.queued_spans()) - assert len(spans) == 3 - - filter = lambda span: span.n == "sdk" - test_span = get_first_span_by_filter(spans, filter) - assert(test_span) - - filter = lambda span: span.n == "celery-client" - client_span = get_first_span_by_filter(spans, filter) - assert(client_span) - - filter = lambda span: span.n == "celery-worker" - worker_span = get_first_span_by_filter(spans, filter) - assert(worker_span) - - assert(client_span.t == test_span.t) - assert(client_span.t == worker_span.t) - assert(client_span.p == test_span.s) - - assert("tests.frameworks.test_celery.add" == client_span.data["celery"]["task"]) - assert("redis" == client_span.data["celery"]["scheme"]) - assert("localhost" == client_span.data["celery"]["host"]) - assert("6379" == client_span.data["celery"]["port"]) - assert(client_span.data["celery"]["task_id"]) - assert(client_span.data["celery"]["error"] == None) - assert(client_span.ec == None) - - assert("tests.frameworks.test_celery.add" == worker_span.data["celery"]["task"]) - assert("redis" == worker_span.data["celery"]["scheme"]) - assert("localhost" == worker_span.data["celery"]["host"]) - assert("6379" == worker_span.data["celery"]["port"]) - assert(worker_span.data["celery"]["task_id"]) - assert(worker_span.data["celery"]["error"] == None) - assert(worker_span.data["celery"]["retry-reason"] == None) - assert(worker_span.ec == None) - - -def test_delay(celery_app, celery_worker): - result = None - with tracer.start_active_span('test'): - result = add.delay(4, 5) - - # Wait for jobs to finish - time.sleep(0.5) - - spans = filter_out_ping_tasks(tracer.recorder.queued_spans()) - assert len(spans) == 3 - - filter = lambda span: span.n == "sdk" - test_span = get_first_span_by_filter(spans, filter) - assert(test_span) - - filter = lambda span: span.n == "celery-client" - client_span = get_first_span_by_filter(spans, filter) - assert(client_span) - - filter = lambda span: span.n == "celery-worker" - worker_span = get_first_span_by_filter(spans, filter) - assert(worker_span) - - assert(client_span.t == test_span.t) - assert(client_span.t == worker_span.t) - assert(client_span.p == test_span.s) - - assert("tests.frameworks.test_celery.add" == client_span.data["celery"]["task"]) - assert("redis" == client_span.data["celery"]["scheme"]) - assert("localhost" == client_span.data["celery"]["host"]) - assert("6379" == client_span.data["celery"]["port"]) - assert(client_span.data["celery"]["task_id"]) - assert(client_span.data["celery"]["error"] == None) - assert(client_span.ec == None) - - assert("tests.frameworks.test_celery.add" == worker_span.data["celery"]["task"]) - assert("redis" == worker_span.data["celery"]["scheme"]) - assert("localhost" == worker_span.data["celery"]["host"]) - assert("6379" == worker_span.data["celery"]["port"]) - assert(worker_span.data["celery"]["task_id"]) - assert(worker_span.data["celery"]["error"] == None) - assert(worker_span.data["celery"]["retry-reason"] == None) - assert(worker_span.ec == None) - - -def test_send_task(celery_app, celery_worker): - result = None - with tracer.start_active_span('test'): - result = celery_app.send_task('tests.frameworks.test_celery.add', (1, 2)) - - # Wait for jobs to finish - time.sleep(0.5) - - spans = filter_out_ping_tasks(tracer.recorder.queued_spans()) - assert len(spans) == 3 - - filter = lambda span: span.n == "sdk" - test_span = get_first_span_by_filter(spans, filter) - assert(test_span) - - filter = lambda span: span.n == "celery-client" - client_span = get_first_span_by_filter(spans, filter) - assert(client_span) - - filter = lambda span: span.n == "celery-worker" - worker_span = get_first_span_by_filter(spans, filter) - assert(worker_span) - - assert(client_span.t == test_span.t) - assert(client_span.t == worker_span.t) - assert(client_span.p == test_span.s) - - assert("tests.frameworks.test_celery.add" == client_span.data["celery"]["task"]) - assert("redis" == client_span.data["celery"]["scheme"]) - assert("localhost" == client_span.data["celery"]["host"]) - assert("6379" == client_span.data["celery"]["port"]) - assert(client_span.data["celery"]["task_id"]) - assert(client_span.data["celery"]["error"] == None) - assert(client_span.ec == None) - - assert("tests.frameworks.test_celery.add" == worker_span.data["celery"]["task"]) - assert("redis" == worker_span.data["celery"]["scheme"]) - assert("localhost" == worker_span.data["celery"]["host"]) - assert("6379" == worker_span.data["celery"]["port"]) - assert(worker_span.data["celery"]["task_id"]) - assert(worker_span.data["celery"]["error"] == None) - assert(worker_span.data["celery"]["retry-reason"] == None) - assert(worker_span.ec == None) - - -def test_error_reporting(celery_app, celery_worker): - result = None - with tracer.start_active_span('test'): - result = will_raise_error.apply_async() - - # Wait for jobs to finish - time.sleep(0.5) - - spans = filter_out_ping_tasks(tracer.recorder.queued_spans()) - assert len(spans) == 4 - - filter = lambda span: span.n == "sdk" - test_span = get_first_span_by_filter(spans, filter) - assert(test_span) - - filter = lambda span: span.n == "celery-client" - client_span = get_first_span_by_filter(spans, filter) - assert(client_span) - - filter = lambda span: span.n == "log" - log_span = get_first_span_by_filter(spans, filter) - assert(log_span) - - filter = lambda span: span.n == "celery-worker" - worker_span = get_first_span_by_filter(spans, filter) - assert(worker_span) - - assert(client_span.t == test_span.t) - assert(client_span.t == worker_span.t) - assert(client_span.t == log_span.t) - - assert(client_span.p == test_span.s) - assert(worker_span.p == client_span.s) - assert(log_span.p == worker_span.s) - - assert("tests.frameworks.test_celery.will_raise_error" == client_span.data["celery"]["task"]) - assert("redis" == client_span.data["celery"]["scheme"]) - assert("localhost" == client_span.data["celery"]["host"]) - assert("6379" == client_span.data["celery"]["port"]) - assert(client_span.data["celery"]["task_id"]) - assert(client_span.data["celery"]["error"] == None) - assert(client_span.ec == None) - - assert("tests.frameworks.test_celery.will_raise_error" == worker_span.data["celery"]["task"]) - assert("redis" == worker_span.data["celery"]["scheme"]) - assert("localhost" == worker_span.data["celery"]["host"]) - assert("6379" == worker_span.data["celery"]["port"]) - assert(worker_span.data["celery"]["task_id"]) - assert(worker_span.data["celery"]["error"] == 'This is a simulated error') - assert(worker_span.data["celery"]["retry-reason"] == None) - assert(worker_span.ec == 1) - +class TestCelery: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + + def test_apply_async( + self, + celery_app: celery.app.base.Celery, + celery_worker: celery.contrib.testing.worker.TestWorkController, + ) -> None: + with tracer.start_as_current_span("test"): + _ = add.apply_async(args=(4, 5)) + + # Wait for jobs to finish + time.sleep(1) + + spans = filter_out_ping_tasks(self.recorder.queued_spans()) + assert len(spans) == 3 + + def filter(span): + return span.n == "sdk" + + test_span = get_first_span_by_filter(spans, filter) + assert test_span + + def filter(span): + return span.n == "celery-client" + + client_span = get_first_span_by_filter(spans, filter) + assert client_span + + def filter(span): + return span.n == "celery-worker" + + worker_span = get_first_span_by_filter(spans, filter) + assert worker_span + + assert client_span.t == test_span.t + assert client_span.t == worker_span.t + assert client_span.p == test_span.s + + assert client_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert client_span.data["celery"]["scheme"] == "redis" + assert client_span.data["celery"]["host"] == "localhost" + assert client_span.data["celery"]["port"] == "6379" + assert client_span.data["celery"]["task_id"] + assert not client_span.data["celery"]["error"] + assert not client_span.ec + + assert worker_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert worker_span.data["celery"]["scheme"] == "redis" + assert worker_span.data["celery"]["host"] == "localhost" + assert worker_span.data["celery"]["port"] == "6379" + assert worker_span.data["celery"]["task_id"] + assert not worker_span.data["celery"]["error"] + assert not worker_span.data["celery"]["retry-reason"] + assert not worker_span.ec + + def test_delay( + self, + celery_app: celery.app.base.Celery, + celery_worker: celery.contrib.testing.worker.TestWorkController, + ) -> None: + with tracer.start_as_current_span("test"): + _ = add.delay(4, 5) + + # Wait for jobs to finish + time.sleep(0.5) + + spans = filter_out_ping_tasks(self.recorder.queued_spans()) + assert len(spans) == 3 + + def filter(span): + return span.n == "sdk" + + test_span = get_first_span_by_filter(spans, filter) + assert test_span + + def filter(span): + return span.n == "celery-client" + + client_span = get_first_span_by_filter(spans, filter) + assert client_span + + def filter(span): + return span.n == "celery-worker" + + worker_span = get_first_span_by_filter(spans, filter) + assert worker_span + + assert client_span.t == test_span.t + assert client_span.t == worker_span.t + assert client_span.p == test_span.s + + assert client_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert client_span.data["celery"]["scheme"] == "redis" + assert client_span.data["celery"]["host"] == "localhost" + assert client_span.data["celery"]["port"] == "6379" + assert client_span.data["celery"]["task_id"] + assert not client_span.data["celery"]["error"] + assert not client_span.ec + + assert worker_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert worker_span.data["celery"]["scheme"] == "redis" + assert worker_span.data["celery"]["host"] == "localhost" + assert worker_span.data["celery"]["port"] == "6379" + assert worker_span.data["celery"]["task_id"] + assert not worker_span.data["celery"]["error"] + assert not worker_span.data["celery"]["retry-reason"] + assert not worker_span.ec + + def test_send_task( + self, + celery_app: celery.app.base.Celery, + celery_worker: celery.contrib.testing.worker.TestWorkController, + ) -> None: + with tracer.start_as_current_span("test"): + _ = celery_app.send_task("tests.frameworks.test_celery.add", (1, 2)) + + # Wait for jobs to finish + time.sleep(0.5) + + spans = filter_out_ping_tasks(self.recorder.queued_spans()) + assert len(spans) == 3 + + def filter(span): + return span.n == "sdk" + + test_span = get_first_span_by_filter(spans, filter) + assert test_span + + def filter(span): + return span.n == "celery-client" + + client_span = get_first_span_by_filter(spans, filter) + assert client_span + + def filter(span): + return span.n == "celery-worker" + + worker_span = get_first_span_by_filter(spans, filter) + assert worker_span + + assert client_span.t == test_span.t + assert client_span.t == worker_span.t + assert client_span.p == test_span.s + + assert client_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert client_span.data["celery"]["scheme"] == "redis" + assert client_span.data["celery"]["host"] == "localhost" + assert client_span.data["celery"]["port"] == "6379" + assert client_span.data["celery"]["task_id"] + assert not client_span.data["celery"]["error"] + assert not client_span.ec + + assert worker_span.data["celery"]["task"] == "tests.frameworks.test_celery.add" + assert worker_span.data["celery"]["scheme"] == "redis" + assert worker_span.data["celery"]["host"] == "localhost" + assert worker_span.data["celery"]["port"] == "6379" + assert worker_span.data["celery"]["task_id"] + assert not worker_span.data["celery"]["error"] + assert not worker_span.data["celery"]["retry-reason"] + assert not worker_span.ec + + def test_error_reporting( + self, + celery_app: celery.app.base.Celery, + celery_worker: celery.contrib.testing.worker.TestWorkController, + ) -> None: + with tracer.start_as_current_span("test"): + _ = will_raise_error.apply_async() + + # Wait for jobs to finish + time.sleep(4) + + spans = filter_out_ping_tasks(self.recorder.queued_spans()) + assert len(spans) == 4 + + def filter(span): + return span.n == "sdk" + + test_span = get_first_span_by_filter(spans, filter) + assert test_span + + def filter(span): + return span.n == "celery-client" + + client_span = get_first_span_by_filter(spans, filter) + assert client_span + + def filter(span): + return span.n == "log" + + log_span = get_first_span_by_filter(spans, filter) + assert log_span + + def filter(span): + return span.n == "celery-worker" + + worker_span = get_first_span_by_filter(spans, filter) + assert worker_span + + assert client_span.t == test_span.t + assert client_span.t == worker_span.t + assert client_span.t == log_span.t + + assert client_span.p == test_span.s + assert worker_span.p == client_span.s + assert log_span.p == worker_span.s + + assert ( + client_span.data["celery"]["task"] + == "tests.frameworks.test_celery.will_raise_error" + ) + assert client_span.data["celery"]["scheme"] == "redis" + assert client_span.data["celery"]["host"] == "localhost" + assert client_span.data["celery"]["port"] == "6379" + assert client_span.data["celery"]["task_id"] + assert not client_span.data["celery"]["error"] + assert not client_span.ec + + assert ( + worker_span.data["celery"]["task"] + == "tests.frameworks.test_celery.will_raise_error" + ) + assert worker_span.data["celery"]["scheme"] == "redis" + assert worker_span.data["celery"]["host"] == "localhost" + assert worker_span.data["celery"]["port"] == "6379" + assert worker_span.data["celery"]["task_id"] + assert worker_span.data["celery"]["error"] == "This is a simulated error" + assert not worker_span.data["celery"]["retry-reason"] + assert worker_span.ec == 1 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..fdbf4919 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -4,954 +4,1122 @@ 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 - def test_get_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["wsgi_server"] + '/') + # 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_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 that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert response.headers.get("tracestate", None) is None # 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 that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert "in=" not in response.headers["tracestate"] # 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_grpcio.py b/tests/frameworks/test_grpcio.py index 5bbd47db..99081883 100644 --- a/tests/frameworks/test_grpcio.py +++ b/tests/frameworks/test_grpcio.py @@ -2,605 +2,699 @@ # (c) Copyright Instana Inc. 2020 import time -import unittest import random +from typing import Generator +import pytest import grpc +from opentelemetry.trace import SpanKind + import tests.apps.grpc_server import tests.apps.grpc_server.stan_pb2 as stan_pb2 import tests.apps.grpc_server.stan_pb2_grpc as stan_pb2_grpc +from tests.helpers import testenv, get_first_span_by_name -from instana.singletons import tracer -from ..helpers import testenv, get_first_span_by_name +from instana.singletons import tracer, agent +from instana.span.span import get_current_span -class TestGRPCIO(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestGRPCIO: + @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.channel = grpc.insecure_channel(testenv["grpc_server"]) self.server_stub = stan_pb2_grpc.StanStub(self.channel) # The grpc client apparently needs a second to connect and initialize time.sleep(1) + # tearDown + # Ensure that allow_exit_as_root has the default value + agent.options.allow_exit_as_root = False - def tearDown(self): - """ Do nothing for now """ - pass - - def generate_questions(self): - """ Used in the streaming grpc tests """ + def generate_questions(self) -> None: + """Used in the streaming grpc tests""" questions = [ stan_pb2.QuestionRequest(question="Are you there?"), stan_pb2.QuestionRequest(question="What time is it?"), stan_pb2.QuestionRequest(question="Where in the world is Waldo?"), - stan_pb2.QuestionRequest(question="What did one campfire say to the other?"), + stan_pb2.QuestionRequest( + question="What did one campfire say to the other?" + ), stan_pb2.QuestionRequest(question="Is cereal soup?"), - stan_pb2.QuestionRequest(question="What is always coming, but never arrives?") + stan_pb2.QuestionRequest( + question="What is always coming, but never arrives?" + ), ] for q in questions: yield q time.sleep(random.uniform(0.2, 0.5)) - def test_vanilla_request(self): - response = self.server_stub.OneQuestionOneResponse(stan_pb2.QuestionRequest(question="Are you there?")) - self.assertEqual(response.answer, "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka") - - def test_vanilla_request_via_with_call(self): - response = self.server_stub.OneQuestionOneResponse.with_call(stan_pb2.QuestionRequest(question="Are you there?")) - self.assertEqual(response[0].answer, "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka") - - def test_unary_one_to_one(self): - with tracer.start_active_span('test'): - response = self.server_stub.OneQuestionOneResponse(stan_pb2.QuestionRequest(question="Are you there?")) - - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(response) - self.assertEqual(response.answer, "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka") + def test_vanilla_request(self) -> None: + response = self.server_stub.OneQuestionOneResponse( + stan_pb2.QuestionRequest(question="Are you there?") + ) + assert ( + response.answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) + + def test_vanilla_request_via_with_call(self) -> None: + response = self.server_stub.OneQuestionOneResponse.with_call( + stan_pb2.QuestionRequest(question="Are you there?") + ) + assert ( + response[0].answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) + + def test_unary_one_to_one(self) -> None: + with tracer.start_as_current_span("test"): + response = self.server_stub.OneQuestionOneResponse( + stan_pb2.QuestionRequest(question="Are you there?") + ) + + assert not get_current_span().is_recording() + assert response + assert ( + response.answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + assert len(spans) == 3 + + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'unary') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "unary" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') - - def test_streaming_many_to_one(self): + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - with tracer.start_active_span('test'): - response = self.server_stub.ManyQuestionsOneResponse(self.generate_questions()) + def test_streaming_many_to_one(self) -> None: + with tracer.start_as_current_span("test"): + response = self.server_stub.ManyQuestionsOneResponse( + self.generate_questions() + ) - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(response) + assert not get_current_span().is_recording() + assert response - self.assertEqual('Ok', response.answer) - self.assertEqual(True, response.was_answered) + assert response.answer == "Ok" + assert response.was_answered spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'stream') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "stream" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_streaming_one_to_many(self): + def test_streaming_one_to_many(self) -> None: + with tracer.start_as_current_span("test"): + responses = self.server_stub.OneQuestionManyResponses( + stan_pb2.QuestionRequest(question="Are you there?") + ) - with tracer.start_active_span('test'): - responses = self.server_stub.OneQuestionManyResponses(stan_pb2.QuestionRequest(question="Are you there?")) - - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(responses) + assert not get_current_span().is_recording() + assert responses final_answers = [] for response in responses: final_answers.append(response) - self.assertEqual(len(final_answers), 6) + assert len(final_answers) == 6 spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/OneQuestionManyResponses') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionManyResponses" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/OneQuestionManyResponses') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'stream') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionManyResponses" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "stream" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_streaming_many_to_many(self): - with tracer.start_active_span('test'): - responses = self.server_stub.ManyQuestionsManyReponses(self.generate_questions()) + def test_streaming_many_to_many(self) -> None: + with tracer.start_as_current_span("test"): + responses = self.server_stub.ManyQuestionsManyReponses( + self.generate_questions() + ) - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(responses) + assert not get_current_span().is_recording() + assert responses final_answers = [] for response in responses: final_answers.append(response) - self.assertEqual(len(final_answers), 6) + assert len(final_answers) == 6 spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsManyReponses') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsManyReponses" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsManyReponses') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'stream') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsManyReponses" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "stream" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') - - def test_unary_one_to_one_with_call(self): - with tracer.start_active_span('test'): - response = self.server_stub.OneQuestionOneResponse.with_call(stan_pb2.QuestionRequest(question="Are you there?")) - - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(response) - self.assertEqual(type(response), tuple) - self.assertEqual(response[0].answer, "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka") + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" + + def test_unary_one_to_one_with_call(self) -> None: + with tracer.start_as_current_span("test"): + response = self.server_stub.OneQuestionOneResponse.with_call( + stan_pb2.QuestionRequest(question="Are you there?") + ) + + assert not get_current_span().is_recording() + assert response + assert type(response) == tuple + assert ( + response[0].answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'unary') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "unary" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_streaming_many_to_one_with_call(self): - with tracer.start_active_span('test'): - response = self.server_stub.ManyQuestionsOneResponse.with_call(self.generate_questions()) + def test_streaming_many_to_one_with_call(self) -> None: + with tracer.start_as_current_span("test"): + response = self.server_stub.ManyQuestionsOneResponse.with_call( + self.generate_questions() + ) - self.assertIsNone(tracer.active_span) - self.assertIsNotNone(response) + assert not get_current_span().is_recording() + assert response - self.assertEqual('Ok', response[0].answer) - self.assertEqual(True, response[0].was_answered) + assert response[0].answer == "Ok" + assert response[0].was_answered spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'stream') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "stream" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_async_unary(self): + def test_async_unary(self) -> None: def process_response(future): result = future.result() - self.assertEqual(type(result), stan_pb2.QuestionResponse) - self.assertTrue(result.was_answered) - self.assertEqual(result.answer, "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka") - - with tracer.start_active_span('test'): + assert type(result) == stan_pb2.QuestionResponse + assert result.was_answered + assert ( + result.answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) + + with tracer.start_as_current_span("test"): future = self.server_stub.OneQuestionOneResponse.future( - stan_pb2.QuestionRequest(question="Are you there?")) + stan_pb2.QuestionRequest(question="Are you there?") + ) future.add_done_callback(process_response) time.sleep(0.7) - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'unary') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "unary" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_async_stream(self): + def test_async_stream(self) -> None: def process_response(future): result = future.result() - self.assertEqual(type(result), stan_pb2.QuestionResponse) - self.assertTrue(result.was_answered) - self.assertEqual(result.answer, 'Ok') - - with tracer.start_active_span('test'): - future = self.server_stub.ManyQuestionsOneResponse.future(self.generate_questions()) + assert type(result) == stan_pb2.QuestionResponse + assert result.was_answered + assert result.answer == "Ok" + + with tracer.start_as_current_span("test"): + future = self.server_stub.ManyQuestionsOneResponse.future( + self.generate_questions() + ) future.add_done_callback(process_response) # The question generator delays at random intervals between questions so to assure that # all questions are sent and processed before we start testing the results. time.sleep(5) - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/ManyQuestionsOneResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'stream') - self.assertIsNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert client_span.data["rpc"]["call"] == "/stan.Stan/ManyQuestionsOneResponse" + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "stream" + assert not client_span.data["rpc"]["error"] # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" - def test_server_error(self): + def test_server_error(self) -> None: response = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: - response = self.server_stub.OneQuestionOneErrorResponse(stan_pb2.QuestionRequest(question="Do u error?")) - except: + response = self.server_stub.OneQuestionOneErrorResponse( + stan_pb2.QuestionRequest(question="Do u error?") + ) + except Exception: pass - self.assertIsNone(tracer.active_span) - self.assertIsNone(response) + assert not get_current_span().is_recording() + assert not response spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - log_span = get_first_span_by_name(spans, 'log') - server_span = get_first_span_by_name(spans, 'rpc-server') - client_span = get_first_span_by_name(spans, 'rpc-client') - test_span = get_first_span_by_name(spans, 'sdk') + log_span = get_first_span_by_name(spans, "log") + server_span = get_first_span_by_name(spans, "rpc-server") + client_span = get_first_span_by_name(spans, "rpc-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(log_span) - self.assertTrue(server_span) - self.assertTrue(client_span) - self.assertTrue(test_span) + assert log_span + assert server_span + assert client_span + assert test_span # Same traceId - self.assertEqual(server_span.t, client_span.t) - self.assertEqual(server_span.t, test_span.t) + assert server_span.t == client_span.t + assert server_span.t == test_span.t # Parent relationships - self.assertEqual(server_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) + assert server_span.p == client_span.s + assert client_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(client_span.ec, 1) - self.assertIsNone(server_span.ec) + assert not test_span.ec + assert client_span.ec == 1 + assert not server_span.ec # rpc-server - self.assertEqual(server_span.n, 'rpc-server') - self.assertEqual(server_span.k, 1) - self.assertIsNone(server_span.stack) - self.assertEqual(server_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(server_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneErrorResponse') - self.assertEqual(server_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(server_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertIsNone(server_span.data["rpc"]["error"]) + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert ( + server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneErrorResponse" + ) + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] # rpc-client - self.assertEqual(client_span.n, 'rpc-client') - self.assertEqual(client_span.k, 2) - self.assertIsNotNone(client_span.stack) - self.assertEqual(client_span.data["rpc"]["flavor"], 'grpc') - self.assertEqual(client_span.data["rpc"]["call"], '/stan.Stan/OneQuestionOneErrorResponse') - self.assertEqual(client_span.data["rpc"]["host"], testenv["grpc_host"]) - self.assertEqual(client_span.data["rpc"]["port"], str(testenv["grpc_port"])) - self.assertEqual(client_span.data["rpc"]["call_type"], 'unary') - self.assertIsNotNone(client_span.data["rpc"]["error"]) + assert client_span.n == "rpc-client" + assert client_span.k is SpanKind.CLIENT + assert client_span.stack + assert client_span.data["rpc"]["flavor"] == "grpc" + assert ( + client_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneErrorResponse" + ) + assert client_span.data["rpc"]["host"] == testenv["grpc_host"] + assert client_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert client_span.data["rpc"]["call_type"] == "unary" + assert client_span.data["rpc"]["error"] # log - self.assertEqual(log_span.n, 'log') - self.assertIsNotNone(log_span.data["log"]) - self.assertEqual(log_span.data["log"]['message'], 'Exception calling application: Simulated error') + assert log_span.n == "log" + assert log_span.data["log"] + assert ( + log_span.data["log"]["message"] + == "Exception calling application: Simulated error" + ) # test-span - self.assertEqual(test_span.n, 'sdk') - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert test_span.n == "sdk" + assert test_span.data["sdk"]["name"] == "test" + + def test_root_exit_span(self) -> None: + agent.options.allow_exit_as_root = True + + response = self.server_stub.OneQuestionOneResponse.with_call( + stan_pb2.QuestionRequest(question="Are you there?") + ) + assert ( + response[0].answer + == "Invention, my dear friends, is 93% perspiration, 6% electricity, 4% evaporation, and 2% butterscotch ripple. – Willy Wonka" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + server_span = spans[0] + + assert server_span + + # Parent relationships + assert not server_span.p + + # Error logging + assert not server_span.ec + + # rpc-server + assert server_span.n == "rpc-server" + assert server_span.k is SpanKind.SERVER + assert not server_span.stack + assert server_span.data["rpc"]["flavor"] == "grpc" + assert server_span.data["rpc"]["call"] == "/stan.Stan/OneQuestionOneResponse" + assert server_span.data["rpc"]["host"] == testenv["grpc_host"] + assert server_span.data["rpc"]["port"] == str(testenv["grpc_port"]) + assert not server_span.data["rpc"]["error"] + + def test_no_root_exit_span(self) -> None: + responses = self.server_stub.OneQuestionManyResponses( + stan_pb2.QuestionRequest(question="Are you there?") + ) + + assert responses + + spans = self.recorder.queued_spans() + assert len(spans) == 0 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_tornado_client.py b/tests/frameworks/test_tornado_client.py index 244a03b7..24b8dca3 100644 --- a/tests/frameworks/test_tornado_client.py +++ b/tests/frameworks/test_tornado_client.py @@ -3,22 +3,23 @@ import time import asyncio -import unittest +import pytest +from typing import Generator import tornado from tornado.httpclient import AsyncHTTPClient -from instana.singletons import tornado_tracer +from instana.singletons import tracer +from instana.span.span import get_current_span import tests.apps.tornado_server -from ..helpers import testenv +from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter -raise unittest.SkipTest("Non deterministic tests TBR") +class TestTornadoClient: -class TestTornadoClient(unittest.TestCase): - - def setUp(self): + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: """ Clear all spans before a test run """ - self.recorder = tornado_tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -27,435 +28,434 @@ def setUp(self): asyncio.set_event_loop(self.loop) self.http_client = AsyncHTTPClient() - - def tearDown(self): + yield self.http_client.close() - def test_get(self): + def test_get(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/") response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsinstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(200, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(200, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", client_span.data["http"]["url"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_post(self): + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + # assert server_span.stack + # assert type(server_span.stack) is list + # assert len(server_span.stack) > 1 + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId + + def test_post(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/", method="POST", body='asdf') response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(200, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("POST", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(200, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", client_span.data["http"]["url"]) - self.assertEqual("POST", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_get_301(self): + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "POST" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "POST" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId + + def test_get_301(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/301") response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 5 server301_span = spans[0] server_span = spans[1] client_span = spans[2] - test_span = spans[3] + client301_span = spans[3] + test_span = spans[4] + + filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 + server301_span = get_first_span_by_filter(spans, filter) + filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 200 + server_span = get_first_span_by_filter(spans, filter) + filter = lambda span: span.n == "tornado-client" and span.data["http"]["url"] == testenv["tornado_server"] + "/" + client_span = get_first_span_by_filter(spans, filter) + filter = lambda span: span.n == "tornado-client" and span.data["http"]["url"] == testenv["tornado_server"] + "/301" + client301_span = get_first_span_by_filter(spans, filter) + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server301_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == client301_span.t + assert traceId == server301_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(server301_span.p, client_span.s) - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert server301_span.p == client301_span.s + assert client_span.p == test_span.s + assert client301_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(200, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-server", server301_span.n) - self.assertEqual(301, server301_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/301", server301_span.data["http"]["url"]) - self.assertIsNone(server301_span.data["http"]["params"]) - self.assertEqual("GET", server301_span.data["http"]["method"]) - self.assertIsNotNone(server301_span.stack) - self.assertTrue(type(server301_span.stack) is list) - self.assertTrue(len(server301_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(200, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/301", client_span.data["http"]["url"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_get_405(self): + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert server301_span.n == "tornado-server" + assert server301_span.data["http"]["status"] == 301 + assert testenv["tornado_server"] + "/301" == server301_span.data["http"]["url"] + assert not server301_span.data["http"]["params"] + assert server301_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_span.stack) > 1 + + assert client301_span.n == "tornado-client" + assert client301_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/301" == client301_span.data["http"]["url"] + assert client301_span.data["http"]["method"] == "GET" + assert client301_span.stack + assert type(client301_span.stack) is list + assert len(client301_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(server_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" % traceId + + def test_get_405(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: return await self.http_client.fetch(testenv["tornado_server"] + "/405") except tornado.httpclient.HTTPClientError as e: return e.response response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(client_span.ec, 1) - self.assertIsNone(server_span.ec) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(405, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/405", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(405, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/405", client_span.data["http"]["url"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_get_500(self): + assert not test_span.ec + assert client_span.ec == 1 + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 405 + assert testenv["tornado_server"] + "/405" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 405 + assert testenv["tornado_server"] + "/405" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId + + def test_get_500(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: return await self.http_client.fetch(testenv["tornado_server"] + "/500") except tornado.httpclient.HTTPClientError as e: return e.response response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(client_span.ec, 1) - self.assertEqual(server_span.ec, 1) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(500, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/500", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(500, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/500", client_span.data["http"]["url"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_get_504(self): + assert not test_span.ec + assert client_span.ec == 1 + assert server_span.ec == 1 + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 500 + assert testenv["tornado_server"] + "/500" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 500 + assert testenv["tornado_server"] + "/500" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId + + def test_get_504(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: return await self.http_client.fetch(testenv["tornado_server"] + "/504") except tornado.httpclient.HTTPClientError as e: return e.response response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(client_span.ec, 1) - self.assertEqual(server_span.ec, 1) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(504, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/504", server_span.data["http"]["url"]) - self.assertIsNone(server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(504, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/504", client_span.data["http"]["url"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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_get_with_params_to_scrub(self): + assert not test_span.ec + assert client_span.ec == 1 + assert server_span.ec == 1 + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 504 + assert testenv["tornado_server"] + "/504" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 504 + assert testenv["tornado_server"] + "/504" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId + + def test_get_with_params_to_scrub(self) -> None: async def test(): - with tornado_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): return await self.http_client.fetch(testenv["tornado_server"] + "/?secret=yeah") response = tornado.ioloop.IOLoop.current().run_sync(test) - self.assertIsInstance(response, tornado.httpclient.HTTPResponse) + assert isinstance(response, tornado.httpclient.HTTPResponse) time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - server_span = spans[0] - client_span = spans[1] - test_span = spans[2] + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNone(tornado_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, client_span.t) - self.assertEqual(traceId, server_span.t) + assert traceId == client_span.t + assert traceId == server_span.t # Parent relationships - self.assertEqual(client_span.p, test_span.s) - self.assertEqual(server_span.p, client_span.s) + assert client_span.p == test_span.s + assert server_span.p == client_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(client_span.ec) - self.assertIsNone(server_span.ec) - - self.assertEqual("tornado-server", server_span.n) - self.assertEqual(200, server_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", server_span.data["http"]["url"]) - self.assertEqual('secret=', server_span.data["http"]["params"]) - self.assertEqual("GET", server_span.data["http"]["method"]) - self.assertIsNotNone(server_span.stack) - self.assertTrue(type(server_span.stack) is list) - self.assertTrue(len(server_span.stack) > 1) - - self.assertEqual("tornado-client", client_span.n) - self.assertEqual(200, client_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", client_span.data["http"]["url"]) - self.assertEqual('secret=', client_span.data["http"]["params"]) - self.assertEqual("GET", client_span.data["http"]["method"]) - self.assertIsNotNone(client_span.stack) - self.assertTrue(type(client_span.stack) is list) - self.assertTrue(len(client_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"], server_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 client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] + assert 'secret=' == server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] + assert 'secret=' == client_span.data["http"]["params"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_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(server_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" % traceId diff --git a/tests/frameworks/test_tornado_server.py b/tests/frameworks/test_tornado_server.py index 9972edf1..96f740d6 100644 --- a/tests/frameworks/test_tornado_server.py +++ b/tests/frameworks/test_tornado_server.py @@ -1,7 +1,8 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +import pytest +from typing import Generator import asyncio import aiohttp @@ -10,11 +11,12 @@ import tests.apps.tornado_server -from instana.singletons import async_tracer, agent -from ..helpers import testenv, get_first_span_by_name, get_first_span_by_filter +from instana.singletons import tracer, agent +from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter +from instana.span.span import get_current_span -class TestTornadoServer(unittest.TestCase): +class TestTornadoServer: async def fetch(self, session, url, headers=None, params=None): try: async with session.get(url, headers=headers, params=params) as response: @@ -29,9 +31,10 @@ async def post(self, session, url, headers=None): except aiohttp.web_exceptions.HTTPException: pass - def setUp(self): + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -40,166 +43,165 @@ def setUp(self): asyncio.set_event_loop(self.loop) self.http_client = AsyncHTTPClient() - - def tearDown(self): + yield self.http_client.close() - def test_get(self): + def test_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["tornado_server"] + "/") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Synthetic - self.assertIsNone(tornado_span.sy) - self.assertIsNone(aiohttp_span.sy) - self.assertIsNone(test_span.sy) + assert not tornado_span.sy + assert not aiohttp_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", tornado_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_post(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == tornado_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + def test_post(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.post(session, testenv["tornado_server"] + "/") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", tornado_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("POST", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", aiohttp_span.data["http"]["url"]) - self.assertEqual("POST", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_synthetic_request(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == tornado_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_span.data["http"]["method"] == "POST" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == aiohttp_span.data["http"]["url"] + assert aiohttp_span.data["http"]["method"] == "POST" + 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(tornado_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" % traceId + + def test_synthetic_request(self) -> None: async def test(): 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["tornado_server"] + "/", headers=headers) tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertTrue(tornado_span.sy) - self.assertIsNone(aiohttp_span.sy) - self.assertIsNone(test_span.sy) + assert tornado_span.sy + assert not aiohttp_span.sy + assert not test_span.sy - def test_get_301(self): + def test_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["tornado_server"] + "/301") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 filter = lambda span: span.n == "tornado-server" and span.data["http"]["status"] == 301 tornado_301_span = get_first_span_by_filter(spans, filter) @@ -208,312 +210,315 @@ async def test(): aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_301_span) - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_301_span + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_301_span.n) - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_301_span.n == "tornado-server" + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) - self.assertEqual(traceId, tornado_301_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t + assert traceId == tornado_301_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_301_span.p, aiohttp_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_301_span.p == aiohttp_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_301_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(301, tornado_301_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/301", tornado_301_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_301_span.data["http"]["method"]) - self.assertIsNone(tornado_301_span.stack) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", tornado_span.data["http"]["url"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/301", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_get_405(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_301_span.ec + assert not tornado_span.ec + + assert tornado_301_span.data["http"]["status"] == 301 + assert testenv["tornado_server"] + "/301" == tornado_301_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_301_span.data["http"]["method"] == "GET" + assert not tornado_301_span.stack + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == tornado_span.data["http"]["url"] + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/301" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + @pytest.mark.skip("Non deterministic (flaky) testcase") + def test_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["tornado_server"] + "/405") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(405, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/405", tornado_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(405, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/405", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_get_500(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 405 + assert testenv["tornado_server"] + "/405" == tornado_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 405 + assert testenv["tornado_server"] + "/405" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + @pytest.mark.skip("Non deterministic (flaky) testcase") + def test_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["tornado_server"] + "/500") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(tornado_span.ec, 1) - - self.assertEqual(500, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/500", tornado_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(500, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_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.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_get_504(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert tornado_span.ec == 1 + + assert tornado_span.data["http"]["status"] == 500 + assert testenv["tornado_server"] + "/500" == tornado_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 500 + assert testenv["tornado_server"] + "/500" == aiohttp_span.data["http"]["url"] + assert aiohttp_span.data["http"]["method"] == "GET" + assert 'Internal Server Error' == aiohttp_span.data["http"]["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(tornado_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" % traceId + + @pytest.mark.skip("Non deterministic (flaky) testcase") + def test_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["tornado_server"] + "/504") response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(tornado_span.ec, 1) - - self.assertEqual(504, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/504", tornado_span.data["http"]["url"]) - self.assertIsNone(tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(504, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_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.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_get_with_params_to_scrub(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert tornado_span.ec == 1 + + assert tornado_span.data["http"]["status"] == 504 + assert testenv["tornado_server"] + "/504" == tornado_span.data["http"]["url"] + assert not tornado_span.data["http"]["params"] + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 504 + assert testenv["tornado_server"] + "/504" == aiohttp_span.data["http"]["url"] + assert aiohttp_span.data["http"]["method"] == "GET" + assert 'Gateway Timeout' == aiohttp_span.data["http"]["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(tornado_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" % traceId + + def test_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["tornado_server"], params={"secret": "yeah"}) response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", tornado_span.data["http"]["url"]) - self.assertEqual("secret=", tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_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.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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_request_header_capture(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == tornado_span.data["http"]["url"] + assert tornado_span.data["http"]["params"] == "secret=" + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + def test_request_header_capture(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: # Hack together a manual custom request headers list agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] @@ -528,67 +533,67 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/", tornado_span.data["http"]["url"]) - self.assertEqual("secret=", tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_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.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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", tornado_span.data["http"]["header"]) - self.assertEqual("this", tornado_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", tornado_span.data["http"]["header"]) - self.assertEqual("that", tornado_span.data["http"]["header"]["X-Capture-That"]) - - def test_response_header_capture(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == tornado_span.data["http"]["url"] + assert tornado_span.data["http"]["params"] == "secret=" + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + assert "X-Capture-This" in tornado_span.data["http"]["header"] + assert tornado_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in tornado_span.data["http"]["header"] + assert tornado_span.data["http"]["header"]["X-Capture-That"] == "that" + + def test_response_header_capture(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: # Hack together a manual custom response headers list agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] @@ -598,60 +603,60 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 tornado_span = get_first_span_by_name(spans, "tornado-server") aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") test_span = get_first_span_by_name(spans, "sdk") - self.assertIsNotNone(tornado_span) - self.assertIsNotNone(aiohttp_span) - self.assertIsNotNone(test_span) + assert tornado_span + assert aiohttp_span + assert test_span - self.assertIsNone(async_tracer.active_span) + assert not get_current_span().is_recording() - self.assertEqual("tornado-server", tornado_span.n) - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual("sdk", test_span.n) + assert tornado_span.n == "tornado-server" + assert aiohttp_span.n == "aiohttp-client" + assert test_span.n == "sdk" # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, tornado_span.t) + assert traceId == aiohttp_span.t + assert traceId == tornado_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(tornado_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert tornado_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(tornado_span.ec) - - self.assertEqual(200, tornado_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/response_headers", tornado_span.data["http"]["url"]) - self.assertEqual("secret=", tornado_span.data["http"]["params"]) - self.assertEqual("GET", tornado_span.data["http"]["method"]) - self.assertIsNone(tornado_span.stack) - - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["tornado_server"] + "/response_headers", 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.assertIsInstance(aiohttp_span.stack, list) - self.assertGreater(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"], tornado_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-Too", tornado_span.data["http"]["header"]) - self.assertEqual("this too", tornado_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", tornado_span.data["http"]["header"]) - self.assertEqual("that too", tornado_span.data["http"]["header"]["X-Capture-That-Too"]) + assert not test_span.ec + assert not aiohttp_span.ec + assert not tornado_span.ec + + assert tornado_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/response_headers" == tornado_span.data["http"]["url"] + assert tornado_span.data["http"]["params"] == "secret=" + assert tornado_span.data["http"]["method"] == "GET" + assert not tornado_span.stack + + assert aiohttp_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/response_headers" == aiohttp_span.data["http"]["url"] + 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(tornado_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" % traceId + + assert "X-Capture-This-Too" in tornado_span.data["http"]["header"] + assert tornado_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in tornado_span.data["http"]["header"] + assert tornado_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" 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/__init__.py b/tests/opentracing/__init__.py deleted file mode 100644 index e69de29b..00000000 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/test_eksfargate.py b/tests/platforms/test_eksfargate.py deleted file mode 100644 index 9d6e2437..00000000 --- a/tests/platforms/test_eksfargate.py +++ /dev/null @@ -1,120 +0,0 @@ -# (c) Copyright IBM Corp. 2024 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import EKSFargateOptions -from instana.recorder import StanRecorder -from instana.agent.aws_eks_fargate import EKSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestFargate(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargate, 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): - os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "INSTANA_TRACER_ENVIRONMENT", - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", - "INSTANA_SECRETS", "INSTANA_DEBUG", "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 = EKSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, EKSFargateOptions)) - - def test_missing_variables(self): - with self.assertLogs("instana", level=logging.WARN) as context: - os.environ.pop("INSTANA_ENDPOINT_URL") - agent = EKSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - self.assertIn('environment variables not set', context.output[0]) - - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - with self.assertLogs("instana", level=logging.WARN) as context: - os.environ.pop("INSTANA_AGENT_KEY") - agent = EKSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - self.assertIn('environment variables not set', context.output[0]) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - 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.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_default_tags(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertIsNone(self.agent.options.tags) - - def test_has_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_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertListEqual(should_headers, self.agent.options.extra_http_headers) - - 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_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/platforms/test_eksfargate_collector.py b/tests/platforms/test_eksfargate_collector.py deleted file mode 100644 index 307dce47..00000000 --- a/tests/platforms/test_eksfargate_collector.py +++ /dev/null @@ -1,80 +0,0 @@ -# (c) Copyright IBM Corp. 2024 - -import os -import json -import unittest - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.aws_eks_fargate import EKSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestFargateCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargateCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "INSTANA_TRACER_ENVIRONMENT", - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_ZONE", "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 = EKSFargateAgent() - 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(2, len(payload.keys())) - self.assertIn('spans',payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(0, len(payload['spans'])) - self.assertIn('metrics', payload) - self.assertEqual(1, len(payload['metrics'].keys())) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(2, len(payload['metrics']['plugins'])) - - - process_plugin = payload['metrics']['plugins'][0] - #self.assertIn('data', process_plugin) - - runtime_plugin = payload['metrics']['plugins'][1] - self.assertIn('name', runtime_plugin) - self.assertIn('entityId', runtime_plugin) - self.assertIn('data', runtime_plugin) - - def test_no_instana_zone(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.zone) - diff --git a/tests/platforms/test_fargate.py b/tests/platforms/test_fargate.py deleted file mode 100644 index 7a50353a..00000000 --- a/tests/platforms/test_fargate.py +++ /dev/null @@ -1,125 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import AWSFargateOptions -from instana.recorder import StanRecorder -from instana.agent.aws_fargate import AWSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestFargate(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargate, 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): - os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - if "INSTANA_SECRETS" in os.environ: - os.environ.pop("INSTANA_SECRETS") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, AWSFargateOptions)) - - def test_invalid_options(self): - # None of the required env vars are available... - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = AWSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - 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.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_default_tags(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertIsNone(self.agent.options.tags) - - def test_has_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_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertListEqual(should_headers, self.agent.options.extra_http_headers) - - 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_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/platforms/test_fargate_collector.py b/tests/platforms/test_fargate_collector.py deleted file mode 100644 index 361d14e8..00000000 --- a/tests/platforms/test_fargate_collector.py +++ /dev/null @@ -1,242 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import json -import unittest - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.aws_fargate import AWSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -def get_docker_plugin(plugins): - """ - Given a list of plugins, find and return the docker plugin that we're interested in from the mock data - """ - docker_plugin = None - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.docker" and plugin["entityId"] == "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82::docker-ssh-aws-fargate": - docker_plugin = plugin - return docker_plugin - - -class TestFargateCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargateCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - # Manually set the ECS Metadata API results on the collector - with open(self.pwd + '/../data/fargate/1.3.0/root_metadata.json', 'r') as json_file: - self.agent.collector.root_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/task_metadata.json', 'r') as json_file: - self.agent.collector.task_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/stats_metadata.json', 'r') as json_file: - self.agent.collector.stats_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/task_stats_metadata.json', 'r') as json_file: - self.agent.collector.task_stats_metadata = json.load(json_file) - - def test_prepare_payload_basics(self): - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(2, len(payload.keys())) - self.assertIn('spans',payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(0, len(payload['spans'])) - self.assertIn('metrics', payload) - self.assertEqual(1, len(payload['metrics'].keys())) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(7, len(payload['metrics']['plugins'])) - - plugins = payload['metrics']['plugins'] - for plugin in plugins: - # print("%s - %s" % (plugin["name"], plugin["entityId"])) - self.assertIn('name', plugin) - self.assertIn('entityId', plugin) - self.assertIn('data', plugin) - - def test_docker_plugin_snapshot_data(self): - self.create_agent_and_setup_tracer() - - first_payload = self.agent.collector.prepare_payload() - second_payload = self.agent.collector.prepare_payload() - - self.assertTrue(first_payload) - self.assertTrue(second_payload) - - plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) - plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) - - self.assertTrue(plugin_first_report) - self.assertIn("data", plugin_first_report) - - # First report should have snapshot data - data = plugin_first_report["data"] - self.assertEqual(data["Id"], "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45") - self.assertEqual(data["Created"], "2020-07-27T12:14:12.583114444Z") - self.assertEqual(data["Started"], "2020-07-27T12:14:13.545410186Z") - self.assertEqual(data["Image"], "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest") - self.assertEqual(data["Labels"], {'com.amazonaws.ecs.cluster': 'arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster', 'com.amazonaws.ecs.container-name': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-arn': 'arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82', 'com.amazonaws.ecs.task-definition-family': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-definition-version': '1'}) - self.assertIsNone(data["Ports"]) - - # Second report should have no snapshot data - self.assertTrue(plugin_second_report) - self.assertIn("data", plugin_second_report) - data = plugin_second_report["data"] - self.assertIn("Id", data) - self.assertNotIn("Created", data) - self.assertNotIn("Started", data) - self.assertNotIn("Image", data) - self.assertNotIn("Labels", data) - self.assertNotIn("Ports", data) - - def test_docker_plugin_metrics(self): - self.create_agent_and_setup_tracer() - - first_payload = self.agent.collector.prepare_payload() - second_payload = self.agent.collector.prepare_payload() - - self.assertTrue(first_payload) - self.assertTrue(second_payload) - - plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) - self.assertTrue(plugin_first_report) - self.assertIn("data", plugin_first_report) - - plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) - self.assertTrue(plugin_second_report) - self.assertIn("data", plugin_second_report) - - # First report should report all metrics - data = plugin_first_report.get("data", None) - self.assertTrue(data) - self.assertNotIn("network", data) - - cpu = data.get("cpu", None) - self.assertTrue(cpu) - self.assertEqual(cpu["total_usage"], 0.011033) - self.assertEqual(cpu["user_usage"], 0.009918) - self.assertEqual(cpu["system_usage"], 0.00089) - self.assertEqual(cpu["throttling_count"], 0) - self.assertEqual(cpu["throttling_time"], 0) - - memory = data.get("memory", None) - self.assertTrue(memory) - self.assertEqual(memory["active_anon"], 78721024) - self.assertEqual(memory["active_file"], 18501632) - self.assertEqual(memory["inactive_anon"], 0) - self.assertEqual(memory["inactive_file"], 71684096) - self.assertEqual(memory["total_cache"], 90185728) - self.assertEqual(memory["total_rss"], 78721024) - self.assertEqual(memory["usage"], 193769472) - self.assertEqual(memory["max_usage"], 195305472) - self.assertEqual(memory["limit"], 536870912) - - blkio = data.get("blkio", None) - self.assertTrue(blkio) - self.assertEqual(blkio["blk_read"], 0) - self.assertEqual(blkio["blk_write"], 128352256) - - # Second report should report the delta (in the test case, nothing) - data = plugin_second_report["data"] - self.assertIn("cpu", data) - self.assertEqual(len(data["cpu"]), 0) - self.assertIn("memory", data) - self.assertEqual(len(data["memory"]), 0) - self.assertIn("blkio", data) - self.assertEqual(len(data["blkio"]), 1) - self.assertEqual(data["blkio"]['blk_write'], 0) - self.assertNotIn('blk_read', data["blkio"]) - - def test_no_instana_zone(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.zone) - - def test_instana_zone(self): - os.environ["INSTANA_ZONE"] = "YellowDog" - self.create_agent_and_setup_tracer() - - self.assertEqual(self.agent.options.zone, "YellowDog") - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - plugins = payload['metrics']['plugins'] - self.assertIsInstance(plugins, list) - - task_plugin = None - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.aws.ecs.task": - task_plugin = plugin - - self.assertTrue(task_plugin) - self.assertIn("data", task_plugin) - self.assertIn("instanaZone", task_plugin["data"]) - self.assertEqual(task_plugin["data"]["instanaZone"], "YellowDog") - - def test_custom_tags(self): - os.environ["INSTANA_TAGS"] = "love,war=1,games" - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertDictEqual(self.agent.options.tags, {"love": None, "war": "1", "games": None}) - - payload = self.agent.collector.prepare_payload() - - self.assertTrue(payload) - task_plugin = None - plugins = payload['metrics']['plugins'] - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.aws.ecs.task": - task_plugin = plugin - self.assertTrue(task_plugin) - self.assertIn("tags", task_plugin["data"]) - tags = task_plugin["data"]["tags"] - self.assertEqual(tags["war"], "1") - self.assertIsNone(tags["love"]) - self.assertIsNone(tags["games"]) diff --git a/tests/platforms/test_host.py b/tests/platforms/test_host.py index 2ca09804..1eb07125 100644 --- a/tests/platforms/test_host.py +++ b/tests/platforms/test_host.py @@ -14,7 +14,6 @@ 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): @@ -22,7 +21,6 @@ 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() @@ -49,9 +47,7 @@ def tearDown(self): 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() diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py index a53c801e..48b6fd3d 100644 --- a/tests/platforms/test_host_collector.py +++ b/tests/platforms/test_host_collector.py @@ -7,7 +7,6 @@ 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 @@ -15,12 +14,12 @@ 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'): + 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() @@ -29,14 +28,18 @@ def setUp(self): self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + '3.8.0' def tearDown(self): - """ Reset all environment variables of consequence """ + """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" - ) + "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: @@ -50,9 +53,7 @@ def tearDown(self): 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() @@ -61,78 +62,102 @@ def test_prepare_payload_basics(self): 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']) + 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]) + 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" @@ -142,61 +167,62 @@ def test_prepare_payload_basics_disable_runtime_metrics(self): 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']) + 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): + 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.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']) + 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"]) @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_disabled_python_packages(self, mock_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.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) - + 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): @@ -206,18 +232,21 @@ def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): 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.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') + self.assertIn("m", snapshot) + self.assertEqual("Autowrapt", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertGreater(len(snapshot["versions"]), 5) + expected_packages = ("instana", "wrapt", "fysom") for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) - + 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): @@ -229,14 +258,18 @@ def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): 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.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') + self.assertIn("m", snapshot) + self.assertEqual("AutoTrace", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertGreater(len(snapshot["versions"]), 5) + expected_packages = ("instana", "wrapt", "fysom") for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) + self.assertIn( + package, + snapshot["versions"], + f"{package} not found in snapshot['versions']", + ) + self.assertEqual(snapshot["versions"]["instana"], VERSION) diff --git a/tests/platforms/test_lambda.py b/tests/platforms/test_lambda.py deleted file mode 100644 index a5a25c09..00000000 --- a/tests/platforms/test_lambda.py +++ /dev/null @@ -1,762 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import json -import time -import logging -import unittest - -import wrapt - -from instana.tracer import InstanaTracer -from instana.agent.aws_lambda import AWSLambdaAgent -from instana.options import AWSLambdaOptions -from instana.recorder import StanRecorder -from instana import lambda_handler -from instana import get_lambda_handler_or_default -from instana.instrumentation.aws.lambda_inst import lambda_handler_with_instana -from instana.instrumentation.aws.triggers import read_http_query_params -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.util.aws import normalize_aws_lambda_arn - - -# Mock Context object -class MockContext(dict): - def __init__(self, **kwargs): - super(MockContext, self).__init__(**kwargs) - self.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:1" - self.function_name = "TestPython" - self.function_version = "1" - - -# This is the target handler that will be instrumented for these tests -def my_lambda_handler(event, context): - # print("target_handler called") - return { - 'statusCode': 200, - 'headers': {'Content-Type': 'application/json'}, - 'body': json.dumps({'site': 'pwpush.com', 'response': 204}) - } - -# We only want to monkey patch the test handler once so do it here -os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" -module_name, function_name = get_lambda_handler_or_default() -wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) - -def my_errored_lambda_handler(event, context): - return { - 'statusCode': 500, - 'headers': {'Content-Type': 'application/json'}, - 'body': json.dumps({'site': 'wikipedia.org', 'response': 500}) - } - -os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" -module_name, function_name = get_lambda_handler_or_default() -wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) - -class TestLambda(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestLambda, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["AWS_EXECUTION_ENV"] = "AWS_Lambda_python_3.8" - os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - self.context = MockContext() - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "LAMBDA_HANDLER" in os.environ: - os.environ.pop("LAMBDA_HANDLER") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_SERVICE_NAME" in os.environ: - os.environ.pop("INSTANA_SERVICE_NAME") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSLambdaAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_invalid_options(self): - # None of the required env vars are available... - if "LAMBDA_HANDLER" in os.environ: - os.environ.pop("LAMBDA_HANDLER") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = AWSLambdaAgent() - self.assertFalse(agent._can_send) - self.assertIsNone(agent.collector) - - 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_has_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, AWSLambdaOptions)) - self.assertDictEqual(self.agent.options.endpoint_proxy, { }) - - def test_get_handler(self): - os.environ["LAMBDA_HANDLER"] = "tests.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_get_handler_with_multi_subpackages(self): - os.environ["LAMBDA_HANDLER"] = "tests.one.two.three.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.one.two.three", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_get_handler_with_space_in_it(self): - os.environ["LAMBDA_HANDLER"] = " tests.another_module.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.another_module", handler_module) - self.assertEqual("lambda_handler", handler_function) - - os.environ["LAMBDA_HANDLER"] = "tests.another_module.lambda_handler " - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.another_module", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertEqual(should_headers, self.agent.options.extra_http_headers) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, { 'https': "http://myproxy.123" }) - - def test_custom_service_name(self): - os.environ['INSTANA_SERVICE_NAME'] = "Legion" - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - os.environ.pop('INSTANA_SERVICE_NAME') - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertTrue(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - - self.assertEqual('Legion', span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_api_gateway_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertTrue(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_api_gateway_v2_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - time.sleep(1) - payload = self.agent.collector.prepare_payload() - self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) - - self.assertEqual(200, result['statusCode']) - span = payload['spans'][0] - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - self.assertEqual(200, span.data['http']['status']) - - - def test_api_gateway_v2_trigger_errored_tracing(self): - - with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: - event = json.load(json_file) - - os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" - self.create_agent_and_setup_tracer() - - result = lambda_handler(event, self.context) - time.sleep(1) - payload = self.agent.collector.prepare_payload() - self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) - - self.assertEqual(500, result['statusCode']) - span = payload['spans'][0] - self.assertEqual(1, span.ec) - self.assertEqual('HTTP status 500', span.data['lambda']['error']) - self.assertEqual(500, span.data['http']['status']) - - - def test_application_lb_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertTrue(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_cloudwatch_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/cloudwatch_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertIsNone(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:cloudwatch.events', span.data['lambda']['trigger']) - self.assertEqual('cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', span.data["lambda"]["cw"]["events"]["id"]) - self.assertEqual(False, span.data["lambda"]["cw"]["events"]["more"]) - self.assertTrue(isinstance(span.data["lambda"]["cw"]["events"]["resources"], list)) - self.assertEqual(1, len(span.data["lambda"]["cw"]["events"]["resources"])) - self.assertEqual('arn:aws:events:eu-west-1:123456789012:rule/ExampleRule', - span.data["lambda"]["cw"]["events"]["resources"][0]) - - def test_cloudwatch_logs_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/cloudwatch_logs_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertIsNone(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:cloudwatch.logs', span.data['lambda']['trigger']) - self.assertFalse("decodingError" in span.data['lambda']['cw']['logs']) - self.assertEqual('testLogGroup', span.data['lambda']['cw']['logs']['group']) - self.assertEqual('testLogStream', span.data['lambda']['cw']['logs']['stream']) - self.assertEqual(None, span.data['lambda']['cw']['logs']['more']) - self.assertTrue(isinstance(span.data['lambda']['cw']['logs']['events'], list)) - self.assertEqual(2, len(span.data['lambda']['cw']['logs']['events'])) - self.assertEqual('[ERROR] First test message', span.data['lambda']['cw']['logs']['events'][0]) - self.assertEqual('[ERROR] Second test message', span.data['lambda']['cw']['logs']['events'][1]) - - def test_s3_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/s3_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertIsNone(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:s3', span.data['lambda']['trigger']) - self.assertTrue(isinstance(span.data["lambda"]["s3"]["events"], list)) - events = span.data["lambda"]["s3"]["events"] - self.assertEqual(1, len(events)) - event = events[0] - self.assertEqual('ObjectCreated:Put', event['event']) - self.assertEqual('example-bucket', event['bucket']) - self.assertEqual('test/key', event['object']) - - def test_sqs_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/sqs_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertIsNone(span.sy) - - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:sqs', span.data['lambda']['trigger']) - self.assertTrue(isinstance(span.data["lambda"]["sqs"]["messages"], list)) - messages = span.data["lambda"]["sqs"]["messages"] - self.assertEqual(1, len(messages)) - message = messages[0] - self.assertEqual('arn:aws:sqs:us-west-1:123456789012:MyQueue', message['queue']) - - def test_read_query_params(self): - event = { "queryStringParameters": {"foo": "bar" }, - "multiValueQueryStringParameters": { "foo": ["bar"] } } - params = read_http_query_params(event) - self.assertEqual("foo=['bar']", params) - - def test_read_query_params_with_none_data(self): - event = { "queryStringParameters": None, - "multiValueQueryStringParameters": None } - params = read_http_query_params(event) - self.assertEqual("", params) - - def test_read_query_params_with_bad_event(self): - event = None - params = read_http_query_params(event) - self.assertEqual("", params) - - def test_arn_parsing(self): - ctx = MockContext() - - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") - - # Without version should return a fully qualified ARN (with version) - ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython" - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") - - # Fully qualified already with the '$LATEST' special tag - ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST") - - 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_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def __validate_result_and_payload_for_gateway_v2_trace(self, result, payload): - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - self.assertIn('statusCode', result) - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('0000000000001234', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0000000000004567', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertTrue(span.sy) - - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual('/my/path', span.data['http']['url']) - self.assertEqual('/my/{resource}', span.data['http']['path_tpl']) - self.assertEqual("secret=key&q=term", span.data['http']['params']) diff --git a/tests/propagators/test_binary_propagator.py b/tests/propagators/test_binary_propagator.py index d96b97a3..efd3fb42 100644 --- a/tests/propagators/test_binary_propagator.py +++ b/tests/propagators/test_binary_propagator.py @@ -1,73 +1,180 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 +from typing import Generator + +import pytest +from opentelemetry.trace import ( + format_span_id, + format_trace_id, +) + from instana.propagators.binary_propagator import BinaryPropagator from instana.span_context import SpanContext -import unittest -class TestBinaryPropagator(unittest.TestCase): - def setUp(self): +class TestBinaryPropagator: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup self.bp = BinaryPropagator() + yield - def test_inject_carrier_dict(self): + def test_inject_carrier_dict(self, trace_id: int, span_id: int) -> None: carrier = {} - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier) - self.assertEqual(carrier[b'x-instana-t'], b"1234d0e0e4736234") - def test_inject_carrier_dict_w3c_True(self): + assert carrier[b"x-instana-t"] == str(trace_id).encode("utf-8") + assert carrier[b"x-instana-s"] == str(span_id).encode("utf-8") + assert carrier[b"x-instana-l"] == b"1" + assert carrier[b"server-timing"] == f"intid;desc={trace_id}".encode("utf-8") + + def test_inject_carrier_dict_w3c_True(self, trace_id: int, span_id: int) -> None: carrier = {} - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier, disable_w3c_trace_context=False) - self.assertEqual(carrier[b'x-instana-t'], b"1234d0e0e4736234") - self.assertEqual(carrier[b'traceparent'], b'00-00000000000000001234d0e0e4736234-1234567890abcdef-01') - self.assertEqual(carrier[b'tracestate'], b'in=1234d0e0e4736234;1234567890abcdef') - def test_inject_carrier_list(self): + assert carrier[b"x-instana-t"] == str(trace_id).encode("utf-8") + assert carrier[b"x-instana-s"] == str(span_id).encode("utf-8") + assert carrier[b"x-instana-l"] == b"1" + assert carrier[b"server-timing"] == f"intid;desc={trace_id}".encode("utf-8") + assert carrier[ + b"traceparent" + ] == f"00-{format_trace_id(trace_id)}-{format_span_id(span_id)}-01".encode( + "utf-8" + ) + assert carrier[b"tracestate"] == f"in={trace_id};{span_id}".encode("utf-8") + + def test_inject_carrier_list(self, trace_id: int, span_id: int) -> None: carrier = [] - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier) - self.assertEqual(carrier[0], (b'x-instana-t', b'1234d0e0e4736234')) - def test_inject_carrier_list_w3c_True(self): + assert isinstance(carrier, list) + assert carrier[0] == (b"x-instana-t", str(trace_id).encode("utf-8")) + assert carrier[1] == (b"x-instana-s", str(span_id).encode("utf-8")) + assert carrier[2] == (b"x-instana-l", b"1") + assert carrier[3] == ( + b"server-timing", + f"intid;desc={trace_id}".encode("utf-8"), + ) + + def test_inject_carrier_list_w3c_True(self, trace_id: int, span_id: int) -> None: carrier = [] - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier, disable_w3c_trace_context=False) - self.assertEqual(carrier[2], (b'x-instana-t', b'1234d0e0e4736234')) - self.assertEqual(carrier[0], (b'traceparent', b'00-00000000000000001234d0e0e4736234-1234567890abcdef-01')) - self.assertEqual(carrier[1], (b'tracestate', b'in=1234d0e0e4736234;1234567890abcdef')) - def test_inject_carrier_tupple(self): + assert isinstance(carrier, list) + assert carrier[0] == ( + b"traceparent", + f"00-{format_trace_id(trace_id)}-{format_span_id(span_id)}-01".encode( + "utf-8" + ), + ) + assert carrier[1] == (b"tracestate", f"in={trace_id};{span_id}".encode("utf-8")) + assert carrier[2] == (b"x-instana-t", str(trace_id).encode("utf-8")) + assert carrier[3] == (b"x-instana-s", str(span_id).encode("utf-8")) + assert carrier[4] == (b"x-instana-l", b"1") + assert carrier[5] == ( + b"server-timing", + f"intid;desc={trace_id}".encode("utf-8"), + ) + + def test_inject_carrier_tuple(self, trace_id: int, span_id: int) -> None: carrier = () - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier) - self.assertEqual(carrier[0], (b'x-instana-t', b'1234d0e0e4736234')) - def test_inject_carrier_tupple_w3c_True(self): + assert isinstance(carrier, tuple) + assert carrier[0] == (b"x-instana-t", str(trace_id).encode("utf-8")) + assert carrier[1] == (b"x-instana-s", str(span_id).encode("utf-8")) + assert carrier[2] == (b"x-instana-l", b"1") + assert carrier[3] == ( + b"server-timing", + f"intid;desc={trace_id}".encode("utf-8"), + ) + + def test_inject_carrier_tuple_w3c_True(self, trace_id: int, span_id: int) -> None: carrier = () - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier, disable_w3c_trace_context=False) - self.assertEqual(carrier[2], (b'x-instana-t', b'1234d0e0e4736234')) - self.assertEqual(carrier[0], (b'traceparent', b'00-00000000000000001234d0e0e4736234-1234567890abcdef-01')) - self.assertEqual(carrier[1], (b'tracestate', b'in=1234d0e0e4736234;1234567890abcdef')) - def test_inject_carrier_set_exception(self): + assert isinstance(carrier, tuple) + assert carrier[0] == ( + b"traceparent", + f"00-{format_trace_id(trace_id)}-{format_span_id(span_id)}-01".encode( + "utf-8" + ), + ) + assert carrier[1] == (b"tracestate", f"in={trace_id};{span_id}".encode("utf-8")) + assert carrier[2] == (b"x-instana-t", str(trace_id).encode("utf-8")) + assert carrier[3] == (b"x-instana-s", str(span_id).encode("utf-8")) + assert carrier[4] == (b"x-instana-l", b"1") + assert carrier[5] == ( + b"server-timing", + f"intid;desc={trace_id}".encode("utf-8"), + ) + + def test_inject_carrier_set_exception(self, trace_id: int, span_id: int) -> None: carrier = set() - ctx = SpanContext(span_id="1234567890abcdef", trace_id="1234d0e0e4736234", - level=1, baggage={}, sampled=True, - synthetic=False) + ctx = SpanContext( + span_id=span_id, + trace_id=trace_id, + is_remote=False, + level=1, + baggage={}, + sampled=True, + synthetic=False, + ) carrier = self.bp.inject(ctx, carrier) - self.assertIsNone(carrier) \ No newline at end of file + assert not carrier diff --git a/tests/propagators/test_http_propagator.py b/tests/propagators/test_http_propagator.py index 42284d44..ae4af3b6 100644 --- a/tests/propagators/test_http_propagator.py +++ b/tests/propagators/test_http_propagator.py @@ -1,364 +1,341 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 +import os +from typing import Any, Dict, Generator + +import pytest +from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + format_span_id, + format_trace_id, +) + from instana.propagators.http_propagator import HTTPPropagator -from instana.w3c_trace_context.traceparent import Traceparent from instana.span_context import SpanContext -from mock import patch -import os -import unittest +from instana.util.ids import header_to_id -class TestHTTPPropagatorTC(unittest.TestCase): - def setUp(self): +class TestHTTPPropagator: + @pytest.fixture(autouse=True) + def _resources(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup self.hptc = HTTPPropagator() - - def tearDown(self): - """ Clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable """ + yield + # teardown + # Clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "" - @patch.object(Traceparent, "get_traceparent_fields") - @patch.object(Traceparent, "validate") - def test_extract_carrier_dict(self, mock_validate, mock_get_traceparent_fields): - carrier = { - 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', - 'tracestate': 'congo=t61rcWkgMzE', - 'X-INSTANA-T': '1234d0e0e4736234', - 'X-INSTANA-S': '1234567890abcdef', - 'X-INSTANA-L': '1, correlationType=web; correlationId=1234567890abcdef' - } - mock_validate.return_value = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - mock_get_traceparent_fields.return_value = ["00", "4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", True] - ctx = self.hptc.extract(carrier) - self.assertEqual(ctx.correlation_id, '1234567890abcdef') - self.assertEqual(ctx.correlation_type, "web") - self.assertIsNone(ctx.instana_ancestor) - self.assertEqual(ctx.level, 1) - self.assertEqual(ctx.long_trace_id, "4bf92f3577b34da6a3ce929d0e0e4736") - self.assertEqual(ctx.span_id, "00f067aa0ba902b7") - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx.trace_id, "a3ce929d0e0e4736") # 16 last chars from traceparent trace_id - self.assertTrue(ctx.trace_parent) - self.assertEqual(ctx.traceparent, '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01') - self.assertEqual(ctx.tracestate, 'congo=t61rcWkgMzE') - - @patch.object(Traceparent, "get_traceparent_fields") - @patch.object(Traceparent, "validate") - def test_extract_carrier_list(self, mock_validate, mock_get_traceparent_fields): - carrier = [('user-agent', 'python-requests/2.23.0'), ('accept-encoding', 'gzip, deflate'), - ('accept', '*/*'), ('connection', 'keep-alive'), - ('traceparent', '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'), - ('tracestate', 'congo=t61rcWkgMzE'), - ('X-INSTANA-T', '1234d0e0e4736234'), - ('X-INSTANA-S', '1234567890abcdef'), - ('X-INSTANA-L', '1')] - - mock_validate.return_value = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - mock_get_traceparent_fields.return_value = ["00", "4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", True] - ctx = self.hptc.extract(carrier) - self.assertIsNone(ctx.correlation_id) - self.assertIsNone(ctx.correlation_type) - self.assertIsNone(ctx.instana_ancestor) - self.assertEqual(ctx.level, 1) - self.assertIsNone(ctx.long_trace_id) - self.assertEqual(ctx.span_id, "1234567890abcdef") - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx.trace_id, "1234d0e0e4736234") # 16 last chars from traceparent trace_id - self.assertIsNone(ctx.trace_parent) - self.assertEqual(ctx.traceparent, '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01') - self.assertEqual(ctx.tracestate, 'congo=t61rcWkgMzE') - - @patch.object(Traceparent, "validate") - def test_extract_carrier_dict_validate_Exception_None_returned(self, mock_validate): - """ - In this test case the traceparent header fails the validation, so traceparent and tracestate are not gonna used - Additionally because in the instana L header the correlation flags are present we need to start a new ctx and - the present values of 'X-INSTANA-T', 'X-INSTANA-S' headers should no be used. This means the ctx should be None - :param mock_validate: - :return: - """ + @pytest.fixture(scope="function") + def _instana_long_tracer_id(self) -> str: + return "4bf92f3577b34da6a3ce929d0e0e4736" + + @pytest.fixture(scope="function") + def _instana_span_id(self) -> str: + return "00f067aa0ba902b7" + + @pytest.fixture(scope="function") + def _trace_id(self, _instana_long_tracer_id: str) -> int: + return int(_instana_long_tracer_id[-16:], 16) + + @pytest.fixture(scope="function") + def _span_id(self, _instana_span_id: str) -> int: + return int(_instana_span_id, 16) + + @pytest.fixture(scope="function") + def _long_tracer_id(self, _instana_long_tracer_id: str) -> int: + return int(_instana_long_tracer_id, 16) + + @pytest.fixture(scope="function") + def _traceparent(self, _instana_long_tracer_id: str, _instana_span_id: str) -> str: + return f"00-{_instana_long_tracer_id}-{_instana_span_id}-01" + + @pytest.fixture(scope="function") + def _tracestate(self) -> str: + return "congo=t61rcWkgMzE" + + def test_extract_carrier_dict( + self, + trace_id: int, + span_id: int, + _instana_long_tracer_id: str, + _instana_span_id: str, + _long_tracer_id: int, + _trace_id: int, + _span_id: int, + _traceparent: str, + _tracestate: str, + ) -> None: carrier = { - 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', - 'tracestate': 'congo=t61rcWkgMzE', - 'X-INSTANA-T': '1234d0e0e4736234', - 'X-INSTANA-S': '1234567890abcdef', - 'X-INSTANA-L': '1, correlationType=web; correlationId=1234567890abcdef' + "traceparent": _traceparent, + "tracestate": _tracestate, + "X-INSTANA-T": f"{trace_id}", + "X-INSTANA-S": f"{span_id}", + "X-INSTANA-L": f"1, correlationType=web; correlationId={span_id}", } - mock_validate.return_value = None - ctx = self.hptc.extract(carrier) - self.assertTrue(isinstance(ctx, SpanContext)) - assert ctx.trace_id is None - assert ctx.span_id is None - assert ctx.synthetic is False - self.assertEqual(ctx.correlation_id, "1234567890abcdef") - self.assertEqual(ctx.correlation_type, "web") - - @patch.object(Traceparent, "validate") - def test_extract_fake_exception(self, mock_validate): - carrier = { - 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', - 'tracestate': 'congo=t61rcWkgMzE', - 'X-INSTANA-T': '1234d0e0e4736234', - 'X-INSTANA-S': '1234567890abcdef', - 'X-INSTANA-L': '1, correlationType=web; correlationId=1234567890abcdef' - } - mock_validate.side_effect = Exception - ctx = self.hptc.extract(carrier) - self.assertIsNone(ctx) - - @patch.object(Traceparent, "get_traceparent_fields") - @patch.object(Traceparent, "validate") - def test_extract_carrier_dict_corrupted_level_header(self, mock_validate, mock_get_traceparent_fields): - """ - In this test case the traceparent header fails the validation, so traceparent and tracestate are not gonna used - Additionally because in the instana L header the correlation flags are present we need to start a new ctx and - the present values of 'X-INSTANA-T', 'X-INSTANA-S' headers should no be used. This means the ctx should be None - :param mock_validate: - :return: - """ - carrier = { - 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', - 'tracestate': 'congo=t61rcWkgMzE', - 'X-INSTANA-T': '1234d0e0e4736234', - 'X-INSTANA-S': '1234567890abcdef', - 'X-INSTANA-L': '1, correlationTypeweb; correlationId1234567890abcdef' - } - mock_validate.return_value = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - mock_get_traceparent_fields.return_value = ["00", "4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", True] - ctx = self.hptc.extract(carrier) - self.assertIsNone(ctx.correlation_id) - self.assertIsNone(ctx.correlation_type) - self.assertIsNone(ctx.instana_ancestor) - self.assertEqual(ctx.level, 1) - self.assertEqual(ctx.long_trace_id, '4bf92f3577b34da6a3ce929d0e0e4736') - self.assertEqual(ctx.span_id, "00f067aa0ba902b7") - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx.trace_id, "a3ce929d0e0e4736") # 16 last chars from traceparent trace_id - self.assertTrue(ctx.trace_parent) - self.assertEqual(ctx.traceparent, '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01') - self.assertEqual(ctx.tracestate, 'congo=t61rcWkgMzE') - - @patch.object(Traceparent, "get_traceparent_fields") - @patch.object(Traceparent, "validate") - def test_extract_carrier_dict_level_header_not_splitable(self, mock_validate, mock_get_traceparent_fields): - """ - In this test case the traceparent header fails the validation, so traceparent and tracestate are not gonna used - Additionally because in the instana L header the correlation flags are present we need to start a new ctx and - the present values of 'X-INSTANA-T', 'X-INSTANA-S' headers should no be used. This means the ctx should be None - :param mock_validate: - :return: - """ - carrier = { - 'traceparent': '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', - 'tracestate': 'congo=t61rcWkgMzE', - 'X-INSTANA-T': '1234d0e0e4736234', - 'X-INSTANA-S': '1234567890abcdef', - 'X-INSTANA-L': ['1'] - } - mock_validate.return_value = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - mock_get_traceparent_fields.return_value = ["00", "4bf92f3577b34da6a3ce929d0e0e4736", "00f067aa0ba902b7", True] - ctx = self.hptc.extract(carrier) - self.assertIsNone(ctx.correlation_id) - self.assertIsNone(ctx.correlation_type) - self.assertIsNone(ctx.instana_ancestor) - self.assertEqual(ctx.level, 1) - self.assertIsNone(ctx.long_trace_id) - self.assertEqual(ctx.span_id, "1234567890abcdef") - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx.trace_id, "1234d0e0e4736234") - self.assertIsNone(ctx.trace_parent) - self.assertEqual(ctx.traceparent, '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01') - self.assertEqual(ctx.tracestate, 'congo=t61rcWkgMzE') - - - # 28 in the tracer_compliance_test_cases.json - # "Scenario/incoming headers": "w3c off, only X-INSTANA-L=0", - def test_w3c_off_only_x_instana_l_0(self): - carrier = { - 'X-INSTANA-L': '0' - } - os.environ['INSTANA_DISABLE_W3C_TRACE_CORRELATION'] = 'yes_please' - ctx = self.hptc.extract(carrier) - - # Assert that the level is (zero) int, not str - self.assertEqual(ctx.level, 0) - # Assert that the suppression is on - self.assertTrue(ctx.suppression) - - # Assert that the rest of the attributes are on their default value - self.assertTrue(ctx.sampled) - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx._baggage, {}) - self.assertTrue( - all(map(lambda x: x is None, - (ctx.correlation_id, ctx.trace_id, ctx.span_id, - ctx.trace_parent, ctx.instana_ancestor, - ctx.long_trace_id, ctx.correlation_type, - ctx.correlation_id, ctx.traceparent, ctx.tracestate) - ))) - - # Simulate the sideffect of starting a span, - # getting a trace_id and span_id: - ctx.trace_id = ctx.span_id = '4dfe94d65496a02c' - - # Test propagation - downstream_carrier = {} - - self.hptc.inject(ctx, downstream_carrier) + ctx = self.hptc.extract(carrier) - # Assert that 'X-INSTANA-L' has been injected with the correct 0 value - self.assertIn('X-INSTANA-L', downstream_carrier) - self.assertEqual(downstream_carrier.get('X-INSTANA-L'), '0') + assert ctx.correlation_id == str(span_id) + assert ctx.correlation_type == "web" + assert not ctx.instana_ancestor + assert ctx.level == 1 + assert ctx.long_trace_id == header_to_id(_instana_long_tracer_id) + assert ctx.span_id == _span_id + assert not ctx.synthetic + assert ctx.trace_id == _trace_id + assert ctx.trace_parent + assert ctx.traceparent == f"00-{_instana_long_tracer_id}-{_instana_span_id}-01" + assert ctx.tracestate == _tracestate + + def test_extract_carrier_list( + self, + trace_id: int, + span_id: int, + _instana_long_tracer_id: str, + _instana_span_id: str, + _traceparent: str, + _tracestate: str, + ) -> None: + carrier = [ + ("user-agent", "python-requests/2.23.0"), + ("accept-encoding", "gzip, deflate"), + ("accept", "*/*"), + ("connection", "keep-alive"), + ("traceparent", _traceparent), + ("tracestate", _tracestate), + ("X-INSTANA-T", f"{trace_id}"), + ("X-INSTANA-S", f"{span_id}"), + ("X-INSTANA-L", "1"), + ] - self.assertIn('traceparent', downstream_carrier) - self.assertEqual('00-0000000000000000' + ctx.trace_id + '-' + ctx.span_id + '-00', - downstream_carrier.get('traceparent')) + ctx = self.hptc.extract(carrier) + assert not ctx.correlation_id + assert not ctx.correlation_type + assert not ctx.instana_ancestor + assert ctx.level == 1 + assert not ctx.long_trace_id + assert ctx.span_id == span_id + assert not ctx.synthetic + assert ctx.trace_id == trace_id + assert not ctx.trace_parent + assert ctx.traceparent == f"00-{_instana_long_tracer_id}-{_instana_span_id}-01" + assert ctx.tracestate == _tracestate + + def test_extract_carrier_dict_validate_Exception_None_returned( + self, + trace_id: int, + span_id: int, + _tracestate: str, + ) -> None: + # In this test case, the traceparent header fails the validation, so + # traceparent and tracestate are not used. + # Additionally, because the correlation flags are present in the + # 'X-INSTANA-L' header, we need to start a new SpanContext, and the + # present values of 'X-INSTANA-T' and 'X-INSTANA-S' headers should not + # be used. - # 29 in the tracer_compliance_test_cases.json - # "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus -T and -S", - def test_w3c_off_x_instana_l_0_plus_t_and_s(self): - os.environ['INSTANA_DISABLE_W3C_TRACE_CORRELATION'] = 'w3c_trace_correlation_stinks' carrier = { - 'X-INSTANA-T': 'fa2375d711a4ca0f', - 'X-INSTANA-S': '37cb2d6e9b1c078a', - 'X-INSTANA-L': '0' + "traceparent": "00-4gf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'", # the long-trace-id is malformed to be invalid. + "tracestate": _tracestate, + "X-INSTANA-T": f"{trace_id}", + "X-INSTANA-S": f"{span_id}", + "X-INSTANA-L": f"1, correlationType=web; correlationId={span_id}", } ctx = self.hptc.extract(carrier) - # Assert that the level is (zero) int, not str - self.assertEqual(ctx.level, 0) - # Assert that the suppression is on - self.assertTrue(ctx.suppression) - - # Assert that the rest of the attributes are on their default value - # And even T and S are None - self.assertTrue(ctx.sampled) - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx._baggage, {}) - - self.assertTrue( - all(map(lambda x: x is None, - (ctx.correlation_id, ctx.trace_id, ctx.span_id, - ctx.trace_parent, ctx.instana_ancestor, - ctx.long_trace_id, ctx.correlation_type, - ctx.correlation_id, ctx.traceparent, ctx.tracestate) - ))) - - # Simulate the sideffect of starting a span, - # getting a trace_id and span_id: - ctx.trace_id = ctx.span_id = '4dfe94d65496a02c' - - # Test propagation - downstream_carrier = {} - - self.hptc.inject(ctx, downstream_carrier) - - # Assert that 'X-INSTANA-L' has been injected with the correct 0 value - self.assertIn('X-INSTANA-L', downstream_carrier) - self.assertEqual(downstream_carrier.get('X-INSTANA-L'), '0') - - self.assertIn('traceparent', downstream_carrier) - self.assertEqual('00-0000000000000000' + ctx.trace_id + '-' + ctx.span_id + '-00', - downstream_carrier.get('traceparent')) - - + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == INVALID_TRACE_ID + assert ctx.span_id == INVALID_SPAN_ID + assert not ctx.synthetic + assert ctx.correlation_id == str(span_id) + assert ctx.correlation_type == "web" + + def test_extract_fake_exception( + self, + trace_id: int, + span_id: int, + _tracestate: str, + mocker, + ) -> None: + carrier = { + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e-00f067aa0ba902b7-01", + "tracestate": _tracestate, + "X-INSTANA-T": f"{trace_id}", + "X-INSTANA-S": f"{span_id}", + "X-INSTANA-L": f"1, correlationType=web; correlationId={span_id}", + } + with pytest.raises(Exception): + ctx = self.hptc.extract(carrier) + assert not ctx + + def test_extract_carrier_dict_corrupted_level_header( + self, + trace_id: int, + span_id: int, + _instana_long_tracer_id: str, + _trace_id: int, + _span_id: int, + _traceparent: str, + _tracestate: str, + ) -> None: + # In this test case, the 'X-INSTANA-L' header is corrupted - # 30 in the tracer_compliance_test_cases.json - # "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus traceparent", - def test_w3c_off_x_instana_l_0_plus_traceparent(self): - os.environ['INSTANA_DISABLE_W3C_TRACE_CORRELATION'] = 'w3c_trace_correlation_stinks' carrier = { - 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'X-INSTANA-L': '0' + "traceparent": _traceparent, + "tracestate": _tracestate, + "X-INSTANA-T": f"{trace_id}", + "X-INSTANA-S": f"{span_id}", + "X-INSTANA-L": f"1, correlationType=web; correlationId{span_id}", } ctx = self.hptc.extract(carrier) - # Assert that the level is (zero) int, not str - self.assertEqual(ctx.level, 0) - # Assert that the suppression is on - self.assertTrue(ctx.suppression) - # Assert that the traceparent is not None - self.assertIsNotNone(ctx.traceparent) - - # Assert that the rest of the attributes are on their default value - self.assertTrue(ctx.sampled) - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx._baggage, {}) - - self.assertTrue( - all(map(lambda x: x is None, - (ctx.correlation_id, ctx.trace_id, ctx.span_id, - ctx.instana_ancestor, ctx.long_trace_id, ctx.correlation_type, - ctx.correlation_id, ctx.tracestate) - ))) - - # Simulate the sideffect of starting a span, - # getting a trace_id and span_id: - ctx.trace_id = ctx.span_id = '4dfe94d65496a02c' - - # Test propagation - downstream_carrier = {} - self.hptc.inject(ctx, downstream_carrier) - - # Assert that 'X-INSTANA-L' has been injected with the correct 0 value - self.assertIn('X-INSTANA-L', downstream_carrier) - self.assertEqual(downstream_carrier.get('X-INSTANA-L'), '0') - # Assert that the traceparent is propagated - self.assertIn('traceparent', downstream_carrier) - self.assertEqual('00-0af7651916cd43dd8448eb211c80319c-' + ctx.trace_id + '-00', - downstream_carrier.get('traceparent')) - - - # 31 in the tracer_compliance_test_cases.json - # "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus traceparent and tracestate", - def test_w3c_off_x_instana_l_0_plus_traceparent_and_tracestate(self): - os.environ['INSTANA_DISABLE_W3C_TRACE_CORRELATION'] = 'w3c_trace_correlation_stinks' + assert not ctx.correlation_id + assert ctx.correlation_type == "web" + assert not ctx.instana_ancestor + assert ctx.level == 1 + assert ctx.long_trace_id == header_to_id(_instana_long_tracer_id) + assert ctx.span_id == _span_id + assert not ctx.synthetic + assert ctx.trace_id == _trace_id + assert ctx.trace_parent + assert ctx.traceparent == _traceparent + assert ctx.tracestate == _tracestate + + def test_extract_carrier_dict_level_header_not_splitable( + self, + trace_id: int, + span_id: int, + _traceparent: str, + _tracestate: str, + ) -> None: carrier = { - 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', - 'tracestate': 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7', - 'X-INSTANA-L': '0' + "traceparent": _traceparent, + "tracestate": _tracestate, + "X-INSTANA-T": f"{trace_id}", + "X-INSTANA-S": f"{span_id}", + "X-INSTANA-L": ["1"], } ctx = self.hptc.extract(carrier) - # Assert that the level is (zero) int, not str - self.assertEqual(ctx.level, 0) - # Assert that the suppression is on - self.assertTrue(ctx.suppression) - # Assert that the traceparent is not None - self.assertIsNotNone(ctx.traceparent) - - # Assert that the rest of the attributes are on their default value - self.assertTrue(ctx.sampled) - self.assertFalse(ctx.synthetic) - self.assertEqual(ctx._baggage, {}) - - self.assertTrue( - all(map(lambda x: x is None, - (ctx.correlation_id, ctx.trace_id, ctx.span_id, - ctx.instana_ancestor, ctx.long_trace_id, ctx.correlation_type, - ctx.correlation_id) - ))) - - # Simulate the sideffect of starting a span, - # getting a trace_id and span_id: - ctx.trace_id = ctx.span_id = '4dfe94d65496a02c' + assert not ctx.correlation_id + assert not ctx.correlation_type + assert not ctx.instana_ancestor + assert ctx.level == 1 + assert not ctx.long_trace_id + assert ctx.span_id == span_id + assert not ctx.synthetic + assert ctx.trace_id == trace_id + assert not ctx.trace_parent + assert ctx.traceparent == _traceparent + assert ctx.tracestate == _tracestate + + # The following tests are based on the test cases defined in the + # tracer_compliance_test_cases.json file. + # + # Each line of the parametrize tuple correlates to a test case scenario: + # - scenario 28: "Scenario/incoming headers": "w3c off, only X-INSTANA-L=0" + # - scenario 29: "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus -T and -S" + # - scenario 30: "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus traceparent" + # - scenario 31: "Scenario/incoming headers": "w3c off, X-INSTANA-L=0 plus traceparent and tracestate", + @pytest.mark.parametrize( + "disable_w3c, carrier_header", + [ + ("yes_please", {"X-INSTANA-L": "0"}), + ( + "w3c_trace_correlation_stinks", + { + "X-INSTANA-T": "11803532876627986230", + "X-INSTANA-S": "67667974448284343", + "X-INSTANA-L": "0", + }, + ), + ( + "w3c_trace_correlation_stinks", + { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + "X-INSTANA-L": "0", + }, + ), + ( + "w3c_trace_correlation_stinks", + { + "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01", + "tracestate": "congo=ucfJifl5GOE,rojo=00f067aa0ba902b7", + "X-INSTANA-L": "0", + }, + ), + ], + ) + def test_w3c_off_x_instana_l_0( + self, + disable_w3c: str, + carrier_header: Dict[str, Any], + trace_id: int, + ) -> None: + os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = disable_w3c + + ctx = self.hptc.extract(carrier_header) + + # Assert the level is (zero) int, not str + assert isinstance(ctx.level, int) + assert ctx.level == 0 + + # Assert the suppression is on + assert ctx.suppression + + # Assert the rest of the attributes are on their default value + assert ctx.trace_id == INVALID_TRACE_ID + assert ctx.span_id == INVALID_SPAN_ID + assert not ctx.synthetic + assert not ctx.correlation_id + assert not ctx.trace_parent + assert not ctx.instana_ancestor + assert not ctx.long_trace_id + assert not ctx.correlation_type + assert not ctx.correlation_id + + # Assert that the traceparent is propagated when it is enabled + if "traceparent" in carrier_header.keys(): + assert ctx.traceparent + tp_trace_id = header_to_id(carrier_header["traceparent"].split("-")[1]) + else: + assert not ctx.traceparent + tp_trace_id = ctx.trace_id + + # Assert that the tracestate is propagated when it is enabled + if "tracestate" in carrier_header.keys(): + assert ctx.tracestate + else: + assert not ctx.tracestate + + # Simulate the side-effect of starting a span, getting a trace_id and span_id. + # Actually, with OTel API using a Tuple to store the SpanContext info, + # this will not change the values. + ctx.trace_id = ctx.span_id = trace_id # Test propagation downstream_carrier = {} + self.hptc.inject(ctx, downstream_carrier) - # Assert that 'X-INSTANA-L' has been injected with the correct 0 value - self.assertIn('X-INSTANA-L', downstream_carrier) - self.assertEqual(downstream_carrier.get('X-INSTANA-L'), '0') - # Assert that the traceparent is propagated - self.assertIn('traceparent', downstream_carrier) - self.assertEqual('00-0af7651916cd43dd8448eb211c80319c-' + ctx.trace_id + '-00', - downstream_carrier.get('traceparent')) - # Assert that the tracestate is propagated - self.assertIn('tracestate', downstream_carrier) - self.assertEqual(carrier['tracestate'], downstream_carrier['tracestate']) + # Assert the 'X-INSTANA-L' has been injected with the correct 0 value + assert "X-INSTANA-L" in downstream_carrier + assert downstream_carrier.get("X-INSTANA-L") == "0" + + assert "traceparent" in downstream_carrier + assert ( + downstream_carrier.get("traceparent") + == f"00-{format_trace_id(tp_trace_id)}-{format_span_id(ctx.span_id)}-00" + ) + + # Assert that the tracestate is propagated when it is enabled + if "tracestate" in carrier_header.keys(): + assert "tracestate" in downstream_carrier + assert carrier_header["tracestate"] == downstream_carrier["tracestate"] 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-with-tornado.txt b/tests/requirements-310-with-tornado.txt deleted file mode 100644 index d09e89ad..00000000 --- a/tests/requirements-310-with-tornado.txt +++ /dev/null @@ -1,8 +0,0 @@ -# pre 6.0 tornado would try to import 'MutableMapping' from 'collections' -# directly, and in Python 3.10 that doesn't work anymore, so that would fail with: -# venv/lib/python3.10/site-packages/tornado/httputil.py:107: in -# AttributeError: module 'collections' has no attribute 'MutableMapping' -# An alternative would be to disable this in testconf: -# collect_ignore_glob.append("*test_tornado*") -tornado>=6.1 --r requirements-310.txt \ No newline at end of file diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 22514153..10bcebf9 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,16 @@ 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>=6.4.1 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..b7dcbcb1 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,16 @@ 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>=6.4.1 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..32795005 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,19 @@ 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 +tornado>=6.4.1 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..1f9c9e3e 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 +tornado>=6.4.1 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..9a7d8891 --- /dev/null +++ b/tests/span/test_base_span.py @@ -0,0 +1,158 @@ +# (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, + "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["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..8c4a5148 --- /dev/null +++ b/tests/span/test_span.py @@ -0,0 +1,886 @@ +# (c) Copyright IBM Corp. 2024 + +import logging +import time +from typing import Generator +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 + + +class TestSpan: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.span = None + yield + if isinstance(self.span, InstanaSpan): + self.span.events.clear() + + def test_span_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + trace_id: int, + span_id: int, + ) -> None: + span_name = "test-span" + timestamp = time.time_ns() + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span is not None + assert isinstance(self.span, InstanaSpan) + assert self.span.name == span_name + + context = self.span.context + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + + assert self.span.start_time + assert isinstance(self.span.start_time, int) + assert self.span.start_time > timestamp + assert not self.span.end_time + assert not self.span.attributes + assert not self.span.events + assert self.span.is_recording() + assert self.span.status + assert self.span.status.is_unset + + def test_span_get_span_context( + self, + span_context: SpanContext, + span_processor: StanRecorder, + trace_id: int, + span_id: int, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + context = self.span.get_span_context() + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + assert context == self.span.context + + def test_span_set_attributes_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + self.span.set_attributes(attributes) + + assert self.span.attributes + assert len(self.span.attributes) == 2 + assert "field1" in self.span.attributes.keys() + assert "two" == self.span.attributes.get("field2") + + def test_span_set_attributes( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + + assert self.span.attributes + assert len(self.span.attributes) == 2 + assert "field1" in self.span.attributes.keys() + assert "two" == self.span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + self.span.set_attributes(attributes) + + assert len(self.span.attributes) == 4 + assert "field3" in self.span.attributes.keys() + assert "vier" in self.span.attributes.get("field4") + + def test_span_set_attribute_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + for key, value in attributes.items(): + self.span.set_attribute(key, value) + + assert self.span.attributes + assert len(self.span.attributes) == 2 + assert "field1" in self.span.attributes.keys() + assert "two" == self.span.attributes.get("field2") + + def test_span_set_attribute( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + + assert self.span.attributes + assert len(self.span.attributes) == 2 + assert "field1" in self.span.attributes.keys() + assert "two" == self.span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + for key, value in attributes.items(): + self.span.set_attribute(key, value) + + assert len(self.span.attributes) == 4 + assert "field3" in self.span.attributes.keys() + assert "vier" in self.span.attributes.get("field4") + + def test_span_update_name( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span-1" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span is not None + assert isinstance(self.span, InstanaSpan) + assert self.span.name == span_name + + new_span_name = "test-span-2" + self.span.update_name(new_span_name) + assert self.span is not None + assert isinstance(self.span, InstanaSpan) + assert self.span.name == new_span_name + + def test_span_set_status_with_Status_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.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" + in caplog.messages + ) + + self.span.set_status(span_status) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + def test_span_set_status_with_Status_and_desc( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.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" + in caplog.messages + ) + + set_status_desc = "Test" + self.span.set_status(span_status, set_status_desc) + excepted_log = f"Description {set_status_desc} ignored. Use either `Status` or `(StatusCode, Description)`" + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert excepted_log == caplog.record_tuples[1][2] + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + def test_span_set_status_with_StatusUNSET_to_StatusERROR( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + 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" + in caplog.messages + ) + + self.span = InstanaSpan( + span_name, span_context, span_processor, status=span_status + ) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + self.span.set_status(span_status) + + assert self.span.status + assert not self.span.status.is_unset + assert not self.span.status.is_ok + assert self.span.status.description == status_desc + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code == StatusCode.ERROR + + def test_span_set_status_with_StatusOK_to_StatusERROR( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + + 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" + in caplog.messages + ) + + self.span = InstanaSpan( + span_name, span_context, span_processor, status=span_status + ) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + self.span.set_status(span_status) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + def test_span_set_status_with_StatusCode_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + span_status_code = StatusCode(StatusCode.OK) + + self.span.set_status(span_status_code) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + def test_span_set_status_with_StatusCode_and_desc( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status_code = StatusCode(StatusCode.OK) + self.span.set_status(span_status_code, status_desc) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + in caplog.messages + ) + + def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + + 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" + in caplog.messages + ) + + self.span = InstanaSpan( + span_name, span_context, span_processor, status=span_status + ) + + assert self.span.status + assert self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code == StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + self.span.set_status(span_status_code, status_desc) + + assert self.span.status + assert not self.span.status.is_unset + assert not self.span.status.is_ok + assert self.span.status.description == status_desc + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code != StatusCode.OK + assert self.span.status.status_code == StatusCode.ERROR + + def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( + self, + span_context: SpanContext, + span_processor: StanRecorder, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.WARNING, logger="opentelemetry.trace.status") + + 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" + in caplog.messages + ) + + self.span = InstanaSpan( + span_name, span_context, span_processor, status=span_status + ) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + self.span.set_status(span_status_code, status_desc) + + assert self.span.status + assert not self.span.status.is_unset + assert self.span.status.is_ok + assert not self.span.status.description + assert self.span.status.status_code != StatusCode.UNSET + assert self.span.status.status_code == StatusCode.OK + assert self.span.status.status_code != StatusCode.ERROR + + def test_span_add_event_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.events + + event_name = "event1" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp = time.time_ns() + self.span.add_event(event_name, attributes, timestamp) + + assert self.span.events + assert len(self.span.events) == 1 + for event in self.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( + self, + 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) + self.span = InstanaSpan(span_name, span_context, span_processor, events=[event]) + + assert self.span.events + assert len(self.span.events) == 1 + for event in self.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() + self.span.add_event(event_name2, attributes, timestamp2) + + assert len(self.span.events) == 2 + for event in self.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( + self, + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + span_attribute: str, + ) -> None: + exception_msg = "Test Exception" + + exception = Exception(exception_msg) + self.span = InstanaSpan(span_name, span_context, span_processor) + + self.span.record_exception(exception) + + assert span_name == self.span.name + assert 1 == self.span.attributes.get("ec", 0) + if span_attribute: + assert span_attribute in self.span.attributes.keys() + assert exception_msg == self.span.attributes.get(span_attribute, None) + else: + event = self.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( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + exception_msg = "Test Exception" + attributes = { + "custom_attr": 0, + } + + exception = Exception(exception_msg) + self.span = InstanaSpan(span_name, span_context, span_processor) + + self.span.record_exception(exception, attributes) + + assert span_name == self.span.name + assert 1 == self.span.attributes.get("ec", 0) + + event = self.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( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "wsgi" + span_attribute = "http.error" + exception_msg = "Test Exception" + + exception = Exception() + exception.message = exception_msg + self.span = InstanaSpan(span_name, span_context, span_processor) + + self.span.record_exception(exception) + + assert span_name == self.span.name + assert 1 == self.span.attributes.get("ec", 0) + assert span_attribute in self.span.attributes.keys() + assert exception_msg == self.span.attributes.get(span_attribute, None) + + def test_span_record_exception_with_Exception_none_msg( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "wsgi" + span_attribute = "http.error" + + exception = Exception() + exception.message = None + self.span = InstanaSpan(span_name, span_context, span_processor) + + self.span.record_exception(exception) + + assert span_name == self.span.name + assert 1 == self.span.attributes.get("ec", 0) + assert span_attribute in self.span.attributes.keys() + assert "Exception()" == self.span.attributes.get(span_attribute, None) + + def test_span_record_exception_with_Exception_raised( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + + exception = None + self.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): + self.span.record_exception(exception) + + def test_span_end_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.end_time + + self.span.end() + + assert self.span.end_time + assert isinstance(self.span.end_time, int) + + def test_span_end( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.end_time + + timestamp_end = time.time_ns() + self.span.end(timestamp_end) + + assert self.span.end_time + assert self.span.end_time == timestamp_end + + def test_span_mark_as_errored_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 0 + + self.span.mark_as_errored() + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 1 + + def test_span_mark_as_errored( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 0 + + attributes = { + "field1": 1, + "field2": "two", + } + self.span.mark_as_errored(attributes) + + assert self.span.attributes + assert len(self.span.attributes) == 3 + assert self.span.attributes.get("ec") == 1 + assert "field1" in self.span.attributes.keys() + assert self.span.attributes.get("field2") == "two" + + self.span.mark_as_errored() + + assert self.span.attributes + assert len(self.span.attributes) == 3 + assert self.span.attributes.get("ec") == 2 + assert "field1" in self.span.attributes.keys() + assert self.span.attributes.get("field2") == "two" + + def test_span_mark_as_errored_exception( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + with patch( + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), + ): + self.span.mark_as_errored() + assert not self.span.attributes + + def test_span_assure_errored_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + self.span.assure_errored() + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 1 + + def test_span_assure_errored( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 0 + + self.span.assure_errored() + + assert self.span.attributes + assert len(self.span.attributes) == 1 + assert self.span.attributes.get("ec") == 1 + + def test_span_assure_errored_exception( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + with patch( + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), + ): + self.span.assure_errored() + assert not self.span.attributes + + def test_get_current_span(self, context: SpanContext) -> None: + self.span = get_current_span(context) + assert isinstance(self.span, InstanaSpan) + + def test_get_current_span_INVALID_SPAN(self) -> None: + self.span = get_current_span() + + assert self.span + assert self.span == INVALID_SPAN + + def test_span_duration_default( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.end_time + assert not self.span.duration + + self.span.end() + + assert self.span.end_time + assert self.span.duration + assert isinstance(self.span.duration, int) + assert self.span.duration > 0 + + def test_span_duration( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-span" + self.span = InstanaSpan(span_name, span_context, span_processor) + + assert not self.span.end_time + assert not self.span.duration + + timestamp_end = time.time_ns() + self.span.end(timestamp_end) + + assert self.span.end_time + assert self.span.end_time == timestamp_end + assert self.span.duration + assert isinstance(self.span.duration, int) + assert self.span.duration > 0 + assert self.span.duration == (timestamp_end - self.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..8ee9f9e2 --- /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"]["tags"]) + assert attributes == sdk_span.data["sdk"]["custom"]["tags"] + 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_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/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 diff --git a/tests/w3c_trace_context/test_traceparent.py b/tests/w3c_trace_context/test_traceparent.py index 6b18d7d6..3eb83a4e 100644 --- a/tests/w3c_trace_context/test_traceparent.py +++ b/tests/w3c_trace_context/test_traceparent.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 +import pytest from instana.w3c_trace_context.traceparent import Traceparent import unittest @@ -38,15 +39,15 @@ def test_validate_traceparent_None(self): def test_get_traceparent_fields(self): traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" version, trace_id, parent_id, sampled_flag = self.tp.get_traceparent_fields(traceparent) - self.assertEqual(trace_id, "4bf92f3577b34da6a3ce929d0e0e4736") - self.assertEqual(parent_id, "00f067aa0ba902b7") + self.assertEqual(trace_id, 11803532876627986230) + self.assertEqual(parent_id, 67667974448284343) self.assertTrue(sampled_flag) def test_get_traceparent_fields_unsampled(self): traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00" version, trace_id, parent_id, sampled_flag = self.tp.get_traceparent_fields(traceparent) - self.assertEqual(trace_id, "4bf92f3577b34da6a3ce929d0e0e4736") - self.assertEqual(parent_id, "00f067aa0ba902b7") + self.assertEqual(trace_id, 11803532876627986230) + self.assertEqual(parent_id, 67667974448284343) self.assertFalse(sampled_flag) def test_get_traceparent_fields_newer_version(self): @@ -54,15 +55,15 @@ def test_get_traceparent_fields_newer_version(self): # parts that we understand (and consider it valid). traceparent = "fe-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-12345-abcd" version, trace_id, parent_id, sampled_flag = self.tp.get_traceparent_fields(traceparent) - self.assertEqual(trace_id, "4bf92f3577b34da6a3ce929d0e0e4736") - self.assertEqual(parent_id, "00f067aa0ba902b7") + self.assertEqual(trace_id, 11803532876627986230) + self.assertEqual(parent_id, 67667974448284343) self.assertTrue(sampled_flag) def test_get_traceparent_fields_unknown_flags(self): traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-ff" version, trace_id, parent_id, sampled_flag = self.tp.get_traceparent_fields(traceparent) - self.assertEqual(trace_id, "4bf92f3577b34da6a3ce929d0e0e4736") - self.assertEqual(parent_id, "00f067aa0ba902b7") + self.assertEqual(trace_id, 11803532876627986230) + self.assertEqual(parent_id, 67667974448284343) self.assertTrue(sampled_flag) def test_get_traceparent_fields_None_input(self): @@ -79,6 +80,7 @@ def test_get_traceparent_fields_string_input_no_dash(self): self.assertIsNone(parent_id) self.assertFalse(sampled_flag) + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_update_traceparent(self): traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" in_trace_id = "1234d0e0e4736234" @@ -87,6 +89,7 @@ def test_update_traceparent(self): expected_traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-1234567890abcdef-01" self.assertEqual(expected_traceparent, self.tp.update_traceparent(traceparent, in_trace_id, in_span_id, level)) + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_update_traceparent_None(self): traceparent = None in_trace_id = "1234d0e0e4736234" diff --git a/tests_aws/01_lambda/conftest.py b/tests_aws/01_lambda/conftest.py new file mode 100644 index 00000000..a7b217c6 --- /dev/null +++ b/tests_aws/01_lambda/conftest.py @@ -0,0 +1,45 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import os +import sys + +import pytest + +os.environ["AWS_EXECUTION_ENV"] = "AWS_Lambda_python_3.10" + +from instana.collector.base import BaseCollector + +if sys.version_info <= (3, 8): + print("Python runtime version not supported by AWS Lambda.") + exit(1) + + +@pytest.fixture +def trace_id() -> int: + return 1812338823475918251 + + +@pytest.fixture +def span_id() -> int: + return 6895521157646639861 + + +def always_true(_: object) -> bool: + return True + + +# 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_aws/01_lambda/test_lambda.py b/tests_aws/01_lambda/test_lambda.py new file mode 100644 index 00000000..a0e17624 --- /dev/null +++ b/tests_aws/01_lambda/test_lambda.py @@ -0,0 +1,836 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +from collections import defaultdict +import json +import logging +import os +import time +from typing import TYPE_CHECKING, Any, Dict, Generator + +import pytest +import wrapt + +from instana import get_aws_lambda_handler, lambda_handler +from instana.agent.aws_lambda import AWSLambdaAgent +from instana.collector.aws_lambda import AWSLambdaCollector +from instana.instrumentation.aws.lambda_inst import lambda_handler_with_instana +from instana.instrumentation.aws.triggers import read_http_query_params +from instana.options import AWSLambdaOptions +from instana.singletons import get_agent +from instana.util.aws import normalize_aws_lambda_arn + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + +# Mock Context object +class MockContext(dict): + def __init__(self, **kwargs: Dict[str, Any]) -> None: + super(MockContext, self).__init__(**kwargs) + self.invoked_function_arn = ( + "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + self.function_name = "TestPython" + self.function_version = "1" + + +# This is the target handler that will be instrumented for these tests +def my_lambda_handler(event: object, context: object) -> Dict[str, Any]: + # print("target_handler called") + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps({"site": "pwpush.com", "response": 204}), + } + + +# We only want to monkey patch the test handler once so do it here +os.environ["LAMBDA_HANDLER"] = "tests_aws.01_lambda.test_lambda.my_lambda_handler" +module_name, function_name = get_aws_lambda_handler() +wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) + + +def my_errored_lambda_handler(event: object, context: object) -> Dict[str, Any]: + return { + "statusCode": 500, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps({"site": "wikipedia.org", "response": 500}), + } + + +os.environ["LAMBDA_HANDLER"] = ( + "tests_aws.01_lambda.test_lambda.my_errored_lambda_handler" +) +module_name, function_name = get_aws_lambda_handler() +wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) + + +class TestLambda: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + os.environ["LAMBDA_HANDLER"] = ( + "tests_aws.01_lambda.test_lambda.my_lambda_handler" + ) + self.pwd = os.path.dirname(os.path.realpath(__file__)) + self.context = MockContext() + self.agent: AWSLambdaAgent = get_agent() + yield + # tearDown + # Reset collector config + self.agent.collector.snapshot_data_sent = False + # Reset all environment variables of consequence + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "LAMBDA_HANDLER" in os.environ: + os.environ.pop("LAMBDA_HANDLER") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_SERVICE_NAME" in os.environ: + os.environ.pop("INSTANA_SERVICE_NAME") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + + def test_invalid_options(self) -> None: + # None of the required env vars are available... + if "LAMBDA_HANDLER" in os.environ: + os.environ.pop("LAMBDA_HANDLER") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + self.agent = AWSLambdaAgent() + assert not self.agent._can_send + assert not self.agent.collector + # Assign a collector to fix CI tests + self.agent.collector = AWSLambdaCollector(self.agent) + + def test_secrets(self) -> None: + 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_has_extra_http_headers(self) -> None: + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_has_options(self) -> None: + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, AWSLambdaOptions) + assert self.agent.options.endpoint_proxy == {} + + def test_get_handler(self) -> None: + os.environ["LAMBDA_HANDLER"] = "tests.lambda_handler" + handler_module, handler_function = get_aws_lambda_handler() + + assert "tests" == handler_module + assert "lambda_handler" == handler_function + + def test_get_handler_with_multi_subpackages(self) -> None: + os.environ["LAMBDA_HANDLER"] = "tests.one.two.three.lambda_handler" + handler_module, handler_function = get_aws_lambda_handler() + + assert "tests.one.two.three" == handler_module + assert "lambda_handler" == handler_function + + def test_get_handler_with_space_in_it(self) -> None: + os.environ["LAMBDA_HANDLER"] = " tests.another_module.lambda_handler" + handler_module, handler_function = get_aws_lambda_handler() + + assert "tests.another_module" == handler_module + assert "lambda_handler" == handler_function + + os.environ["LAMBDA_HANDLER"] = "tests.another_module.lambda_handler " + handler_module, handler_function = get_aws_lambda_handler() + + assert "tests.another_module" == handler_module + assert "lambda_handler" == handler_function + + def test_agent_extra_http_headers(self) -> None: + os.environ["INSTANA_EXTRA_HTTP_HEADERS"] = ( + "X-Test-Header;X-Another-Header;X-And-Another-Header" + ) + self.agent = AWSLambdaAgent() + + assert self.agent.options.extra_http_headers + should_headers = ["x-test-header", "x-another-header", "x-and-another-header"] + assert should_headers == self.agent.options.extra_http_headers + + def test_custom_proxy(self) -> None: + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.agent = AWSLambdaAgent() + + assert self.agent.options.endpoint_proxy == {"https": "http://myproxy.123"} + + def test_custom_service_name(self, trace_id: int, span_id: int) -> None: + os.environ["INSTANA_SERVICE_NAME"] = "Legion" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + # We need reset the AWSLambdaOptions with new INSTANA_SERVICE_NAME + self.agent.options = AWSLambdaOptions() + + with open( + self.pwd + "/../data/lambda/api_gateway_event.json", "r" + ) as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + os.environ.pop("INSTANA_SERVICE_NAME") + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t == hex(trace_id)[2:] + assert span.s + assert span.p == hex(span_id)[2:] + assert span.ts + + server_timing_value = f"intid;desc={trace_id}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + + assert span.data["service"] == "Legion" + + assert span.data["lambda"]["trigger"] == "aws:api.gateway" + assert span.data["http"]["method"] == "POST" + assert span.data["http"]["status"] == 200 + assert span.data["http"]["url"] == "/path/to/resource" + assert span.data["http"]["path_tpl"] == "/{proxy+}" + assert span.data["http"]["params"] == "foo=['bar']" + + def test_api_gateway_trigger_tracing(self, trace_id: int, span_id: int) -> None: + with open( + self.pwd + "/../data/lambda/api_gateway_event.json", "r" + ) as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t == hex(trace_id)[2:] + assert span.s + assert span.p == hex(span_id)[2:] + assert span.ts + + server_timing_value = f"intid;desc={trace_id}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:api.gateway" + assert span.data["http"]["method"] == "POST" + assert span.data["http"]["status"] == 200 + assert span.data["http"]["url"] == "/path/to/resource" + assert span.data["http"]["path_tpl"] == "/{proxy+}" + assert span.data["http"]["params"] == "foo=['bar']" + + def test_api_gateway_v2_trigger_tracing(self) -> None: + with open( + self.pwd + "/../data/lambda/api_gateway_v2_event.json", "r" + ) as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + assert result["statusCode"] == 200 + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + span = self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) + + assert not span.ec + assert not span.data["lambda"]["error"] + assert span.data["http"]["status"] == 200 + + def test_api_gateway_v2_trigger_errored_tracing(self) -> None: + with open( + self.pwd + "/../data/lambda/api_gateway_v2_event.json", "r" + ) as json_file: + event = json.load(json_file) + + os.environ["LAMBDA_HANDLER"] = ( + "tests_aws.01_lambda.test_lambda.my_errored_lambda_handler" + ) + + result = lambda_handler(event, self.context) + assert result["statusCode"] == 500 + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + span = self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) + + assert span.ec == 1 + assert span.data["lambda"]["error"] == "HTTP status 500" + assert span.data["http"]["status"] == 500 + + def test_application_lb_trigger_tracing(self, trace_id: int, span_id: int) -> None: + with open( + self.pwd + "/../data/lambda/api_gateway_event.json", "r" + ) as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t == hex(trace_id)[2:] + assert span.s + assert span.p == hex(span_id)[2:] + assert span.ts + + server_timing_value = f"intid;desc={trace_id}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:api.gateway" + assert span.data["http"]["method"] == "POST" + assert span.data["http"]["status"] == 200 + assert span.data["http"]["url"] == "/path/to/resource" + assert span.data["http"]["params"] == "foo=['bar']" + + def test_cloudwatch_trigger_tracing(self, trace_id: int) -> None: + with open(self.pwd + "/../data/lambda/cloudwatch_event.json", "r") as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t + assert span.s + assert not span.p + assert span.ts + + server_timing_value = f"intid;desc={int(span.t, 16)}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert not span.sy + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:cloudwatch.events" + assert ( + span.data["lambda"]["cw"]["events"]["id"] + == "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c" + ) + assert not span.data["lambda"]["cw"]["events"]["more"] + assert isinstance(span.data["lambda"]["cw"]["events"]["resources"], list) + + assert len(span.data["lambda"]["cw"]["events"]["resources"]) == 1 + assert ( + span.data["lambda"]["cw"]["events"]["resources"][0] + == "arn:aws:events:eu-west-1:123456789012:rule/ExampleRule" + ) + + def test_cloudwatch_logs_trigger_tracing(self) -> None: + with open( + self.pwd + "/../data/lambda/cloudwatch_logs_event.json", "r" + ) as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t + assert span.s + assert not span.p + assert span.ts + + server_timing_value = f"intid;desc={int(span.t, 16)}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert not span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:cloudwatch.logs" + assert "decodingError" not in span.data["lambda"]["cw"]["logs"] + assert span.data["lambda"]["cw"]["logs"]["group"] == "testLogGroup" + assert span.data["lambda"]["cw"]["logs"]["stream"] == "testLogStream" + assert not span.data["lambda"]["cw"]["logs"]["more"] + assert isinstance(span.data["lambda"]["cw"]["logs"]["events"], list) + assert len(span.data["lambda"]["cw"]["logs"]["events"]) == 2 + assert ( + span.data["lambda"]["cw"]["logs"]["events"][0] + == "[ERROR] First test message" + ) + assert ( + span.data["lambda"]["cw"]["logs"]["events"][1] + == "[ERROR] Second test message" + ) + + def test_s3_trigger_tracing(self) -> None: + with open(self.pwd + "/../data/lambda/s3_event.json", "r") as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t + assert span.s + assert not span.p + assert span.ts + + server_timing_value = f"intid;desc={int(span.t, 16)}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert not span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:s3" + assert isinstance(span.data["lambda"]["s3"]["events"], list) + events = span.data["lambda"]["s3"]["events"] + assert len(events) == 1 + event = events[0] + assert event["event"] == "ObjectCreated:Put" + assert event["bucket"] == "example-bucket" + assert event["object"] == "test/key" + + def test_sqs_trigger_tracing(self) -> None: + with open(self.pwd + "/../data/lambda/sqs_event.json", "r") as json_file: + event = json.load(json_file) + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t + assert span.s + assert not span.p + assert span.ts + + server_timing_value = f"intid;desc={int(span.t, 16)}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert not span.sy + + assert not span.ec + assert not span.data["lambda"]["error"] + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:sqs" + assert isinstance(span.data["lambda"]["sqs"]["messages"], list) + messages = span.data["lambda"]["sqs"]["messages"] + assert len(messages) == 1 + message = messages[0] + assert message["queue"] == "arn:aws:sqs:us-west-1:123456789012:MyQueue" + + def test_read_query_params(self) -> None: + event = { + "queryStringParameters": {"foo": "bar"}, + "multiValueQueryStringParameters": {"foo": ["bar"]}, + } + params = read_http_query_params(event) + assert params == "foo=['bar']" + + def test_read_query_params_with_none_data(self) -> None: + event = {"queryStringParameters": None, "multiValueQueryStringParameters": None} + params = read_http_query_params(event) + assert params == "" + + def test_read_query_params_with_bad_event(self) -> None: + event = None + params = read_http_query_params(event) + assert params == "" + + def test_arn_parsing(self) -> None: + ctx = MockContext() + + assert ( + normalize_aws_lambda_arn(ctx) + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + # Without version should return a fully qualified ARN (with version) + ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython" + assert ( + normalize_aws_lambda_arn(ctx) + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + # Fully qualified already with the '$LATEST' special tag + ctx.invoked_function_arn = ( + "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" + ) + assert ( + normalize_aws_lambda_arn(ctx) + == "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" + ) + + def test_agent_default_log_level(self) -> None: + assert self.agent.options.log_level == logging.WARNING + + def __validate_result_and_payload_for_gateway_v2_trace(self, result: Dict[str, Any], payload: defaultdict) -> "InstanaSpan": + assert isinstance(result, dict) + assert "headers" in result + assert "Server-Timing" in result["headers"] + assert "statusCode" in result + + assert "metrics" in payload + assert "spans" in payload + assert len(payload.keys()) == 2 + + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + plugin_data = payload["metrics"]["plugins"][0] + + assert plugin_data["name"] == "com.instana.plugin.aws.lambda" + assert ( + plugin_data["entityId"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + + assert len(payload["spans"]) >= 1 + + span = payload["spans"].pop() + assert span.n == "aws.lambda.entry" + assert span.t == hex(int("0000000000001234"))[2:].zfill(16) + assert span.s + assert span.p == hex(int("0000000000004567"))[2:].zfill(16) + assert span.ts + + server_timing_value = f"intid;desc={int('0000000000001234')}" + assert result["headers"]["Server-Timing"] == server_timing_value + + assert span.f == { + "hl": True, + "cp": "aws", + "e": "arn:aws:lambda:us-east-2:12345:function:TestPython:1", + } + assert span.sy + + assert ( + span.data["lambda"]["arn"] + == "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + ) + assert not span.data["lambda"]["alias"] + assert span.data["lambda"]["runtime"] == "python" + assert span.data["lambda"]["functionName"] == "TestPython" + assert span.data["lambda"]["functionVersion"] == "1" + assert not span.data["service"] + + assert span.data["lambda"]["trigger"] == "aws:api.gateway" + assert span.data["http"]["method"] == "POST" + assert span.data["http"]["url"] == "/my/path" + assert span.data["http"]["path_tpl"] == "/my/{resource}" + assert span.data["http"]["params"] == "secret=key&q=term" + + return span \ No newline at end of file diff --git a/tests_aws/02_fargate/conftest.py b/tests_aws/02_fargate/conftest.py new file mode 100644 index 00000000..d249421a --- /dev/null +++ b/tests_aws/02_fargate/conftest.py @@ -0,0 +1,21 @@ +import os +import pytest + +os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + +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/data/fargate/1.3.0/README.md b/tests_aws/02_fargate/data/1.3.0/README.md similarity index 100% rename from tests/data/fargate/1.3.0/README.md rename to tests_aws/02_fargate/data/1.3.0/README.md diff --git a/tests/data/fargate/1.3.0/root_metadata.json b/tests_aws/02_fargate/data/1.3.0/root_metadata.json similarity index 100% rename from tests/data/fargate/1.3.0/root_metadata.json rename to tests_aws/02_fargate/data/1.3.0/root_metadata.json diff --git a/tests/data/fargate/1.3.0/stats_metadata.json b/tests_aws/02_fargate/data/1.3.0/stats_metadata.json similarity index 100% rename from tests/data/fargate/1.3.0/stats_metadata.json rename to tests_aws/02_fargate/data/1.3.0/stats_metadata.json diff --git a/tests/data/fargate/1.3.0/task_metadata.json b/tests_aws/02_fargate/data/1.3.0/task_metadata.json similarity index 100% rename from tests/data/fargate/1.3.0/task_metadata.json rename to tests_aws/02_fargate/data/1.3.0/task_metadata.json diff --git a/tests/data/fargate/1.3.0/task_stats_metadata.json b/tests_aws/02_fargate/data/1.3.0/task_stats_metadata.json similarity index 100% rename from tests/data/fargate/1.3.0/task_stats_metadata.json rename to tests_aws/02_fargate/data/1.3.0/task_stats_metadata.json diff --git a/tests_aws/02_fargate/test_fargate.py b/tests_aws/02_fargate/test_fargate.py new file mode 100644 index 00000000..551b0968 --- /dev/null +++ b/tests_aws/02_fargate/test_fargate.py @@ -0,0 +1,110 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import logging +import os +from typing import Generator + +import pytest + +from instana.agent.aws_fargate import AWSFargateAgent +from instana.options import AWSFargateOptions +from instana.singletons import get_agent + + +class TestFargate: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + self.agent = AWSFargateAgent() + yield + # tearDown + # Reset all environment variables of consequence + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + def test_has_options(self) -> None: + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, AWSFargateOptions) + + def test_invalid_options(self) -> None: + # None of the required env vars are available... + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = AWSFargateAgent() + assert not agent.can_send() + assert not agent.collector + + def test_default_secrets(self) -> None: + assert not self.agent.options.secrets + 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_custom_secrets(self) -> None: + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + agent = AWSFargateAgent() + + assert hasattr(agent.options, "secrets_matcher") + assert agent.options.secrets_matcher == "equals" + assert hasattr(agent.options, "secrets_list") + assert agent.options.secrets_list == ["love", "war", "games"] + + def test_default_tags(self) -> None: + assert hasattr(self.agent.options, "tags") + assert not self.agent.options.tags + + def test_has_extra_http_headers(self) -> None: + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_agent_extra_http_headers(self) -> None: + os.environ["INSTANA_EXTRA_HTTP_HEADERS"] = ( + "X-Test-Header;X-Another-Header;X-And-Another-Header" + ) + agent = AWSFargateAgent() + assert agent.options.extra_http_headers + assert agent.options.extra_http_headers == [ + "x-test-header", + "x-another-header", + "x-and-another-header", + ] + + def test_agent_default_log_level(self) -> None: + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level(self) -> None: + os.environ["INSTANA_LOG_LEVEL"] = "eRror" + agent = AWSFargateAgent() + assert agent.options.log_level == logging.ERROR + + def test_custom_proxy(self) -> None: + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + agent = AWSFargateAgent() + assert agent.options.endpoint_proxy == {"https": "http://myproxy.123"} diff --git a/tests_aws/02_fargate/test_fargate_collector.py b/tests_aws/02_fargate/test_fargate_collector.py new file mode 100644 index 00000000..673b7c78 --- /dev/null +++ b/tests_aws/02_fargate/test_fargate_collector.py @@ -0,0 +1,279 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import json +import os +from typing import Generator + +import pytest + +from instana.agent.aws_fargate import AWSFargateAgent +from instana.singletons import get_agent + + +def get_docker_plugin(plugins): + """ + Given a list of plugins, find and return the docker plugin that we're interested in from the mock data + """ + docker_plugin = None + for plugin in plugins: + if ( + plugin["name"] == "com.instana.plugin.docker" + and plugin["entityId"] + == "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82::docker-ssh-aws-fargate" + ): + docker_plugin = plugin + return docker_plugin + + +def _set_ecs_metadata(agent: AWSFargateAgent) -> None: + """ + Manually set the ECS Metadata API results on the collector + """ + pwd = os.path.dirname(os.path.realpath(__file__)) + with open(pwd + "/data/1.3.0/root_metadata.json", "r") as json_file: + agent.collector.root_metadata = json.load(json_file) + with open(pwd + "/data/1.3.0/task_metadata.json", "r") as json_file: + agent.collector.task_metadata = json.load(json_file) + with open(pwd + "/data/1.3.0/stats_metadata.json", "r") as json_file: + agent.collector.stats_metadata = json.load(json_file) + with open(pwd + "/data/1.3.0/task_stats_metadata.json", "r") as json_file: + agent.collector.task_stats_metadata = json.load(json_file) + + +def _unset_ecs_metadata(agent: AWSFargateAgent) -> None: + """ + Manually unset the ECS Metadata API results on the collector + """ + agent.collector.root_metadata = None + agent.collector.task_metadata = None + agent.collector.stats_metadata = None + agent.collector.task_stats_metadata = None + + +class TestFargateCollector: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + self.agent = AWSFargateAgent() + + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + _set_ecs_metadata(self.agent) + yield + # tearDown + # Reset all environment variables of consequence + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + self.agent.collector.snapshot_data_last_sent = 0 + _unset_ecs_metadata(self.agent) + + def test_prepare_payload_basics(self) -> None: + payload = self.agent.collector.prepare_payload() + + assert payload + assert len(payload.keys()) == 2 + + 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"]) == 7 + + plugins = payload["metrics"]["plugins"] + for plugin in plugins: + assert "name" in plugin + assert "entityId" in plugin + assert "data" in plugin + + def test_docker_plugin_snapshot_data(self) -> None: + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + assert first_payload + assert second_payload + + plugin_first_report = get_docker_plugin(first_payload["metrics"]["plugins"]) + plugin_second_report = get_docker_plugin(second_payload["metrics"]["plugins"]) + + # First report should have snapshot data + assert plugin_first_report + assert "data" in plugin_first_report + + data = plugin_first_report["data"] + + assert ( + data["Id"] + == "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45" + ) + assert data["Created"] == "2020-07-27T12:14:12.583114444Z" + assert data["Started"] == "2020-07-27T12:14:13.545410186Z" + assert ( + data["Image"] + == "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest" + ) + assert data["Labels"] == { + "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster", + "com.amazonaws.ecs.container-name": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82", + "com.amazonaws.ecs.task-definition-family": "docker-ssh-aws-fargate", + "com.amazonaws.ecs.task-definition-version": "1", + } + assert not data["Ports"] + + # Second report should have no snapshot data + assert plugin_second_report + assert "data" in plugin_second_report + + data = plugin_second_report["data"] + + assert "Id" in data + assert "Created" not in data + assert "Started" not in data + assert "Image" not in data + assert "Labels" not in data + assert "Ports" not in data + + def test_docker_plugin_metrics(self) -> None: + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + assert first_payload + assert second_payload + + plugin_first_report = get_docker_plugin(first_payload["metrics"]["plugins"]) + + assert plugin_first_report + assert "data" in plugin_first_report + + plugin_second_report = get_docker_plugin(second_payload["metrics"]["plugins"]) + + assert plugin_second_report + assert "data" in plugin_second_report + + # First report should report all metrics + data = plugin_first_report.get("data", None) + + assert data + assert "network" not in data + + cpu = data.get("cpu", None) + + assert cpu + assert cpu["total_usage"] == 0.011033 + assert cpu["user_usage"] == 0.009918 + assert cpu["system_usage"] == 0.00089 + assert cpu["throttling_count"] == 0 + assert cpu["throttling_time"] == 0 + + memory = data.get("memory", None) + + assert memory + assert memory["active_anon"] == 78721024 + assert memory["active_file"] == 18501632 + assert memory["inactive_anon"] == 0 + assert memory["inactive_file"] == 71684096 + assert memory["total_cache"] == 90185728 + assert memory["total_rss"] == 78721024 + assert memory["usage"] == 193769472 + assert memory["max_usage"] == 195305472 + assert memory["limit"] == 536870912 + + blkio = data.get("blkio", None) + + assert blkio + assert blkio["blk_read"] == 0 + assert blkio["blk_write"] == 128352256 + + # Second report should report the delta (in the test case, nothing) + data = plugin_second_report["data"] + + assert "cpu" in data + assert len(data["cpu"]) == 0 + assert "memory" in data + assert len(data["memory"]) == 0 + assert "blkio" in data + assert len(data["blkio"]) == 1 + assert data["blkio"]["blk_write"] == 0 + assert "blk_read" not in data["blkio"] + + def test_no_instana_zone(self) -> None: + assert not self.agent.options.zone + + def test_instana_zone(self) -> None: + os.environ["INSTANA_ZONE"] = "YellowDog" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + agent = AWSFargateAgent() + _set_ecs_metadata(agent) + + assert agent.options.zone == "YellowDog" + + payload = agent.collector.prepare_payload() + assert payload + + plugins = payload["metrics"]["plugins"] + assert isinstance(plugins, list) + + task_plugin = None + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + + assert task_plugin + assert "data" in task_plugin + assert "instanaZone" in task_plugin["data"] + assert task_plugin["data"]["instanaZone"] == "YellowDog" + + _unset_ecs_metadata(agent) + + def test_custom_tags(self) -> None: + os.environ["INSTANA_TAGS"] = "love,war=1,games" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + agent = AWSFargateAgent() + _set_ecs_metadata(agent) + + assert hasattr(agent.options, "tags") + assert agent.options.tags == {"love": None, "war": "1", "games": None} + + payload = agent.collector.prepare_payload() + assert payload + + task_plugin = None + plugins = payload["metrics"]["plugins"] + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + + assert task_plugin + assert "tags" in task_plugin["data"] + + tags = task_plugin["data"]["tags"] + assert tags["war"] == "1" + assert not tags["love"] + assert not tags["games"] + + _unset_ecs_metadata(agent) diff --git a/tests_aws/03_eks/test_eksfargate.py b/tests_aws/03_eks/test_eksfargate.py new file mode 100644 index 00000000..6f7984d9 --- /dev/null +++ b/tests_aws/03_eks/test_eksfargate.py @@ -0,0 +1,114 @@ +# (c) Copyright IBM Corp. 2024 + +import logging +import os +from typing import Generator + +import pytest + +from instana.agent.aws_eks_fargate import EKSFargateAgent +from instana.options import EKSFargateOptions +from instana.singletons import get_agent + + +class TestEKSFargate: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + self.agent = EKSFargateAgent() + yield + # tearDown + # Reset all environment variables of consequence + variable_names = ( + "INSTANA_TRACER_ENVIRONMENT", + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_LOG_LEVEL", + "INSTANA_SECRETS", + "INSTANA_DEBUG", + "INSTANA_TAGS", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + def test_has_options(self) -> None: + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, EKSFargateOptions) + + def test_missing_variables(self, caplog) -> None: + os.environ.pop("INSTANA_ENDPOINT_URL") + agent = EKSFargateAgent() + assert not agent.can_send() + assert not agent.collector + assert ( + "Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. We will not be able to monitor this Pod." + in caplog.messages + ) + + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ.pop("INSTANA_AGENT_KEY") + agent = EKSFargateAgent() + assert not agent.can_send() + assert not agent.collector + assert ( + "Required INSTANA_AGENT_KEY and/or INSTANA_ENDPOINT_URL environment variables not set. We will not be able to monitor this Pod." + in caplog.messages + ) + + def test_default_secrets(self) -> None: + assert not self.agent.options.secrets + 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_custom_secrets(self) -> None: + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + agent = EKSFargateAgent() + + assert hasattr(agent.options, "secrets_matcher") + assert agent.options.secrets_matcher == "equals" + assert hasattr(agent.options, "secrets_list") + assert agent.options.secrets_list == ["love", "war", "games"] + + def test_default_tags(self) -> None: + assert hasattr(self.agent.options, "tags") + assert not self.agent.options.tags + + def test_has_extra_http_headers(self) -> None: + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_agent_extra_http_headers(self) -> None: + os.environ["INSTANA_EXTRA_HTTP_HEADERS"] = ( + "X-Test-Header;X-Another-Header;X-And-Another-Header" + ) + agent = EKSFargateAgent() + assert agent.options.extra_http_headers + assert agent.options.extra_http_headers == [ + "x-test-header", + "x-another-header", + "x-and-another-header", + ] + + def test_agent_default_log_level(self) -> None: + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level(self) -> None: + os.environ["INSTANA_LOG_LEVEL"] = "eRror" + agent = EKSFargateAgent() + assert agent.options.log_level == logging.ERROR + + def test_custom_proxy(self) -> None: + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + agent = EKSFargateAgent() + assert agent.options.endpoint_proxy == {"https": "http://myproxy.123"} diff --git a/tests_aws/03_eks/test_eksfargate_collector.py b/tests_aws/03_eks/test_eksfargate_collector.py new file mode 100644 index 00000000..32f8f93e --- /dev/null +++ b/tests_aws/03_eks/test_eksfargate_collector.py @@ -0,0 +1,61 @@ +# (c) Copyright IBM Corp. 2024 + +import os +from typing import Generator + +import pytest + +from instana.agent.aws_eks_fargate import EKSFargateAgent +from instana.singletons import get_agent + + +class TestEKSFargateCollector: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + self.agent = EKSFargateAgent() + yield + # tearDown + # Reset all environment variables of consequence + variable_names = ( + "INSTANA_TRACER_ENVIRONMENT", + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_ZONE", + "INSTANA_TAGS", + ) + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + def test_prepare_payload_basics(self) -> None: + payload = self.agent.collector.prepare_payload() + + assert payload + assert len(payload.keys()) == 2 + 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"]) == 2 + + process_plugin = payload["metrics"]["plugins"][0] + assert "data" in process_plugin + + runtime_plugin = payload["metrics"]["plugins"][1] + assert "name" in runtime_plugin + assert "entityId" in runtime_plugin + assert "data" in runtime_plugin + + def test_no_instana_zone(self) -> None: + assert not self.agent.options.zone diff --git a/src/instana/instrumentation/pyramid/__init__.py b/tests_aws/__init__.py similarity index 100% rename from src/instana/instrumentation/pyramid/__init__.py rename to tests_aws/__init__.py diff --git a/tests_aws/conftest.py b/tests_aws/conftest.py new file mode 100644 index 00000000..90dea412 --- /dev/null +++ b/tests_aws/conftest.py @@ -0,0 +1,7 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import os + +os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" +os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" diff --git a/tests/data/lambda/api_gateway_event.json b/tests_aws/data/lambda/api_gateway_event.json similarity index 96% rename from tests/data/lambda/api_gateway_event.json rename to tests_aws/data/lambda/api_gateway_event.json index 23f54928..2a6dc49e 100644 --- a/tests/data/lambda/api_gateway_event.json +++ b/tests_aws/data/lambda/api_gateway_event.json @@ -37,8 +37,8 @@ "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https", - "X-Instana-T": "d5cb361b256413a9", - "X-Instana-S": "0901d8ae4fbf1529", + "X-Instana-T": "1812338823475918251", + "X-Instana-S": "6895521157646639861", "X-Instana-L": "1", "X-Instana-Synthetic": "1" }, @@ -98,10 +98,10 @@ "https" ], "X-Instana-T": [ - "d5cb361b256413a9" + "1812338823475918251" ], "X-Instana-S": [ - "0901d8ae4fbf1529" + "6895521157646639861" ], "X-Instana-L": [ "1" diff --git a/tests/data/lambda/api_gateway_v2_event.json b/tests_aws/data/lambda/api_gateway_v2_event.json similarity index 100% rename from tests/data/lambda/api_gateway_v2_event.json rename to tests_aws/data/lambda/api_gateway_v2_event.json diff --git a/tests/data/lambda/cloudwatch_event.json b/tests_aws/data/lambda/cloudwatch_event.json similarity index 100% rename from tests/data/lambda/cloudwatch_event.json rename to tests_aws/data/lambda/cloudwatch_event.json diff --git a/tests/data/lambda/cloudwatch_logs_event.json b/tests_aws/data/lambda/cloudwatch_logs_event.json similarity index 100% rename from tests/data/lambda/cloudwatch_logs_event.json rename to tests_aws/data/lambda/cloudwatch_logs_event.json diff --git a/tests/data/lambda/s3_event.json b/tests_aws/data/lambda/s3_event.json similarity index 100% rename from tests/data/lambda/s3_event.json rename to tests_aws/data/lambda/s3_event.json diff --git a/tests/data/lambda/sqs_event.json b/tests_aws/data/lambda/sqs_event.json similarity index 100% rename from tests/data/lambda/sqs_event.json rename to tests_aws/data/lambda/sqs_event.json