├── .bumpversion.cfg ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .pypirc ├── CODEOWNERS ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── docker-compose.yml ├── foresight ├── __init__.py ├── constants │ ├── __init__.py │ └── constants.py ├── context │ ├── __init__.py │ ├── test_case_execution_context.py │ └── test_suite_execution_context.py ├── environment │ ├── __init__.py │ ├── azure │ │ ├── __init__.py │ │ └── azure_environment_info_provider.py │ ├── bitbucket │ │ ├── __init__.py │ │ └── bitbucket_environment_info_provider.py │ ├── circleci │ │ ├── __init__.py │ │ └── circleci_environment_info_provider.py │ ├── environment_info.py │ ├── environment_info_support.py │ ├── git │ │ ├── __init__.py │ │ ├── git_env_info_provider.py │ │ ├── git_helper.py │ │ └── utils.py │ ├── github │ │ ├── __init__.py │ │ └── github_environment_info_provider.py │ ├── gitlab │ │ ├── __init__.py │ │ └── gitlab_environment_info_provider.py │ ├── jenkins │ │ ├── __init__.py │ │ └── jenkins_environment_info_provider.py │ └── travisci │ │ ├── __init__.py │ │ └── travisci_environment_info_provider.py ├── foresight_executor.py ├── model │ ├── __init__.py │ ├── terminator.py │ ├── test_run_context.py │ ├── test_run_finish.py │ ├── test_run_monitoring.py │ ├── test_run_result.py │ ├── test_run_start.py │ └── test_run_status.py ├── pytest_integration │ ├── __init__.py │ ├── constants.py │ ├── plugin.py │ ├── pytest_helper.py │ └── utils.py ├── sampler │ ├── __init__.py │ └── max_count_aware_sampler.py ├── test_runner_support.py ├── test_runner_tags.py ├── test_status.py └── utils │ ├── __init__.py │ ├── generic_utils.py │ ├── handler_utils.py │ ├── test_runner_utils.py │ └── test_wrapper.py ├── layer ├── deploy_layer.sh ├── release_layer.sh └── setup.cfg ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── config │ ├── __init__.py │ └── test_config_provider.py ├── conftest.py ├── integrations │ ├── __init__.py │ ├── conftest.py │ ├── test_aws_integrations.py │ ├── test_es_integration.py │ ├── test_http_integration.py │ ├── test_mongo_integration.py │ ├── test_mysql_integration.py │ ├── test_postgre_integration.py │ ├── test_redis_integrations.py │ └── test_sqlalchemy_integration.py ├── listeners │ ├── conftest.py │ ├── test_error_injector_span_listener.py │ ├── test_filtering_span_listener.py │ ├── test_latency_injector_span_listener.py │ └── test_security_aware_span_listener.py ├── opentracing │ ├── __init__.py │ ├── test_span.py │ └── test_tracer.py ├── plugins │ ├── __init__.py │ ├── conftest.py │ ├── invocation │ │ ├── __init__.py │ │ ├── test_invocation_plugin.py │ │ ├── test_invocation_support.py │ │ └── test_invocation_trace_support.py │ ├── log │ │ ├── __init__.py │ │ ├── test_log_config.ini │ │ └── test_log_plugin.py │ ├── patch │ │ ├── __init__.py │ │ └── test_patcher.py │ └── trace │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_trace_aware_wrapper.py │ │ ├── test_trace_plugin.py │ │ ├── test_trace_support.py │ │ └── test_traceable.py ├── samplers │ ├── test_composite_sampler.py │ ├── test_count_aware_sampler.py │ └── test_time_aware_sampler.py ├── test_application_support.py ├── test_lambda_event_utils.py ├── test_reporter.py ├── test_thundra_agent.py ├── test_utils.py ├── testdemo │ ├── __init__.py │ └── movie │ │ ├── __init__.py │ │ ├── movie.py │ │ ├── movie_repository.py │ │ └── movie_service.py └── wrappers │ ├── django │ ├── app │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wsgi.py │ ├── conftest.py │ └── test_django.py │ ├── fastapi │ ├── conftest.py │ ├── main.py │ └── test_fastapi.py │ ├── flask │ └── test_flask.py │ └── tornado │ ├── hello.py │ └── test_tornado.py └── thundra ├── __init__.py ├── _version.py ├── application ├── __init__.py ├── application_info.py ├── application_info_provider.py └── global_application_info_provider.py ├── compat.py ├── composite.py ├── config ├── __init__.py ├── config_metadata.py ├── config_names.py └── config_provider.py ├── constants.py ├── context ├── __init__.py ├── context_provider.py ├── execution_context.py ├── execution_context_manager.py ├── global_execution_context_provider.py ├── plugin_context.py └── tracing_execution_context_provider.py ├── debug ├── __init__.py └── bridge.py ├── encoder.py ├── handler.py ├── integrations ├── __init__.py ├── aiohttp │ ├── __init__.py │ └── client.py ├── base_integration.py ├── botocore.py ├── django.py ├── elasticsearch.py ├── handler_wrappers │ ├── __init__.py │ └── chalice.py ├── modules │ ├── __init__.py │ ├── aiohttp.py │ ├── botocore.py │ ├── db.py │ ├── django.py │ ├── elasticsearch.py │ ├── fastapi.py │ ├── flask.py │ ├── mysql.py │ ├── psycopg2.py │ ├── pymongo.py │ ├── redis.py │ ├── requests.py │ ├── sqlalchemy.py │ ├── sqlite3.py │ └── tornado.py ├── mongodb.py ├── mysql.py ├── postgre.py ├── rdb_base.py ├── redis.py ├── requests.py ├── sqlalchemy.py └── sqlite3.py ├── listeners ├── __init__.py ├── composite_span_filter.py ├── error_injector_span_listener.py ├── filtering_span_listener.py ├── latency_injector_span_listener.py ├── security_aware_span_listener.py ├── tag_injector_span_listener.py ├── thundra_span_filterer.py └── thundra_span_listener.py ├── opentracing ├── __init__.py ├── propagation │ ├── __init__.py │ ├── http.py │ ├── propagator.py │ └── text.py ├── recorder.py ├── span.py ├── span_context.py └── tracer.py ├── plugins ├── __init__.py ├── config │ ├── __init__.py │ ├── base_plugin_config.py │ ├── log_config.py │ ├── metric_config.py │ ├── thundra_config.py │ └── trace_config.py ├── invocation │ ├── __init__.py │ ├── invocation_plugin.py │ ├── invocation_support.py │ └── invocation_trace_support.py ├── log │ ├── __init__.py │ ├── log_plugin.py │ ├── log_support.py │ ├── thundra_log_handler.py │ └── thundra_logger.py ├── metric │ ├── __init__.py │ ├── metric_plugin.py │ └── metric_support.py └── trace │ ├── __init__.py │ ├── patcher.py │ ├── trace_aware_wrapper.py │ ├── trace_plugin.py │ ├── trace_support.py │ └── traceable.py ├── reporter.py ├── samplers ├── __init__.py ├── base_sampler.py ├── composite_sampler.py ├── count_aware_sampler.py ├── duration_aware_sampler.py ├── error_aware_sampler.py └── time_aware_sampler.py ├── serializable.py ├── thundra_agent.py ├── timeout.py ├── utils.py └── wrappers ├── __init__.py ├── aws_lambda ├── __init__.py ├── handler.py ├── lambda_application_info_provider.py ├── lambda_event_utils.py ├── lambda_executor.py └── lambda_wrapper.py ├── base_wrapper.py ├── cp_wrapper_utils.py ├── django ├── __init__.py ├── django_executor.py ├── django_wrapper.py └── middleware.py ├── fastapi ├── __init__.py ├── fastapi_executor.py ├── fastapi_utils.py ├── fastapi_wrapper.py └── middleware.py ├── flask ├── __init__.py ├── flask_executor.py ├── flask_wrapper.py └── middleware.py ├── tornado ├── __init__.py ├── middleware.py ├── tornado_executor.py └── tornado_wrapper.py ├── web_wrapper_utils.py ├── wrapper_factory.py └── wrapper_utils.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.0.3 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:thundra/_version.py] 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Thundra CI Check 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Python 3.6.13 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: "3.6.13" 19 | - name: Start Docker Images 20 | run: docker-compose up -d --quiet-pull --no-recreate 21 | - name: Install pipenv 22 | run: | 23 | pip install pipenv 24 | pipenv --python 3.6.13 25 | - name: Install dependencies 26 | run: pipenv install --dev 27 | - name: Run Tests 28 | run: pipenv run pytest --junitxml=test-reports/pytest/junit.xml tests 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Release Python Agent 10 | 11 | on: 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | deploy: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: '3.8.12' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package to Test PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | with: 35 | password: ${{ secrets.THUNDRA_PYPI_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | .coverage 3 | dist/ 4 | build/ 5 | .serverless/ 6 | thundra.egg-info/ 7 | .benchmarks/ 8 | .pytest_cache/ 9 | .DS_Store 10 | .idea/ 11 | env/* 12 | __pycache__/ 13 | .vscode 14 | python/ 15 | python-* 16 | test-reports/ 17 | .venv 18 | TODO 19 | *.pyc 20 | *.toml -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers = 3 | pypitest 4 | 5 | [pypitest] 6 | repository:https://test.pypi.org -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @PythonAgentOwners 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | opentracing = "*" 8 | requests = "*" 9 | wrapt = "*" 10 | simplejson = "*" 11 | enum-compat = "*" 12 | jsonpickle = "*" 13 | websocket-client = "*" 14 | jsonpath-ng = "*" 15 | fastcounter = "*" 16 | GitPython = "*" 17 | pympler = "*" 18 | 19 | [dev-packages] 20 | mock = "*" 21 | pytest = "*" 22 | "boto3" = "*" 23 | redis = "*" 24 | "aws_xray_sdk" = "*" 25 | pymongo = "*" 26 | mysql-connector = "*" 27 | elasticsearch = "*" 28 | sqlalchemy = "*" 29 | psycopg2-binary = "*" 30 | django = "*" 31 | flask = "*" 32 | fastapi = "*" 33 | uvicorn = "*" 34 | async-exit-stack = "*" 35 | async-generator = "*" 36 | databases = "*" 37 | asyncpg = "*" 38 | httpx = "*" 39 | pytest-asyncio = "*" 40 | tornado = "*" 41 | pytest-tornado = "*" 42 | 43 | [requires] 44 | python_version = "3.6.4" 45 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | postgres: 4 | image: postgres:latest 5 | environment: 6 | - POSTGRES_PASSWORD=userpass 7 | - POSTGRES_USER=user 8 | - POSTGRES_DB=db 9 | ports: 10 | - '127.0.0.1:5432:5432' 11 | mysql: 12 | image: mysql:5.7 13 | environment: 14 | - MYSQL_ROOT_PASSWORD=rootpass 15 | - MYSQL_PASSWORD=userpass 16 | - MYSQL_USER=user 17 | - MYSQL_DATABASE=db 18 | ports: 19 | - "127.0.0.1:3306:3306" 20 | elasticsearch: 21 | image: docker.elastic.co/elasticsearch/elasticsearch:6.6.1 22 | container_name: elasticsearch 23 | environment: 24 | - cluster.name=docker-cluster 25 | - bootstrap.memory_lock=true 26 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 27 | ulimits: 28 | memlock: 29 | soft: -1 30 | hard: -1 31 | ports: 32 | - 9200:9200 33 | mongo: 34 | image: mongo 35 | ports: 36 | - "27017:27017" 37 | -------------------------------------------------------------------------------- /foresight/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ["THUNDRA_AGENT_TEST_ACTIVE"] = "True" -------------------------------------------------------------------------------- /foresight/constants/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/constants/__init__.py -------------------------------------------------------------------------------- /foresight/constants/constants.py: -------------------------------------------------------------------------------- 1 | import foresight.utils.generic_utils as utils 2 | 3 | class ForesightConstants: 4 | TEST_APP_INSTANCE_ID_PREFIX = utils.create_uuid4() + ":" 5 | TEST_APP_STAGE = "test" 6 | TEST_FIXTURE_DOMAIN_NAME = "TestFixture" 7 | TEST_DOMAIN_NAME = "Test" 8 | TEST_SUITE_DOMAIN_NAME = "TestSuite" 9 | TEST_OPERATION_NAME = "RunTest" 10 | MAX_TEST_METHOD_NAME = 100 11 | -------------------------------------------------------------------------------- /foresight/context/__init__.py: -------------------------------------------------------------------------------- 1 | from foresight.context.test_case_execution_context import TestCaseExecutionContext 2 | from foresight.context.test_suite_execution_context import TestSuiteExecutionContext -------------------------------------------------------------------------------- /foresight/context/test_case_execution_context.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context import ExecutionContext 2 | from foresight.test_runner_tags import TestRunnerTags 3 | 4 | class TestCaseExecutionContext(ExecutionContext): 5 | 6 | def __init__(self, **opts): 7 | super(TestCaseExecutionContext, self).__init__(**opts) 8 | self.node_id = opts.get("node_id", "") 9 | self.name = opts.get("name", "") 10 | self.method = opts.get("method", "") 11 | self.test_class = opts.get("test_class", '') 12 | self.test_suite_name = opts.get("test_suite_name", "") 13 | self.parent_transaction_id = opts.get("parent_transaction_id", "") 14 | self.status = opts.get("status", "") 15 | 16 | def set_status(self, status): 17 | self.status = status 18 | 19 | def get_operation_name(self): 20 | return "RunTest" 21 | 22 | def get_additional_start_tags(self): 23 | from foresight.test_runner_support import TestRunnerSupport 24 | test_run_scope = TestRunnerSupport.test_run_scope 25 | return { 26 | TestRunnerTags.TEST_RUN_ID: test_run_scope.id, 27 | TestRunnerTags.TEST_RUN_TASK_ID: test_run_scope.task_id, 28 | TestRunnerTags.TEST_NAME: self.name, 29 | TestRunnerTags.TEST_SUITE: self.test_suite_name, 30 | TestRunnerTags.TEST_METHOD: self.method, 31 | TestRunnerTags.TEST_CLASS: self.test_class, 32 | TestRunnerTags.TEST_SUITE_TRANSACTION_ID: self.parent_transaction_id 33 | } 34 | 35 | def get_additional_finish_tags(self): 36 | return { 37 | TestRunnerTags.TEST_STATUS: self.status 38 | } -------------------------------------------------------------------------------- /foresight/context/test_suite_execution_context.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context import ExecutionContext 2 | from foresight.test_runner_tags import TestRunnerTags 3 | from foresight.model import TestRunContext 4 | 5 | 6 | class TestSuiteExecutionContext(TestRunContext, ExecutionContext): 7 | 8 | def __init__(self, **opts): 9 | super(TestSuiteExecutionContext, self).__init__(**opts) 10 | super(TestRunContext, self).__init__(**opts) 11 | self.test_suite_name = opts.get("node_id", '') 12 | self.completed = False 13 | 14 | 15 | def get_operation_name(self): 16 | return "TEST_SUITE" 17 | 18 | 19 | def get_additional_start_tags(self): 20 | from foresight.test_runner_support import TestRunnerSupport 21 | test_run_scope = TestRunnerSupport.test_run_scope 22 | return { 23 | TestRunnerTags.TEST_RUN_ID: test_run_scope.id, 24 | TestRunnerTags.TEST_RUN_TASK_ID: test_run_scope.task_id, 25 | TestRunnerTags.TEST_SUITE: self.test_suite_name 26 | } 27 | 28 | 29 | def get_additional_finish_tags(self): 30 | return { 31 | TestRunnerTags.TEST_SUITE_FAILED_COUNT: self.failed_count.value, 32 | TestRunnerTags.TEST_SUITE_TOTAL_COUNT: self.total_count.value, 33 | TestRunnerTags.TEST_SUITE_ABORTED_COUNT: self.aborted_count.value, 34 | TestRunnerTags.TEST_SUITE_SKIPPED_COUNT: self.ignored_count.value, 35 | TestRunnerTags.TEST_SUITE_SUCCESSFUL_COUNT: self.successful_count.value, 36 | TestRunnerTags.TEST_TIMEOUT: self.timeout 37 | } -------------------------------------------------------------------------------- /foresight/environment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/__init__.py -------------------------------------------------------------------------------- /foresight/environment/azure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/azure/__init__.py -------------------------------------------------------------------------------- /foresight/environment/bitbucket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/bitbucket/__init__.py -------------------------------------------------------------------------------- /foresight/environment/bitbucket/bitbucket_environment_info_provider.py: -------------------------------------------------------------------------------- 1 | from foresight.environment.git.git_helper import GitHelper 2 | from foresight.environment.environment_info import EnvironmentInfo 3 | from foresight.utils.test_runner_utils import TestRunnerUtils 4 | import os, logging 5 | from foresight.utils.generic_utils import print_debug_message_to_console 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | class BitbucketEnvironmentInfoProvider: 10 | 11 | ENVIRONMENT = "BitBucket" 12 | BITBUCKET_GIT_HTTP_ORIGIN_ENV_VAR_NAME = "BITBUCKET_GIT_HTTP_ORIGIN" 13 | BITBUCKET_GIT_SSH_ORIGIN_ENV_VAR_NAME = "BITBUCKET_GIT_SSH_ORIGIN" 14 | BITBUCKET_BRANCH_ENV_VAR_NAME = "BITBUCKET_BRANCH" 15 | BITBUCKET_COMMIT_ENV_VAR_NAME = "BITBUCKET_COMMIT" 16 | BITBUCKET_BUILD_NUMBER_ENV_VAR_NAME = "BITBUCKET_BUILD_NUMBER" 17 | 18 | @classmethod 19 | def get_test_run_id(cls, repo_url, commit_hash): 20 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 21 | if configured_test_run_id: 22 | return configured_test_run_id 23 | build_number = os.getenv(cls.BITBUCKET_BUILD_NUMBER_ENV_VAR_NAME) 24 | if build_number: 25 | return TestRunnerUtils.get_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash, build_number) 26 | else: 27 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 28 | 29 | 30 | @classmethod 31 | def build_env_info(cls): 32 | try: 33 | repo_url = os.getenv(cls.BITBUCKET_GIT_HTTP_ORIGIN_ENV_VAR_NAME) 34 | if not repo_url: 35 | repo_url = os.getenv(cls.BITBUCKET_GIT_SSH_ORIGIN_ENV_VAR_NAME) 36 | repo_name = GitHelper.extractRepoName(repo_url) 37 | branch = os.getenv(cls.BITBUCKET_BRANCH_ENV_VAR_NAME) 38 | commit_hash = os.getenv(cls.BITBUCKET_COMMIT_ENV_VAR_NAME) 39 | commit_message = GitHelper.get_commit_message() 40 | 41 | if not branch: 42 | branch = GitHelper.get_branch() 43 | 44 | if not commit_hash: 45 | commit_hash = GitHelper.get_commit_hash() 46 | 47 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 48 | 49 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, repo_name, 50 | branch, commit_hash, commit_message) 51 | print_debug_message_to_console("Bitbucket Environment info: {}".format(env_info.to_json())) 52 | return env_info 53 | except Exception as err: 54 | print_debug_message_to_console("Unable to build environment info: {}".format(err)) 55 | LOGGER.error("Unable to build environment info: {}".format(err)) 56 | pass 57 | return None -------------------------------------------------------------------------------- /foresight/environment/circleci/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/circleci/__init__.py -------------------------------------------------------------------------------- /foresight/environment/circleci/circleci_environment_info_provider.py: -------------------------------------------------------------------------------- 1 | from foresight.environment.git.git_helper import GitHelper 2 | from foresight.environment.environment_info import EnvironmentInfo 3 | from foresight.utils.test_runner_utils import TestRunnerUtils 4 | import os, logging 5 | from foresight.utils.generic_utils import print_debug_message_to_console 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | class CircleCIEnvironmentInfoProvider: 10 | ENVIRONMENT = "CircleCI" 11 | CIRCLE_REPOSITORY_URL_ENV_VAR_NAME = "CIRCLE_REPOSITORY_URL" 12 | CIRCLE_BRANCH_ENV_VAR_NAME = "CIRCLE_BRANCH" 13 | CIRCLE_SHA1_ENV_VAR_NAME = "CIRCLE_SHA1" 14 | CIRCLE_BUILD_URL_ENV_VAR_NAME = "CIRCLE_BUILD_URL" 15 | CIRCLE_BUILD_NUM_ENV_VAR_NAME = "CIRCLE_BUILD_NUM" 16 | 17 | @classmethod 18 | def get_test_run_id(cls, repo_url, commit_hash): 19 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 20 | if configured_test_run_id: 21 | return configured_test_run_id 22 | build_url = os.getenv(cls.CIRCLE_BUILD_URL_ENV_VAR_NAME) 23 | build_num = os.getenv(cls.CIRCLE_BUILD_NUM_ENV_VAR_NAME) 24 | test_run_key = TestRunnerUtils.string_concat_by_underscore(build_url, build_num) 25 | if test_run_key != "": 26 | return TestRunnerUtils.get_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash, 27 | test_run_key) 28 | else: 29 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 30 | 31 | 32 | @classmethod 33 | def build_env_info(cls): 34 | try: 35 | repo_url = os.getenv(cls.CIRCLE_REPOSITORY_URL_ENV_VAR_NAME) 36 | repo_name = GitHelper.extractRepoName(repo_url) 37 | branch = os.getenv(cls.CIRCLE_BRANCH_ENV_VAR_NAME) 38 | commit_hash = os.getenv(cls.CIRCLE_SHA1_ENV_VAR_NAME) 39 | commit_message = GitHelper.get_commit_message() 40 | 41 | if not branch: 42 | branch = GitHelper.get_branch() 43 | 44 | if not commit_hash: 45 | commit_hash = GitHelper.get_commit_hash() 46 | 47 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 48 | 49 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, repo_name, branch, 50 | commit_hash, commit_message) 51 | print_debug_message_to_console("CircleCI Environment info: {}".format(env_info.to_json())) 52 | return env_info 53 | except Exception as err: 54 | print_debug_message_to_console("CircleCI Unable to build environment info: {}".format(err)) 55 | LOGGER.error("CircleCI Unable to build environment info: {}".format(err)) 56 | pass 57 | return None -------------------------------------------------------------------------------- /foresight/environment/environment_info.py: -------------------------------------------------------------------------------- 1 | class EnvironmentInfo: 2 | 3 | def __init__(self, test_run_id, environment=None, repo_url=None, repo_name=None, 4 | branch=None, commit_hash=None, commit_message=None): 5 | 6 | self.test_run_id = test_run_id 7 | self.environment = environment 8 | self.repo_url = repo_url 9 | self.repo_name = repo_name 10 | self.branch = branch 11 | self.commit_hash = commit_hash 12 | self.commit_message = commit_message 13 | 14 | def to_json(self): 15 | return { 16 | "testRunId": self.test_run_id, 17 | "environment": self.environment, 18 | "repoURL": self.repo_url, 19 | "repoName": self.repo_name, 20 | "branch": self.branch, 21 | "commitHash": self.commit_hash, 22 | "commitMessage": self.commit_message 23 | } -------------------------------------------------------------------------------- /foresight/environment/git/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/git/__init__.py -------------------------------------------------------------------------------- /foresight/environment/git/git_env_info_provider.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from foresight.environment.git.git_helper import GitHelper 3 | from foresight.environment.environment_info import EnvironmentInfo 4 | from foresight.utils.test_runner_utils import TestRunnerUtils 5 | from foresight.utils.generic_utils import print_debug_message_to_console 6 | 7 | LOGGER = logging.getLogger(__name__) 8 | 9 | class GitEnvironmentInfoProvider: 10 | 11 | ENVIRONMENT = "Git" 12 | 13 | @classmethod 14 | def get_test_run_id(cls, repo_url, commit_hash): 15 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 16 | if configured_test_run_id: 17 | return configured_test_run_id 18 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 19 | 20 | @classmethod 21 | def build_env_info(cls): 22 | try: 23 | repo_url = GitHelper.get_repo_url() 24 | if not repo_url: 25 | return None 26 | repo_name = GitHelper.extractRepoName(repo_url) 27 | branch = GitHelper.get_branch() 28 | commit_hash = GitHelper.get_commit_hash() 29 | commit_message = GitHelper.get_commit_message() 30 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 31 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, repo_name, 32 | branch, commit_hash, commit_message) 33 | print_debug_message_to_console("Git Environment info: {}".format(env_info.to_json())) 34 | return env_info 35 | except Exception as err: 36 | print_debug_message_to_console("Unable to build environment info: {}".format(err)) 37 | LOGGER.error("Unable to build environment info: {}".format(err)) 38 | pass 39 | return None -------------------------------------------------------------------------------- /foresight/environment/git/git_helper.py: -------------------------------------------------------------------------------- 1 | import __future__ 2 | import os 3 | import logging 4 | from foresight.environment.git.utils import backward_search_for_file 5 | 6 | LOGGER = logging.getLogger(__name__) 7 | 8 | class GitHelper: 9 | 10 | USER_DIR = os.getcwd() 11 | SOURCE_CODE_PATH = "sourceCodePath" 12 | REPOSITORY_URL = "repositoryURL" 13 | COMMIT_HASH = "commitHash" 14 | COMMIT_MESSAGE = "commitMessage" 15 | BRANCH = "branch" 16 | GIT_FOLDER_NAME = ".git" 17 | git_info_map = dict() 18 | 19 | 20 | @classmethod 21 | def get_source_root_path(cls): 22 | return cls.git_info_map.get(GitHelper.SOURCE_CODE_PATH, None) 23 | 24 | 25 | @classmethod 26 | def get_repo_url(cls): 27 | return cls.git_info_map.get(GitHelper.REPOSITORY_URL, None) 28 | 29 | 30 | @classmethod 31 | def get_branch(cls): 32 | return cls.git_info_map.get(GitHelper.BRANCH, None) 33 | 34 | 35 | @classmethod 36 | def get_commit_hash(cls): 37 | return cls.git_info_map.get(GitHelper.COMMIT_HASH, None) 38 | 39 | 40 | @classmethod 41 | def get_commit_message(cls): 42 | return cls.git_info_map.get(GitHelper.COMMIT_MESSAGE, None) 43 | 44 | 45 | @classmethod 46 | def populate_git_info_map(cls): 47 | git_folder_path = backward_search_for_file(cls.USER_DIR, cls.GIT_FOLDER_NAME) 48 | if not git_folder_path: 49 | LOGGER.debug("Could not locate " + cls.USER_DIR + " starting from user.dir: " + cls.GIT_FOLDER_NAME) 50 | cls.git_info_map = {} 51 | return 52 | try: 53 | from git import Repo 54 | git_repo = Repo(git_folder_path) 55 | active_branch = git_repo.active_branch 56 | commit_hash = str(active_branch.commit) 57 | commit_message = active_branch.commit.message 58 | repo_url = git_repo.remotes[0].config_reader.get("url") 59 | source_code_path = git_repo.working_dir 60 | cls.git_info_map[GitHelper.SOURCE_CODE_PATH] = source_code_path 61 | cls.git_info_map[GitHelper.BRANCH] = active_branch.name 62 | cls.git_info_map[GitHelper.COMMIT_HASH] = commit_hash 63 | cls.git_info_map[GitHelper.COMMIT_MESSAGE] = commit_message 64 | cls.git_info_map[GitHelper.REPOSITORY_URL] = repo_url 65 | except Exception as err: 66 | LOGGER.error("Couldn't set git_info_map: {}: ".format(err)) 67 | pass 68 | 69 | @staticmethod 70 | def extractRepoName(repo_url): 71 | try: 72 | return os.path.splitext(os.path.basename(repo_url))[0] 73 | except Exception as err: 74 | LOGGER.error("Couldn't extract Repo Name: {}".format(err)) 75 | pass 76 | 77 | GitHelper.populate_git_info_map() -------------------------------------------------------------------------------- /foresight/environment/git/utils.py: -------------------------------------------------------------------------------- 1 | import os, logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | # Platform independently user home folder path. 6 | user_home_dir = os.path.expanduser("~") 7 | 8 | 9 | def get_parent_dir(path): 10 | return os.path.abspath(os.path.join(path, os.pardir)) 11 | 12 | 13 | def backward_search_for_file(starting_path, filename_to_search): 14 | try: 15 | if starting_path and filename_to_search and starting_path != user_home_dir: 16 | current_folder_items = os.listdir(starting_path) 17 | if filename_to_search in current_folder_items: 18 | return starting_path 19 | starting_path = get_parent_dir(starting_path) 20 | return backward_search_for_file(starting_path, filename_to_search) 21 | except Exception as e: 22 | logger.error("backward_search_for_file error: {}".format(e)) 23 | pass 24 | return None -------------------------------------------------------------------------------- /foresight/environment/github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/github/__init__.py -------------------------------------------------------------------------------- /foresight/environment/gitlab/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/gitlab/__init__.py -------------------------------------------------------------------------------- /foresight/environment/gitlab/gitlab_environment_info_provider.py: -------------------------------------------------------------------------------- 1 | from foresight.environment.git.git_helper import GitHelper 2 | from foresight.environment.environment_info import EnvironmentInfo 3 | from foresight.utils.test_runner_utils import TestRunnerUtils 4 | import logging 5 | import os 6 | from foresight.utils.generic_utils import print_debug_message_to_console 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | class GitlabEnvironmentInfoProvider: 11 | 12 | ENVIRONMENT = "GitLab" 13 | CI_REPOSITORY_URL_ENV_VAR_NAME = "CI_REPOSITORY_URL" 14 | CI_COMMIT_BRANCH_ENV_VAR_NAME = "CI_COMMIT_BRANCH" 15 | CI_COMMIT_REF_NAME_ENV_VAR_NAME = "CI_COMMIT_REF_NAME" 16 | CI_COMMIT_SHA_ENV_VAR_NAME = "CI_COMMIT_SHA" 17 | CI_COMMIT_MESSAGE_ENV_VAR_NAME = "CI_COMMIT_MESSAGE" 18 | CI_JOB_ID_ENV_VAR_NAME = "CI_JOB_ID" 19 | CI_JOB_URL_ENV_VAR_NAME = "CI_JOB_URL" 20 | 21 | 22 | @classmethod 23 | def get_test_run_id(cls, repo_url, commit_hash): 24 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 25 | if configured_test_run_id: 26 | return configured_test_run_id 27 | job_id = os.getenv(cls.CI_JOB_ID_ENV_VAR_NAME) 28 | job_url = os.getenv(cls.CI_JOB_URL_ENV_VAR_NAME) 29 | test_run_key = TestRunnerUtils.string_concat_by_underscore(job_id, job_url) 30 | if test_run_key != "": 31 | return TestRunnerUtils.get_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash, 32 | test_run_key) 33 | else: 34 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 35 | 36 | 37 | @classmethod 38 | def build_env_info(cls): 39 | try: 40 | repo_url = os.getenv(cls.CI_REPOSITORY_URL_ENV_VAR_NAME) 41 | repo_name = GitHelper.extractRepoName(repo_url) 42 | branch = os.getenv(cls.CI_COMMIT_BRANCH_ENV_VAR_NAME) 43 | if not branch: 44 | branch = os.getenv(cls.CI_COMMIT_REF_NAME_ENV_VAR_NAME) 45 | commit_hash = os.getenv(cls.CI_COMMIT_SHA_ENV_VAR_NAME) 46 | commit_message = os.getenv(cls.CI_COMMIT_MESSAGE_ENV_VAR_NAME) 47 | 48 | if not branch: 49 | branch = GitHelper.get_branch() 50 | 51 | if not commit_hash: 52 | commit_hash = GitHelper.get_commit_hash() 53 | 54 | if not commit_message: 55 | commit_message = GitHelper.get_commit_message() 56 | 57 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 58 | 59 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, repo_name, branch, commit_hash, commit_message) 60 | print_debug_message_to_console("Gitlab Environment info: {}".format(env_info.to_json())) 61 | return env_info 62 | except Exception as err: 63 | print_debug_message_to_console("Gitlab Unable to build environment info: {}".format(err)) 64 | LOGGER.error("Gitlab Unable to build environment info: {}".format(err)) 65 | pass 66 | return None -------------------------------------------------------------------------------- /foresight/environment/jenkins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/jenkins/__init__.py -------------------------------------------------------------------------------- /foresight/environment/jenkins/jenkins_environment_info_provider.py: -------------------------------------------------------------------------------- 1 | from foresight.environment.environment_info import EnvironmentInfo 2 | from foresight.environment.git.git_helper import GitHelper 3 | from foresight.utils.test_runner_utils import TestRunnerUtils 4 | import os 5 | import logging 6 | from foresight.utils.generic_utils import print_debug_message_to_console 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | class JenkinsEnvironmentInfoProvider: 11 | 12 | ENVIRONMENT = "Jenkins" 13 | GIT_URL_ENV_VAR_NAME = "GIT_URL" 14 | GIT_URL_1_ENV_VAR_NAME = "GIT_URL_1" 15 | GIT_BRANCH_ENV_VAR_NAME = "GIT_BRANCH" 16 | GIT_COMMIT_ENV_VAR_NAME = "GIT_COMMIT" 17 | JOB_NAME_ENV_VAR_NAME = "JOB_NAME" 18 | BUILD_ID_ENV_VAR_NAME = "BUILD_ID" 19 | 20 | @classmethod 21 | def get_test_run_id(cls, repo_url, commit_hash): 22 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 23 | if configured_test_run_id: 24 | return configured_test_run_id 25 | job_name = os.getenv(cls.JOB_NAME_ENV_VAR_NAME) 26 | build_id = os.getenv(cls.BUILD_ID_ENV_VAR_NAME) 27 | test_run_key = TestRunnerUtils.string_concat_by_underscore(job_name, build_id) 28 | if test_run_key != "": 29 | return TestRunnerUtils.get_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash, 30 | test_run_key) 31 | else: 32 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 33 | 34 | 35 | @classmethod 36 | def build_env_info(cls): 37 | try: 38 | repo_url = os.getenv(cls.GIT_URL_ENV_VAR_NAME) 39 | if not repo_url: 40 | repo_url = os.getenv(cls.GIT_URL_1_ENV_VAR_NAME) 41 | 42 | repo_name = GitHelper.extractRepoName(repo_url) 43 | branch = os.getenv(cls.GIT_BRANCH_ENV_VAR_NAME) 44 | commit_hash = os.getenv(cls.GIT_COMMIT_ENV_VAR_NAME) 45 | commit_message = GitHelper.get_commit_message() 46 | 47 | if not branch: 48 | branch = GitHelper.get_branch() 49 | 50 | if not commit_hash: 51 | commit_hash = GitHelper.get_commit_hash() 52 | 53 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 54 | 55 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, repo_name, 56 | branch, commit_hash, commit_message) 57 | print_debug_message_to_console("Jenkins Environment info: {}".format(env_info.to_json())) 58 | return env_info 59 | except Exception as err: 60 | print_debug_message_to_console("Jenkins Unable to build environment info: {}".format(err)) 61 | LOGGER.error("Jenkins Unable to build environment info: {}".format(err)) 62 | pass 63 | return None 64 | 65 | -------------------------------------------------------------------------------- /foresight/environment/travisci/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/environment/travisci/__init__.py -------------------------------------------------------------------------------- /foresight/environment/travisci/travisci_environment_info_provider.py: -------------------------------------------------------------------------------- 1 | from foresight.environment.git.git_helper import GitHelper 2 | from foresight.environment.environment_info import EnvironmentInfo 3 | from foresight.utils.test_runner_utils import TestRunnerUtils 4 | import os, logging 5 | from foresight.utils.generic_utils import print_debug_message_to_console 6 | 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class TravisCIEnvironmentInfoProvider: 12 | ENVIRONMENT = "TravisCI" 13 | TRAVIS_REPO_SLUG_VAR_NAME = "TRAVIS_REPO_SLUG" 14 | TRAVIS_PULL_REQUEST_BRANCH_ENV_VAR_NAME = "TRAVIS_PULL_REQUEST_BRANCH" 15 | TRAVIS_BRANCH_ENV_VAR_NAME = "TRAVIS_BRANCH" 16 | TRAVIS_COMMIT_ENV_VAR_NAME = "TRAVIS_COMMIT" 17 | TRAVIS_COMMIT_MESSAGE_ENV_VAR_NAME = "TRAVIS_COMMIT_MESSAGE" 18 | TRAVIS_BUILD_WEB_URL_ENV_VAR_NAME = "TRAVIS_BUILD_WEB_URL" 19 | TRAVIS_BUILD_ID_ENV_VAR_NAME = "TRAVIS_BUILD_ID" 20 | 21 | 22 | @classmethod 23 | def get_test_run_id(cls, repo_url, commit_hash): 24 | configured_test_run_id = TestRunnerUtils.get_configured_test_run_id() 25 | if configured_test_run_id: 26 | return configured_test_run_id 27 | build_web_url = os.getenv(cls.TRAVIS_BUILD_WEB_URL_ENV_VAR_NAME) 28 | build_id = os.getenv(cls.TRAVIS_BUILD_ID_ENV_VAR_NAME) 29 | test_run_key = TestRunnerUtils.string_concat_by_underscore(build_web_url, build_id) 30 | if test_run_key != "": 31 | return TestRunnerUtils.get_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash, 32 | test_run_key) 33 | else: 34 | return TestRunnerUtils.get_default_test_run_id(cls.ENVIRONMENT, repo_url, commit_hash) 35 | 36 | 37 | @classmethod 38 | def build_env_info(cls): 39 | try: 40 | repo_url = "https://github.com/{}.git".format(os.getenv(cls.TRAVIS_REPO_SLUG_VAR_NAME)) 41 | repo_name = GitHelper.extractRepoName(repo_url) 42 | branch = os.getenv(cls.TRAVIS_PULL_REQUEST_BRANCH_ENV_VAR_NAME) 43 | commit_hash = os.getenv(cls.TRAVIS_COMMIT_ENV_VAR_NAME) 44 | commit_message = os.getenv(cls.TRAVIS_COMMIT_MESSAGE_ENV_VAR_NAME) 45 | 46 | if not branch: 47 | branch = GitHelper.get_branch() 48 | 49 | if not commit_hash: 50 | commit_hash = GitHelper.get_commit_hash() 51 | 52 | if not commit_message: 53 | commit_message = GitHelper.get_commit_message() 54 | 55 | test_run_id = cls.get_test_run_id(repo_url, commit_hash) 56 | 57 | env_info = EnvironmentInfo(test_run_id, cls.ENVIRONMENT, repo_url, 58 | repo_name, branch, commit_hash, commit_message) 59 | print_debug_message_to_console("TravisCI Environment info: {}".format(env_info.to_json())) 60 | return env_info 61 | except Exception as err: 62 | print_debug_message_to_console("TravisCI Unable to build environment info: {}".format(err)) 63 | LOGGER.error("TravisCI Unable to build environment info: {}".format(err)) 64 | pass 65 | return None -------------------------------------------------------------------------------- /foresight/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_run_context import TestRunContext 2 | from .test_run_finish import TestRunFinish 3 | from .test_run_monitoring import TestRunMonitoring 4 | from .test_run_result import TestRunResult 5 | from .test_run_start import TestRunStart 6 | from .test_run_status import TestRunStatus 7 | from .terminator import Terminator, ThreadExecutorTerminator 8 | 9 | __all__ = [ 10 | "TestRunContext", 11 | "TestRunFinish", 12 | "TestRunMonitoring", 13 | "TestRunResult", 14 | "TestRunStart", 15 | "TestRunStatus", 16 | "Terminator", 17 | "ThreadExecutorTerminator" 18 | ] -------------------------------------------------------------------------------- /foresight/model/terminator.py: -------------------------------------------------------------------------------- 1 | from thundra.utils import Singleton 2 | import foresight.utils.generic_utils as utils 3 | import logging, threading 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | class ThreadExecutorTerminator(threading.Thread): 8 | def __init__(self, *args, **kwargs): 9 | threading.Thread.__init__(self) 10 | self.event = threading.Event() 11 | 12 | def run(self): 13 | terminator = Terminator() 14 | terminator._wait() 15 | 16 | class Terminator(Singleton): 17 | def __init__(self): 18 | self.tasks = [] 19 | 20 | 21 | def register_task(self, task): # task => BaseWrapper 22 | self.tasks.append(task) 23 | 24 | 25 | def _wait(self): 26 | for task in self.tasks: 27 | try: 28 | task.thread_pool_executor.shutdown(wait=True) 29 | except Exception as e: 30 | logger.error(f"Task wait error in Terminator".format(e)) 31 | 32 | 33 | def wait(self, timeout=30): 34 | terminator_thread = ThreadExecutorTerminator() 35 | terminator_thread.start() 36 | terminator_thread.join(timeout) 37 | if terminator_thread.is_alive(): 38 | logger.debug("Thread is killed by event!") 39 | terminator_thread.event.set() 40 | else: 41 | logger.debug("Thread has already finished!") -------------------------------------------------------------------------------- /foresight/model/test_run_context.py: -------------------------------------------------------------------------------- 1 | import fastcounter 2 | 3 | class TestRunContext: 4 | 5 | def __init__(self, total_count=0, successful_count = 0, failed_count=0, ignored_count=0, 6 | aborted_count=0, **ignored): 7 | self.total_count = fastcounter.FastWriteCounter(total_count) 8 | self.successful_count = fastcounter.FastWriteCounter(successful_count) 9 | self.failed_count = fastcounter.FastWriteCounter(failed_count) 10 | self.ignored_count = fastcounter.FastWriteCounter(ignored_count) 11 | self.aborted_count = fastcounter.FastWriteCounter(aborted_count) 12 | 13 | def increase_total_count(self, count=1): 14 | self.total_count.increment(count) 15 | 16 | def increase_successful_count(self): 17 | self.successful_count.increment() 18 | 19 | def increase_failed_count(self): 20 | self.failed_count.increment() 21 | 22 | def increase_ignored_count(self, count=1): 23 | self.ignored_count.increment(count) 24 | 25 | def increase_aborted_count(self): 26 | self.aborted_count.increment() 27 | 28 | def get_total_count(self): 29 | return self.total_count.value 30 | 31 | def get_successful_count(self): 32 | return self.successful_count.value 33 | 34 | def get_failed_count(self): 35 | return self.failed_count.value 36 | 37 | def get_ignored_count(self): 38 | return self.ignored_count.value 39 | 40 | def get_aborted_count(self): 41 | return self.aborted_count.value -------------------------------------------------------------------------------- /foresight/model/test_run_finish.py: -------------------------------------------------------------------------------- 1 | from foresight.model.test_run_result import TestRunResult 2 | from foresight.model.test_run_monitoring import TestRunMonitoring 3 | 4 | class TestRunFinish(TestRunResult, TestRunMonitoring): 5 | EVENT_NAME = "TestRunFinish" 6 | 7 | def __init__(self, id=None, project_id=None, task_id=None, total_count=None, successful_count=None, 8 | failed_count=None, ignored_count=None, aborted_count=None, start_timestamp=None, finish_timestamp=None, 9 | duration=None, host_name=None, environment_info=None, tags=None): 10 | super(TestRunFinish, self).__init__(total_count, successful_count, failed_count, 11 | ignored_count, aborted_count) 12 | self.id = id 13 | self.project_id = project_id 14 | self.task_id = task_id 15 | self.start_timestamp = start_timestamp 16 | self.finish_timestamp = finish_timestamp 17 | self.duration = duration 18 | self.host_name = host_name 19 | if environment_info != None: 20 | self.environment = environment_info.environment if environment_info.environment else None 21 | self.repo_url = environment_info.repo_url if environment_info.repo_url else None 22 | self.repo_name = environment_info.repo_name if environment_info.repo_name else None 23 | self.branch = environment_info.branch if environment_info.branch else None 24 | self.commit_hash = environment_info.commit_hash if environment_info.commit_hash else None 25 | self.commit_message = environment_info.commit_message if environment_info.commit_message else None 26 | else: 27 | self.environment = None 28 | self.repo_url = None 29 | self.repo_name = None 30 | self.branch = None 31 | self.commit_hash = None 32 | self.commit_message = None 33 | self.tags = tags 34 | 35 | def to_json(self): 36 | return { 37 | "id": self.id, 38 | "projectId": self.project_id, 39 | "taskId": self.task_id, 40 | "type": self.EVENT_NAME, 41 | "agentVersion": self.AGENT_VERSION, 42 | "dataModelVersion": self.TEST_RUN_DATA_MODEL_VERSION, 43 | "totalCount" : self.total_count, 44 | "successfulCount" : self.successful_count, 45 | "failedCount" : self.failed_count, 46 | "ignoredCount" : self.ignored_count, 47 | "abortedCount" : self.aborted_count, 48 | "startTimestamp" : self.start_timestamp, 49 | "finishTimestamp" : self.finish_timestamp, 50 | "duration" : self.duration, 51 | "environment": self.environment, 52 | "repoURL": self.repo_url, 53 | "repoName": self.repo_name, 54 | "branch": self.branch, 55 | "commitHash": self.commit_hash, 56 | "commitMessage": self.commit_message, 57 | "tags": self.tags 58 | } 59 | 60 | def get_monitoring_data(self): 61 | monitoring_data = super().get_monitoring_data() 62 | monitoring_data["type"] = self.EVENT_NAME 63 | monitoring_data["data"] = self.to_json() 64 | return monitoring_data -------------------------------------------------------------------------------- /foresight/model/test_run_monitoring.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from thundra.constants import (TEST_RUN_EVENTS_DATA_VERSION, 3 | DATA_FORMAT_VERSION, 4 | THUNDRA_AGENT_VERSION) 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.config.config_names import THUNDRA_APIKEY 7 | 8 | 9 | ABC = abc.ABCMeta('ABC', (object,), {}) 10 | 11 | class TestRunMonitoring(ABC): 12 | 13 | TEST_RUN_DATA_MODEL_VERSION = TEST_RUN_EVENTS_DATA_VERSION 14 | DATA_FORMAT_VERSION = DATA_FORMAT_VERSION 15 | AGENT_VERSION = THUNDRA_AGENT_VERSION 16 | 17 | def get_monitoring_data(self): 18 | return { 19 | 'apiKey': ConfigProvider.get(THUNDRA_APIKEY, None), 20 | 'type': None, 21 | 'dataModelVersion': self.DATA_FORMAT_VERSION, 22 | 'data': None 23 | } -------------------------------------------------------------------------------- /foresight/model/test_run_result.py: -------------------------------------------------------------------------------- 1 | class TestRunResult: 2 | 3 | def __init__(self, total_count=0, successful_count=0, failed_count=0, 4 | ignored_count=0, aborted_count=0): 5 | self.total_count = total_count 6 | self.successful_count = successful_count 7 | self.failed_count = failed_count 8 | self.ignored_count = ignored_count 9 | self.aborted_count = aborted_count 10 | 11 | 12 | def to_json(self): 13 | return { 14 | "totalCount" : self.total_count, 15 | "successfulCount" : self.successful_count, 16 | "failedCount" : self.failed_count, 17 | "ignoredCount" : self.ignored_count, 18 | "abortedCount" : self.aborted_count, 19 | } -------------------------------------------------------------------------------- /foresight/model/test_run_start.py: -------------------------------------------------------------------------------- 1 | from foresight.model.test_run_monitoring import TestRunMonitoring 2 | 3 | class TestRunStart(TestRunMonitoring): 4 | 5 | EVENT_NAME = "TestRunStart" 6 | 7 | def __init__(self, id=None, project_id=None, task_id=None, start_timestamp=None, 8 | host_name=None, environment_info=None, tags=None): 9 | self.id = id 10 | self.project_id = project_id 11 | self.task_id = task_id 12 | self.start_timestamp = start_timestamp 13 | self.host_name = host_name 14 | if environment_info != None: 15 | self.environment = environment_info.environment if environment_info.environment else None 16 | self.repo_url = environment_info.repo_url if environment_info.repo_url else None 17 | self.repo_name = environment_info.repo_name if environment_info.repo_name else None 18 | self.branch = environment_info.branch if environment_info.branch else None 19 | self.commit_hash = environment_info.commit_hash if environment_info.commit_hash else None 20 | self.commit_message = environment_info.commit_message if environment_info.commit_message else None 21 | else: 22 | self.environment = None 23 | self.repo_url = None 24 | self.repo_name = None 25 | self.branch = None 26 | self.commit_hash = None 27 | self.commit_message = None 28 | self.tags = tags 29 | 30 | def to_json(self): 31 | return { 32 | "id": self.id, 33 | "projectId": self.project_id, 34 | "taskId": self.task_id, 35 | "type": self.EVENT_NAME, 36 | "agentVersion": self.AGENT_VERSION, 37 | "dataModelVersion": self.TEST_RUN_DATA_MODEL_VERSION, 38 | "startTimestamp" : self.start_timestamp, 39 | "environment": self.environment, 40 | "repoURL": self.repo_url, 41 | "repoName": self.repo_name, 42 | "branch": self.branch, 43 | "commitHash": self.commit_hash, 44 | "commitMessage": self.commit_message, 45 | "tags": self.tags 46 | } 47 | 48 | def get_monitoring_data(self): 49 | monitoring_data = super().get_monitoring_data() 50 | monitoring_data["type"] = self.EVENT_NAME 51 | monitoring_data["data"] = self.to_json() 52 | return monitoring_data -------------------------------------------------------------------------------- /foresight/model/test_run_status.py: -------------------------------------------------------------------------------- 1 | from foresight.model.test_run_result import TestRunResult 2 | from foresight.model.test_run_monitoring import TestRunMonitoring 3 | 4 | class TestRunStatus(TestRunResult, TestRunMonitoring): 5 | 6 | EVENT_NAME="TestRunStatus" 7 | 8 | def __init__(self, id=None, project_id=None, task_id=None, total_count=0, successful_count=0, 9 | failed_count=0, ignored_count=0, aborted_count=0, start_timestamp=None, status_timestamp=None, 10 | host_name=None, environment_info=None, tags=None): 11 | super(TestRunStatus, self).__init__(total_count, successful_count, failed_count, 12 | ignored_count, aborted_count) 13 | self.id = id 14 | self.project_id = project_id 15 | self.task_id = task_id 16 | self.start_timestamp = start_timestamp 17 | self.status_timestamp = status_timestamp 18 | self.host_name = host_name 19 | if environment_info != None: 20 | self.environment = environment_info.environment if environment_info.environment else None 21 | self.repo_url = environment_info.repo_url if environment_info.repo_url else None 22 | self.repo_name = environment_info.repo_name if environment_info.repo_name else None 23 | self.branch = environment_info.branch if environment_info.branch else None 24 | self.commit_hash = environment_info.commit_hash if environment_info.commit_hash else None 25 | self.commit_message = environment_info.commit_message if environment_info.commit_message else None 26 | else: 27 | self.environment = None 28 | self.repo_url = None 29 | self.repo_name = None 30 | self.branch = None 31 | self.commit_hash = None 32 | self.commit_message = None 33 | self.tags = tags 34 | 35 | def to_json(self): 36 | return { 37 | "id": self.id, 38 | "projectId": self.project_id, 39 | "taskId": self.task_id, 40 | "type": self.EVENT_NAME, 41 | "agentVersion": self.AGENT_VERSION, 42 | "dataModelVersion": self.TEST_RUN_DATA_MODEL_VERSION, 43 | "totalCount" : self.total_count, 44 | "successfulCount" : self.successful_count, 45 | "failedCount" : self.failed_count, 46 | "ignoredCount" : self.ignored_count, 47 | "abortedCount" : self.aborted_count, 48 | "startTimestamp" : self.start_timestamp, 49 | "statusTimestamp" : self.status_timestamp, 50 | "environment": self.environment, 51 | "repoURL": self.repo_url, 52 | "repoName": self.repo_name, 53 | "branch": self.branch, 54 | "commitHash": self.commit_hash, 55 | "commitMessage": self.commit_message, 56 | "tags": self.tags 57 | } 58 | def get_monitoring_data(self): 59 | monitoring_data = super().get_monitoring_data() 60 | monitoring_data["type"] = self.EVENT_NAME 61 | monitoring_data["data"] = self.to_json() 62 | return monitoring_data -------------------------------------------------------------------------------- /foresight/pytest_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/pytest_integration/__init__.py -------------------------------------------------------------------------------- /foresight/pytest_integration/constants.py: -------------------------------------------------------------------------------- 1 | THUNDRA_MARKED_AS_SKIPPED = "thundra_marked_as_skipped" 2 | THUNDRA_TEST_ALREADY_FINISHED = "thundra_test_already_finished" 3 | THUNDRA_TEST_STARTED = "thundra_test_started" 4 | THUNDRA_TEST_RESULTED = "thundra_test_resulted" 5 | THUNDRA_TEST_FINISH_IN_HELPER = "thundra_test_finish_in_helper" 6 | THUNDRA_FIXTURE_PREFIX = "x_thundra" 7 | THUNDRA_SCOPE = "x-thundra-scope" -------------------------------------------------------------------------------- /foresight/sampler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/sampler/__init__.py -------------------------------------------------------------------------------- /foresight/sampler/max_count_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from thundra.samplers.base_sampler import BaseSampler 2 | import fastcounter 3 | 4 | class MaxCountAwareSampler(BaseSampler): 5 | 6 | def __init__(self, max_count): 7 | self.max_count = max_count 8 | self.counter = fastcounter.FastWriteCounter() 9 | 10 | def is_sampled(self, args=None): 11 | self.counter.increment() 12 | return self.counter.value <= self.max_count -------------------------------------------------------------------------------- /foresight/test_runner_tags.py: -------------------------------------------------------------------------------- 1 | class TestRunnerTags: 2 | TEST_RUN_ID = "test.run.id" 3 | TEST_RUN_TASK_ID = "test.run.task.id" 4 | TEST_SUITE_TRANSACTION_ID = "test.suite.transaction.id" 5 | TEST_NAME = "test.name" 6 | TEST_SUITE = "test.suite" 7 | TEST_FIXTURE = "test.fixture" 8 | TEST_STATUS = "test.status" 9 | TEST_SUITE_TOTAL_COUNT = "test.suite.total.count" 10 | TEST_SUITE_SUCCESSFUL_COUNT = "test.suite.successful.count" 11 | TEST_SUITE_FAILED_COUNT = "test.suite.failed.count" 12 | TEST_SUITE_ABORTED_COUNT = "test.suite.aborted.count" 13 | TEST_SUITE_SKIPPED_COUNT = "test.suite.skipped.count" 14 | TEST_METHOD = "test.method" 15 | TEST_CLASS = "test.class" 16 | TEST_TIMEOUT = "test.timeout" 17 | TEST_BEFORE_EACH_DURATION = "test.before.each.duration" 18 | TEST_AFTER_EACH_DURATION = "test.after.each.duration" 19 | TEST_BEFORE_ALL_DURATION = "test.before.all.duration" 20 | TEST_AFTER_ALL_DURATION = "test.after.all.duration" 21 | TEST_ENVIRONMENT = "test.env" 22 | SOURCE_CODE_REPO_URL = "src.repo.url" 23 | SOURCE_CODE_REPO_NAME = "src.repo.name" 24 | SOURCE_CODE_BRANCH = "src.branch" 25 | SOURCE_CODE_COMMIT_HASH = "src.commit.hash" 26 | SOURCE_CODE_COMMIT_MESSAGE = "src.commit.message" -------------------------------------------------------------------------------- /foresight/test_status.py: -------------------------------------------------------------------------------- 1 | from foresight.test_runner_support import TestRunnerSupport 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | class TestStatus: 7 | SUCCESSFUL = "SUCCESSFUL" 8 | FAILED = "FAILED" 9 | ABORTED = "ABORTED" 10 | SKIPPED = "SKIPPED" 11 | TOTAL = "TOTAL" 12 | 13 | def increase_successful_count(): 14 | try: 15 | test_run_context = TestRunnerSupport.test_run_scope 16 | test_suite_context = TestRunnerSupport.test_suite_execution_context 17 | 18 | test_run_context.context.increase_successful_count() 19 | test_suite_context.increase_successful_count() 20 | _increase_total_count(test_run_context, test_suite_context) 21 | except Exception as err: 22 | logger.error("increase_successful_count error: {}".format(err)) 23 | pass 24 | 25 | 26 | def increase_failed_count(): 27 | try: 28 | test_run_context = TestRunnerSupport.test_run_scope 29 | test_suite_context = TestRunnerSupport.test_suite_execution_context 30 | 31 | test_run_context.context.increase_failed_count() 32 | test_suite_context.increase_failed_count() 33 | _increase_total_count(test_run_context, test_suite_context) 34 | except Exception as err: 35 | logger.error("increase_failed_count error: {}".format(err)) 36 | pass 37 | 38 | def increase_aborted_count(): 39 | try: 40 | test_run_context = TestRunnerSupport.test_run_scope 41 | test_suite_context = TestRunnerSupport.test_suite_execution_context 42 | 43 | test_run_context.context.increase_aborted_count() 44 | test_suite_context.increase_aborted_count() 45 | _increase_total_count(test_run_context, test_suite_context) 46 | except Exception as err: 47 | logger.error("increase_aborted_count error: {}".format(err)) 48 | pass 49 | 50 | 51 | def increase_skipped_count(): 52 | try: 53 | test_run_context = TestRunnerSupport.test_run_scope 54 | test_suite_context = TestRunnerSupport.test_suite_execution_context 55 | 56 | test_run_context.context.increase_ignored_count() 57 | test_suite_context.increase_ignored_count() 58 | _increase_total_count(test_run_context, test_suite_context) 59 | except Exception as err: 60 | logger.error("increase_skipped_count error: {}".format(err)) 61 | pass 62 | 63 | 64 | def _increase_total_count(test_run_context, test_suite_context): 65 | try: 66 | test_run_context.context.increase_total_count() 67 | test_suite_context.increase_total_count() 68 | except Exception as err: 69 | logger.error("_increase_total_count error: {}".format(err)) 70 | pass 71 | 72 | increase_actions = { 73 | TestStatus.SUCCESSFUL: increase_successful_count, 74 | TestStatus.FAILED: increase_failed_count, 75 | TestStatus.ABORTED: increase_aborted_count, 76 | TestStatus.SKIPPED: increase_skipped_count, 77 | } -------------------------------------------------------------------------------- /foresight/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/foresight/utils/__init__.py -------------------------------------------------------------------------------- /foresight/utils/generic_utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import uuid 3 | 4 | def current_milli_time(): 5 | return int(time.time() * 1000) 6 | 7 | 8 | def create_uuid4(): 9 | return str(uuid.uuid4()) 10 | 11 | 12 | def print_debug_message_to_console(msg): 13 | from thundra.plugins.log.thundra_logger import debug_logger 14 | from thundra.config.config_provider import ConfigProvider 15 | from thundra.config import config_names 16 | if ConfigProvider.get(config_names.THUNDRA_DEBUG_ENABLE): 17 | debug_logger("[THUNDRA] " + msg) -------------------------------------------------------------------------------- /foresight/utils/test_runner_utils.py: -------------------------------------------------------------------------------- 1 | from thundra.config.config_provider import ConfigProvider 2 | from thundra.config import config_names 3 | import foresight.utils.generic_utils as utils 4 | import uuid, logging 5 | 6 | LOGGER = logging.getLogger(__name__) 7 | 8 | class NULL_NAMESPACE: 9 | bytes = b'' 10 | 11 | class TestRunnerUtils: 12 | 13 | @staticmethod 14 | def get_configured_test_run_id(): 15 | return ConfigProvider.get(config_names.THUNDRA_TEST_RUN_ID) 16 | 17 | 18 | @classmethod 19 | def get_test_run_id(cls, environment, repo_url, commit_hash, test_run_key): 20 | try: 21 | test_run_id_seed = cls.string_concat_by_underscore(environment, repo_url, 22 | commit_hash, test_run_key) 23 | # UUID.nameUUIDFromBytes is used in java and its uuid version is 3. To keep compatibality, using uuid3. 24 | # https://stackoverflow.com/questions/27939281/reproduce-uuid-from-java-code-in-python 25 | return str(uuid.uuid3(NULL_NAMESPACE, test_run_id_seed)) 26 | except Exception as err: 27 | LOGGER.error("Test run id could not be created by uuid: {}".format(err)) 28 | pass 29 | return str(uuid.uuid4()) 30 | 31 | 32 | @staticmethod 33 | def get_default_test_run_id(environment=None, repo_url=None, commit_hash=None): 34 | try: 35 | #TODO Find a way to generate uuid with parameters 36 | return utils.create_uuid4() 37 | except Exception as err: 38 | LOGGER.error("get_default_test_run_id couldn't created") 39 | pass 40 | 41 | 42 | @staticmethod 43 | def string_concat_by_underscore(*str_list): 44 | try: 45 | return "_".join(str_list) 46 | except Exception as err: 47 | LOGGER.error("test runner utils string concat error: {}".format(err)) 48 | pass 49 | return "" -------------------------------------------------------------------------------- /layer/deploy_layer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | BUCKET_PREFIX=$1 5 | LAYER_NAME_SUFFIX=$2 6 | 7 | REGIONS=( "ap-northeast-1" "ap-northeast-2" "ap-south-1" "ap-southeast-1" "ap-southeast-2" "ca-central-1" "eu-central-1" "eu-north-1" "eu-west-1" "eu-west-2" "eu-west-3" "sa-east-1" "us-east-1" "us-east-2" "us-west-1" "us-west-2" ) 8 | LAYER_NAME_BASE="thundra-lambda-python-layer" 9 | LAYER_NAME="$LAYER_NAME_BASE" 10 | 11 | if [[ ! -z "$LAYER_NAME_SUFFIX" ]]; then 12 | LAYER_NAME="$LAYER_NAME_BASE-$LAYER_NAME_SUFFIX" 13 | fi 14 | 15 | SCRIPT_PATH=${0%/*} 16 | STATEMENT_ID_BASE="$LAYER_NAME_BASE-$(($(date +%s)))" 17 | 18 | echo "Creating layer zip for: '$LAYER_NAME'" 19 | rm -rf $SCRIPT_PATH/python 20 | pip3 install thundra -t python 21 | export VERSION=$(python3.6 ../setup.py --version) 22 | if [ ! -f $SCRIPT_PATH/python/thundra/handler.py ]; then 23 | echo "Wrapper handler not found in the pip version of thundra, adding manually..." 24 | cp $SCRIPT_PATH/../thundra/handler.py $SCRIPT_PATH/python/thundra/ 25 | fi 26 | zip -r "$LAYER_NAME.zip" $SCRIPT_PATH/python/ --exclude=*.DS_Store* --exclude=*.sh --exclude=*.git* --exclude=README.md --exclude=*.dist-info/* --exclude=*__pycache__/* 27 | 28 | echo "Zip completed." 29 | 30 | for REGION in "${REGIONS[@]}" 31 | do 32 | ARTIFACT_BUCKET=$BUCKET_PREFIX-$REGION 33 | ARTIFACT_OBJECT=layers/python/thundra-agent-lambda-layer-$VERSION.zip 34 | 35 | echo "Uploading '$LAYER_NAME.zip' at $ARTIFACT_BUCKET with key $ARTIFACT_OBJECT" 36 | 37 | aws s3 cp "$SCRIPT_PATH/$LAYER_NAME.zip" "s3://$ARTIFACT_BUCKET/$ARTIFACT_OBJECT" 38 | 39 | echo "Uploaded '$LAYER_NAME' to $ARTIFACT_BUCKET" 40 | done 41 | 42 | rm -rf "$SCRIPT_PATH/$LAYER_NAME.zip" 43 | -------------------------------------------------------------------------------- /layer/release_layer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | export VERSION=$(python3.6 ../setup.py --version) 4 | BUCKET_PREFIX=$1 5 | LAYER_NAME_SUFFIX=$2 6 | REGIONS=( "ap-northeast-1" "ap-northeast-2" "ap-south-1" "ap-southeast-1" "ap-southeast-2" "ca-central-1" "eu-central-1" "eu-north-1" "eu-west-1" "eu-west-2" "eu-west-3" "sa-east-1" "us-east-1" "us-east-2" "us-west-1" "us-west-2" ) 7 | LAYER_NAME_BASE="thundra-lambda-python-layer" 8 | LAYER_NAME="$LAYER_NAME_BASE" 9 | 10 | if [[ ! -z "$LAYER_NAME_SUFFIX" ]]; then 11 | LAYER_NAME="$LAYER_NAME_BASE-$LAYER_NAME_SUFFIX" 12 | fi 13 | 14 | STATEMENT_ID_BASE="$LAYER_NAME_BASE-$(($(date +%s)))" 15 | 16 | for REGION in "${REGIONS[@]}" 17 | do 18 | echo "Releasing '$LAYER_NAME' layer for region $REGION ..." 19 | 20 | ARTIFACT_BUCKET=$BUCKET_PREFIX-$REGION 21 | ARTIFACT_OBJECT=layers/python/thundra-agent-lambda-layer-$VERSION.zip 22 | 23 | echo "Publishing '$LAYER_NAME' layer from artifact $ARTIFACT_OBJECT" \ 24 | " at bucket $ARTIFACT_BUCKET ..." 25 | 26 | PUBLISHED_LAYER_VERSION=$(aws lambda publish-layer-version \ 27 | --layer-name $LAYER_NAME \ 28 | --content S3Bucket=$ARTIFACT_BUCKET,S3Key=$ARTIFACT_OBJECT \ 29 | --compatible-runtimes python2.7 python3.6 python3.7 python3.8 python3.9 \ 30 | --region $REGION \ 31 | --query 'Version') 32 | 33 | echo $PUBLISHED_LAYER_VERSION 34 | 35 | echo "Published '$LAYER_NAME' layer with version $PUBLISHED_LAYER_VERSION" 36 | 37 | # ################################################################################################################# 38 | 39 | echo "Adding layer permission for '$LAYER_NAME' layer with version $PUBLISHED_LAYER_VERSION" \ 40 | " to make it accessible by everyone ..." 41 | 42 | STATEMENT_ID="$STATEMENT_ID_BASE-$REGION" 43 | aws lambda add-layer-version-permission \ 44 | --layer-name $LAYER_NAME \ 45 | --version-number $PUBLISHED_LAYER_VERSION \ 46 | --statement-id "$LAYER_NAME-$STATEMENT_ID" \ 47 | --action lambda:GetLayerVersion \ 48 | --principal '*' \ 49 | --region $REGION \ 50 | 51 | echo "Added layer permission for '$LAYER_NAME' layer" 52 | 53 | done 54 | -------------------------------------------------------------------------------- /layer/setup.cfg: -------------------------------------------------------------------------------- 1 | [install] 2 | prefix= 3 | 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | def read(rel_path): 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | with codecs.open(os.path.join(here, rel_path), 'r') as fp: 10 | return fp.read() 11 | 12 | 13 | def get_version(rel_path): 14 | for line in read(rel_path).splitlines(): 15 | if line.startswith('__version__'): 16 | delim = '"' if '"' in line else "'" 17 | return line.split(delim)[1] 18 | else: 19 | raise RuntimeError("Unable to find version string.") 20 | 21 | 22 | setup(name='thundra', 23 | version=get_version('thundra/_version.py'), 24 | description='Thundra Python agent', 25 | long_description='Thundra Python agent', 26 | url='https://github.com/thundra-agent-python', 27 | author='Thundra', 28 | author_email='python@thundra.io', 29 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*', 30 | packages=find_packages(exclude=('tests', 'tests.*',)), 31 | install_requires=['requests>=2.16.0', 'opentracing>=2.0', 'wrapt>=1.10.11', 'simplejson', 'enum-compat', 32 | 'jsonpickle==1.3', 'websocket-client', 'python-dateutil', 'GitPython>=3.1.18', 'fastcounter>=1.1.0', 'pympler'], 33 | zip_safe=True, 34 | entry_points={"pytest11": ["thundra-foresight = foresight.pytest_integration.plugin"]}, 35 | classifiers=[ 36 | "Development Status :: 5 - Production/Stable", 37 | "Intended Audience :: Developers", 38 | "License :: OSI Approved :: Apache Software License", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 2", 41 | "Programming Language :: Python :: 2.7", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.6", 44 | "Programming Language :: Python :: 3.7", 45 | "Programming Language :: Python :: 3.8", 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/__init__.py -------------------------------------------------------------------------------- /tests/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/config/__init__.py -------------------------------------------------------------------------------- /tests/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/integrations/__init__.py -------------------------------------------------------------------------------- /tests/integrations/conftest.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | 4 | from thundra.context.execution_context_manager import ExecutionContextManager 5 | from thundra.opentracing.tracer import ThundraTracer 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def start_root_span(): 10 | tracer = ThundraTracer.get_instance() 11 | execution_context = ExecutionContextManager.get() 12 | tracer.start_active_span(operation_name="test", 13 | finish_on_close=False, 14 | trace_id="test-trace-id", 15 | transaction_id="test-transaction-id", 16 | execution_context=execution_context) 17 | 18 | 19 | def mock_tracer_get_call(self): 20 | return True 21 | 22 | 23 | @pytest.fixture(scope="module", autouse=True) 24 | def mock_get_active_span(): 25 | with mock.patch('thundra.opentracing.tracer.ThundraTracer.get_active_span', mock_tracer_get_call): 26 | yield 27 | -------------------------------------------------------------------------------- /tests/listeners/conftest.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | 4 | class MockSubsegment(object): 5 | def __init__(self): 6 | self.annotations = {} 7 | self.metadata = {} 8 | 9 | def clear_annotations(self): 10 | self.annotations.clear() 11 | 12 | def put_annotation(self, key, value): 13 | self.annotations[key] = value 14 | 15 | def put_metadata(self, key, value): 16 | self.metadata[key] = value 17 | 18 | @pytest.fixture() 19 | def mocked_subsegment(): 20 | return MockSubsegment() 21 | 22 | @pytest.fixture() 23 | def mocked_span(): 24 | return mock.Mock(name='mocked_span') 25 | 26 | @pytest.fixture() 27 | def mocked_listener(): 28 | return mock.Mock(name='mocked_listener') 29 | -------------------------------------------------------------------------------- /tests/listeners/test_latency_injector_span_listener.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from thundra.listeners import LatencyInjectorSpanListener 3 | from thundra.opentracing.tracer import ThundraTracer 4 | 5 | @mock.patch('time.sleep') 6 | def test_delay_amount(mocked_time): 7 | try: 8 | tracer = ThundraTracer.get_instance() 9 | with tracer.start_active_span(operation_name='foo', finish_on_close=True) as scope: 10 | span = scope.span 11 | delay = 370 12 | lsl = LatencyInjectorSpanListener(delay=delay) 13 | 14 | lsl.on_span_started(span) 15 | 16 | called_delay = (mocked_time.call_args[0][0]) * 1000 17 | 18 | assert delay == called_delay 19 | assert lsl.delay == delay 20 | assert lsl.distribution == 'uniform' 21 | assert lsl.variation == 0 22 | except Exception: 23 | raise 24 | finally: 25 | tracer.clear() 26 | 27 | @mock.patch('time.sleep') 28 | def test_delay_variaton(mocked_time): 29 | try: 30 | tracer = ThundraTracer.get_instance() 31 | with tracer.start_active_span(operation_name='foo', finish_on_close=True) as scope: 32 | span = scope.span 33 | delay = 100 34 | variation = 50 35 | lsl = LatencyInjectorSpanListener(delay=delay, variation=variation) 36 | 37 | lsl.on_span_started(span) 38 | 39 | called_delay = (mocked_time.call_args[0][0]) * 1000 40 | 41 | assert called_delay <= delay+variation and called_delay >= delay-variation 42 | assert lsl.delay == delay 43 | assert lsl.variation == variation 44 | assert lsl.distribution == 'uniform' 45 | except Exception: 46 | raise 47 | finally: 48 | tracer.clear() 49 | 50 | def test_create_from_config(): 51 | config = { 52 | 'delay': '370', 53 | 'sigma': '73', 54 | 'distribution': 'normal', 55 | 'variation': '37', 56 | } 57 | 58 | lsl = LatencyInjectorSpanListener.from_config(config) 59 | 60 | assert lsl.delay == 370 61 | assert lsl.sigma == 73 62 | assert lsl.variation == 37 63 | assert lsl.distribution == 'normal' 64 | 65 | def test_create_from_config_with_type_errors(): 66 | config = { 67 | 'delay': 'foo', 68 | 'sigma': 'bar', 69 | 'distribution': 12, 70 | 'variation': 'foobar', 71 | } 72 | 73 | lsl = LatencyInjectorSpanListener.from_config(config) 74 | 75 | assert lsl.delay == 0 76 | assert lsl.sigma == 0 77 | assert lsl.variation == 0 78 | assert lsl.distribution == 'uniform' 79 | -------------------------------------------------------------------------------- /tests/opentracing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/opentracing/__init__.py -------------------------------------------------------------------------------- /tests/opentracing/test_span.py: -------------------------------------------------------------------------------- 1 | import time 2 | import mock 3 | from thundra.opentracing.tracer import ThundraTracer 4 | 5 | 6 | def test_set_operation_name(): 7 | tracer = ThundraTracer.get_instance() 8 | with tracer.start_active_span(operation_name='operation name', finish_on_close=True) as scope: 9 | span = scope.span 10 | assert span.operation_name == 'operation name' 11 | 12 | span.set_operation_name('second operation name') 13 | assert span.operation_name == 'second operation name' 14 | 15 | 16 | def test_tag(): 17 | tracer = ThundraTracer.get_instance() 18 | with tracer.start_active_span(operation_name='operation name', finish_on_close=True) as scope: 19 | span = scope.span 20 | assert bool(span.tags) == False 21 | 22 | span.set_tag('tag', 'test') 23 | tag = span.get_tag('tag') 24 | assert tag == 'test' 25 | 26 | 27 | @mock.patch('thundra.opentracing.recorder.ThundraRecorder') 28 | def test_finish(mock_recorder): 29 | tracer = ThundraTracer.get_instance() 30 | with tracer.start_active_span(operation_name='operation name', finish_on_close=True) as scope: 31 | span = scope.span 32 | assert span.finish_time == 0 33 | 34 | end_time = time.time() 35 | span.finish(f_time=end_time) 36 | 37 | duration = end_time - span.start_time 38 | assert span.get_duration() == duration 39 | 40 | mock_recorder.record.assert_called_once 41 | 42 | 43 | def test_log_kv(): 44 | tracer = ThundraTracer.get_instance() 45 | with tracer.start_active_span(operation_name='operation name', finish_on_close=True) as scope: 46 | span = scope.span 47 | assert len(span.logs) == 0 48 | 49 | t = time.time() 50 | span.log_kv({ 51 | 'log1': 'log', 52 | 'log2': 2, 53 | }, t) 54 | span.finish() 55 | 56 | assert len(span.logs) == 1 57 | log = span.logs[0] 58 | assert log['timestamp'] == t 59 | 60 | assert log['log1'] == 'log' 61 | assert log['log2'] == 2 62 | 63 | 64 | def test_baggage_item(): 65 | tracer = ThundraTracer.get_instance() 66 | with tracer.start_active_span(operation_name='operation name', finish_on_close=True) as scope: 67 | span = scope.span 68 | assert bool(span.context.baggage) == False 69 | 70 | span.set_baggage_item('baggage', 'item') 71 | assert span.get_baggage_item('baggage') == 'item' 72 | span.finish() 73 | -------------------------------------------------------------------------------- /tests/opentracing/test_tracer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import mock 3 | import pytest 4 | from thundra.opentracing.tracer import ThundraTracer 5 | 6 | 7 | @pytest.fixture 8 | def span(): 9 | tracer = ThundraTracer.get_instance() 10 | scope = tracer.start_active_span(operation_name='operation name') 11 | return scope.span 12 | 13 | 14 | @mock.patch('thundra.opentracing.recorder.ThundraRecorder') 15 | @mock.patch('opentracing.scope_managers.ThreadLocalScopeManager') 16 | def test_start_active_span(mock_recorder, mock_scope_manager, span): 17 | tracer = ThundraTracer.get_instance() 18 | start_time = time.time() 19 | with tracer.start_active_span(operation_name='test', child_of=span, start_time=start_time) as active_scope: 20 | active_span = active_scope.span 21 | 22 | assert active_span.operation_name == 'test' 23 | assert active_span.start_time == start_time 24 | assert active_span.context.parent_span_id == span.context.span_id 25 | assert active_span.context.trace_id == span.context.trace_id 26 | 27 | mock_scope_manager.activate.assert_called_once 28 | mock_recorder.record.assert_called_once 29 | 30 | 31 | @mock.patch('thundra.opentracing.recorder.ThundraRecorder') 32 | def test_start_span(mock_recorder, span): 33 | tracer = ThundraTracer.get_instance() 34 | start_time = time.time() 35 | with tracer.start_span(operation_name='test', child_of=span, start_time=start_time) as active_span: 36 | assert active_span.operation_name == 'test' 37 | assert active_span.start_time == start_time 38 | assert active_span.context.parent_span_id == span.context.span_id 39 | assert active_span.context.trace_id == span.context.trace_id 40 | 41 | mock_recorder.record.assert_called_once 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/plugins/__init__.py -------------------------------------------------------------------------------- /tests/plugins/invocation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/plugins/invocation/__init__.py -------------------------------------------------------------------------------- /tests/plugins/invocation/test_invocation_support.py: -------------------------------------------------------------------------------- 1 | import thundra.plugins.invocation.invocation_support as invocation_support 2 | 3 | 4 | def test_set_get_tag(): 5 | (key, value) = ('test_key', 'test_value') 6 | invocation_support.set_tag(key, value) 7 | 8 | assert invocation_support.get_tag(key) == value 9 | 10 | 11 | def test_set_get_agent_tag(): 12 | (key, value) = ('test_key', 'test_value') 13 | invocation_support.set_agent_tag(key, value) 14 | 15 | assert invocation_support.get_agent_tag(key) == value 16 | 17 | 18 | def test_remove_tag(): 19 | (key, value) = ('test_key', 'test_value') 20 | invocation_support.set_tag(key, value) 21 | invocation_support.remove_tag(key) 22 | assert invocation_support.get_tag(key) is None 23 | 24 | 25 | def test_remove_agent_tag(): 26 | (key, value) = ('test_key', 'test_value') 27 | invocation_support.set_agent_tag(key, value) 28 | invocation_support.remove_agent_tag(key) 29 | assert invocation_support.get_agent_tag(key) is None 30 | 31 | 32 | def test_clear_tags(): 33 | pairs = { 34 | 'test_key_1': 'test_value_1', 35 | 'test_key_2': 'test_value_2', 36 | 'test_key_3': 'test_value_3', 37 | } 38 | invocation_support.set_many(pairs) 39 | invocation_support.clear() 40 | 41 | assert all([invocation_support.get_tag(key) is None for key in pairs]) 42 | 43 | 44 | def test_clear_agent_tags(): 45 | pairs = { 46 | 'test_key_1': 'test_value_1', 47 | 'test_key_2': 'test_value_2', 48 | 'test_key_3': 'test_value_3', 49 | } 50 | invocation_support.set_many_agent(pairs) 51 | invocation_support.clear() 52 | 53 | assert all([invocation_support.get_agent_tag(key) is None for key in pairs]) 54 | 55 | 56 | def test_set_error(): 57 | e = Exception("test exception") 58 | invocation_support.set_error(e) 59 | 60 | assert invocation_support.get_error() == e 61 | -------------------------------------------------------------------------------- /tests/plugins/log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/plugins/log/__init__.py -------------------------------------------------------------------------------- /tests/plugins/log/test_log_config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=thundraHandler 6 | 7 | [formatters] 8 | keys= 9 | 10 | [logger_root] 11 | level=NOTSET 12 | handlers=thundraHandler 13 | 14 | [handler_thundraHandler] 15 | class=ThundraLogHandler 16 | level=NOTSET 17 | formatter= 18 | args=() 19 | 20 | -------------------------------------------------------------------------------- /tests/plugins/log/test_log_plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from thundra.context.execution_context_manager import ExecutionContextManager 5 | from thundra.plugins.log.thundra_log_handler import ThundraLogHandler 6 | 7 | 8 | def test_when_thundra_log_handler_is_not_added_to_logger(handler, mock_context, mock_event): 9 | _, handler = handler 10 | 11 | handler(mock_event, mock_context) 12 | execution_context = ExecutionContextManager.get() 13 | assert len(execution_context.logs) == 0 14 | 15 | 16 | def test_log_plugin_with_initialization(): 17 | logger = logging.getLogger('test_handler') 18 | log_handler = ThundraLogHandler() 19 | logger.addHandler(log_handler) 20 | logger.setLevel(logging.INFO) 21 | execution_context = ExecutionContextManager.get() 22 | execution_context.capture_log = True 23 | logger.info("This is an info log") 24 | 25 | try: 26 | assert len(execution_context.logs) == 1 27 | log = execution_context.logs[0] 28 | 29 | assert log['logMessage'] == "This is an info log" 30 | assert log['logContextName'] == 'test_handler' 31 | assert log['logLevel'] == "INFO" 32 | assert log['logLevelCode'] == 2 33 | finally: 34 | del execution_context.logs[:] 35 | logger.removeHandler(log_handler) 36 | 37 | 38 | def test_log_plugin_with_config_file(): 39 | # config file path. Make sure path is correct with respect to where test is invoked 40 | fileConfig('tests/plugins/log/test_log_config.ini') 41 | logger = logging.getLogger('test_config_handler') 42 | execution_context = ExecutionContextManager.get() 43 | execution_context.capture_log = True 44 | logger.debug("This is a debug log") 45 | 46 | assert len(execution_context.logs) == 1 47 | log = execution_context.logs[0] 48 | 49 | assert log['logMessage'] == "This is a debug log" 50 | assert log['logContextName'] == 'test_config_handler' 51 | assert log['logLevel'] == "DEBUG" 52 | assert log['logLevelCode'] == 1 53 | -------------------------------------------------------------------------------- /tests/plugins/patch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/plugins/patch/__init__.py -------------------------------------------------------------------------------- /tests/plugins/patch/test_patcher.py: -------------------------------------------------------------------------------- 1 | from thundra.config.config_provider import ConfigProvider 2 | from thundra.config import config_names 3 | 4 | target_function_prefix = "get" 5 | target_module_name = "...testdemo.movie_service.MovieService" 6 | target_trace_arguments = "[trace_args=True]" 7 | target_trace_arguments_list = ['trace_args'] 8 | 9 | from thundra.compat import PY2 10 | 11 | if not PY2: 12 | from thundra.plugins.trace.patcher import ImportPatcher 13 | import pytest 14 | 15 | @pytest.mark.skip(reason="This functionality not in use!") 16 | def test_retrieving_function_prefix(): 17 | ConfigProvider.set(config_names.THUNDRA_TRACE_INSTRUMENT_TRACEABLECONFIG, \ 18 | "{}.{}*{}".format(target_module_name ,target_function_prefix, target_trace_arguments)) 19 | patcher = ImportPatcher() 20 | 21 | assert patcher.get_module_function_prefix(target_module_name) == target_function_prefix 22 | 23 | 24 | @pytest.mark.skip(reason="This functionality not in use!") 25 | def test_retrieving_trace_args(): 26 | ConfigProvider.set(config_names.THUNDRA_TRACE_INSTRUMENT_TRACEABLECONFIG, \ 27 | "{}.{}*{}".format(target_module_name ,target_function_prefix, target_trace_arguments)) 28 | patcher = ImportPatcher() 29 | for arg in patcher.get_trace_arguments(target_module_name): 30 | assert arg in target_trace_arguments_list 31 | -------------------------------------------------------------------------------- /tests/plugins/trace/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/plugins/trace/__init__.py -------------------------------------------------------------------------------- /tests/plugins/trace/test_trace_aware_wrapper.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.pool import ThreadPool 2 | from threading import Thread 3 | from thundra.opentracing.tracer import ThundraTracer 4 | from thundra.plugins.trace.trace_aware_wrapper import TraceAwareWrapper 5 | from thundra.plugins.trace.traceable import Traceable 6 | 7 | 8 | def test_via_threadpool(): 9 | numbers = [1, 2, 3, 4, 5] 10 | squared_numbers = calculate_parallel(numbers, 4) 11 | expected_result = [1, 4, 9, 16, 25] 12 | 13 | assert squared_numbers == expected_result 14 | 15 | tracer = ThundraTracer.get_instance() 16 | nodes = tracer.get_spans() 17 | active_span = None 18 | for key in nodes: 19 | if key.operation_name == 'calculate_parallel': 20 | active_span = key 21 | 22 | args = active_span.get_tag('method.args') 23 | assert args[0]['name'] == 'arg-0' 24 | assert args[0]['value'] == numbers 25 | assert args[0]['type'] == 'list' 26 | assert args[1]['name'] == 'arg-1' 27 | assert args[1]['value'] == 4 28 | assert args[1]['type'] == 'int' 29 | 30 | return_value = active_span.get_tag('method.return_value') 31 | assert return_value['value'] == squared_numbers 32 | assert return_value['type'] == 'list' 33 | 34 | error = active_span.get_tag('thrownError') 35 | assert error is None 36 | 37 | 38 | def test_via_threading(): 39 | numbers = [1, 2, 3, 4, 5] 40 | expected_result = [1, 4, 9, 16, 25] 41 | 42 | wrapper = TraceAwareWrapper() 43 | thread = Thread(target=wrapper(calculate_in_parallel), args=(numbers,)) 44 | thread.start() 45 | thread.join() 46 | 47 | tracer = ThundraTracer.get_instance() 48 | nodes = tracer.get_spans() 49 | active_span = None 50 | for key in nodes: 51 | if key.operation_name == 'calculate_in_parallel': 52 | active_span = key 53 | 54 | assert active_span is not None 55 | args = active_span.get_tag('method.args') 56 | assert args[0]['name'] == 'arg-0' 57 | assert args[0]['value'] == numbers 58 | assert args[0]['type'] == 'list' 59 | 60 | return_value = active_span.get_tag('method.return_value') 61 | assert return_value['value'] == expected_result 62 | assert return_value['type'] == 'list' 63 | 64 | error = active_span.get_tag('error') 65 | assert error is None 66 | 67 | 68 | def square_number(n): 69 | return n ** 2 70 | 71 | 72 | @Traceable(trace_args=True, trace_return_value=True) 73 | def calculate_in_parallel(numbers): 74 | result = [] 75 | for number in numbers: 76 | result.append(square_number(number)) 77 | return result 78 | 79 | 80 | @Traceable(trace_args=True, trace_return_value=True) 81 | def calculate_parallel(numbers, threads=2): 82 | pool = ThreadPool(threads) 83 | wrapper = TraceAwareWrapper() 84 | result = pool.map(wrapper(square_number), numbers) 85 | pool.close() 86 | pool.join() 87 | return result 88 | -------------------------------------------------------------------------------- /tests/plugins/trace/test_trace_plugin.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context_manager import ExecutionContextManager 2 | 3 | 4 | def test_invocation_support_error_set_to_root_span(handler_with_user_error, mock_context, mock_event): 5 | thundra, handler = handler_with_user_error 6 | 7 | handler(mock_event, mock_context) 8 | execution_context = ExecutionContextManager.get() 9 | 10 | assert execution_context.root_span.get_tag('error') is True 11 | assert execution_context.root_span.get_tag('error.kind') == 'Exception' 12 | assert execution_context.root_span.get_tag('error.message') == 'test' 13 | -------------------------------------------------------------------------------- /tests/samplers/test_composite_sampler.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | from thundra.samplers import CompositeSampler 4 | 5 | 6 | @pytest.fixture 7 | def mocked_sampler(): 8 | def sampler(is_sampled=False): 9 | m = mock.Mock(name='mocked_sampler') 10 | m.is_sampled.return_value = is_sampled 11 | 12 | return m 13 | 14 | return sampler 15 | 16 | 17 | def test_with_no_samplers(): 18 | cms = CompositeSampler() 19 | 20 | assert not cms.is_sampled() 21 | 22 | 23 | def test_and_operator(mocked_sampler): 24 | s1 = mocked_sampler(is_sampled=False) 25 | s2 = mocked_sampler(is_sampled=True) 26 | s3 = mocked_sampler(is_sampled=True) 27 | 28 | cms1 = CompositeSampler(samplers=[s2, s3], operator='and') 29 | cms2 = CompositeSampler(samplers=[s1, s2, s3], operator='and') 30 | 31 | assert cms1.is_sampled() 32 | assert not cms2.is_sampled() 33 | 34 | 35 | def test_or_operator(mocked_sampler): 36 | s1 = mocked_sampler(is_sampled=True) 37 | s2 = mocked_sampler(is_sampled=False) 38 | s3 = mocked_sampler(is_sampled=False) 39 | 40 | cms1 = CompositeSampler(samplers=[s2, s3], operator='or') 41 | cms2 = CompositeSampler(samplers=[s1, s2, s3], operator='or') 42 | 43 | assert not cms1.is_sampled() 44 | assert cms2.is_sampled() 45 | 46 | 47 | def test_with_unknown_operator(mocked_sampler): 48 | s1 = mocked_sampler(is_sampled=True) 49 | s2 = mocked_sampler(is_sampled=False) 50 | s3 = mocked_sampler(is_sampled=False) 51 | 52 | cms1 = CompositeSampler(samplers=[s2, s3], operator='foo') 53 | cms2 = CompositeSampler(samplers=[s1, s2, s3], operator='foo') 54 | 55 | assert not cms1.is_sampled() 56 | assert cms2.is_sampled() 57 | -------------------------------------------------------------------------------- /tests/samplers/test_count_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.samplers import CountAwareSampler 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.config import config_names 5 | 6 | def test_default_count_freq(): 7 | cams = CountAwareSampler() 8 | 9 | assert cams.count_freq == constants.DEFAULT_METRIC_SAMPLING_COUNT_FREQ 10 | 11 | 12 | def test_freq_from_env(): 13 | count_freq = 37 14 | ConfigProvider.set(config_names.THUNDRA_SAMPLER_COUNTAWARE_COUNTFREQ, '{}'.format(count_freq)) 15 | 16 | cams = CountAwareSampler() 17 | 18 | assert cams.count_freq == count_freq 19 | 20 | 21 | def test_count_freq(): 22 | cams = CountAwareSampler(count_freq=10) 23 | count_freq = 10 24 | invocation_count = 100 25 | expected = invocation_count // count_freq 26 | 27 | res = 0 28 | for i in range(invocation_count): 29 | if cams.is_sampled(): 30 | res += 1 31 | 32 | assert res == expected 33 | 34 | 35 | def test_first_invocation_sampled(): 36 | cams = CountAwareSampler(count_freq=10) 37 | 38 | assert cams.is_sampled() 39 | -------------------------------------------------------------------------------- /tests/samplers/test_time_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import mock 3 | from thundra import constants 4 | from thundra.samplers import TimeAwareSampler 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.config import config_names 7 | 8 | 9 | def test_default_time_freq(): 10 | tams = TimeAwareSampler() 11 | 12 | assert tams.time_freq == constants.DEFAULT_METRIC_SAMPLING_TIME_FREQ 13 | 14 | 15 | def test_freq_from_env(): 16 | time_freq = 37 17 | ConfigProvider.set(config_names.THUNDRA_SAMPLER_TIMEAWARE_TIMEFREQ, '{}'.format(time_freq)) 18 | tams = TimeAwareSampler() 19 | 20 | assert tams.time_freq == time_freq 21 | 22 | 23 | @mock.patch('time.time') 24 | def test_time_freq(mocked_time): 25 | tams = TimeAwareSampler() 26 | 27 | cases = [ 28 | { 29 | 'lt': 0, # Latest time 30 | 'ct': 30, # Current time 31 | 'f': 37, # Frequency 32 | 'e': False, # Expexted 33 | }, 34 | { 35 | 'lt': 0, 36 | 'ct': 38, 37 | 'f': 37, 38 | 'e': True, 39 | }, 40 | ] 41 | 42 | for case in cases: 43 | tams.time_freq = case['f'] 44 | tams._latest_time = case['lt'] 45 | mocked_time.return_value = case['ct'] / 1000 46 | 47 | assert tams.is_sampled() == case['e'] 48 | -------------------------------------------------------------------------------- /tests/test_application_support.py: -------------------------------------------------------------------------------- 1 | from thundra.application.application_info_provider import ApplicationInfoProvider 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | 5 | 6 | def test_if_can_get_integer_tag(): 7 | tag_name = 'integerField' 8 | (env_key, env_val) = (config_names.THUNDRA_APPLICATION_TAG_PREFIX + '.' + tag_name, 3773) 9 | ConfigProvider.set(env_key, str(env_val)) 10 | 11 | application_tags = ApplicationInfoProvider.parse_application_tags() 12 | 13 | assert application_tags[tag_name] == env_val 14 | 15 | 16 | def test_if_can_get_float_tag(): 17 | tag_name = 'floatField' 18 | (env_key, env_val) = (config_names.THUNDRA_APPLICATION_TAG_PREFIX + '.' + tag_name, 12.3221) 19 | ConfigProvider.set(env_key, str(env_val)) 20 | 21 | application_tags = ApplicationInfoProvider.parse_application_tags() 22 | 23 | assert application_tags[tag_name] == env_val 24 | 25 | 26 | def test_if_can_get_string_tag(): 27 | tag_name = 'stringField' 28 | (env_key, env_val) = (config_names.THUNDRA_APPLICATION_TAG_PREFIX + '.' + tag_name, 'fooBar') 29 | ConfigProvider.set(env_key, str(env_val)) 30 | 31 | application_tags = ApplicationInfoProvider.parse_application_tags() 32 | 33 | assert application_tags[tag_name] == env_val 34 | 35 | 36 | def test_if_can_get_bool_tag(): 37 | tag_name = 'boolField' 38 | (env_key, env_val) = (config_names.THUNDRA_APPLICATION_TAG_PREFIX + '.' + tag_name, True) 39 | ConfigProvider.set(env_key, str(env_val)) 40 | 41 | application_tags = ApplicationInfoProvider.parse_application_tags() 42 | 43 | assert application_tags[tag_name] == env_val 44 | -------------------------------------------------------------------------------- /tests/test_reporter.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import json 3 | 4 | from thundra import constants 5 | from thundra.config import config_names 6 | from thundra.config.config_provider import ConfigProvider 7 | from thundra.reporter import Reporter 8 | from thundra.encoder import to_json 9 | 10 | 11 | @mock.patch('thundra.reporter.requests') 12 | def test_send_report_to_url(mock_requests, mock_report): 13 | ConfigProvider.set(config_names.THUNDRA_REPORT_REST_BASEURL, 'different_url/api') 14 | ConfigProvider.set(config_names.THUNDRA_REPORT_REST_COMPOSITE_ENABLE, 'false') 15 | test_session = mock_requests.Session() 16 | reporter = Reporter('api key', session=test_session) 17 | responses = reporter.send_reports([mock_report]) 18 | 19 | post_url = 'different_url/api/monitoring-data' 20 | headers = { 21 | 'Content-Type': 'application/json', 22 | 'Authorization': 'ApiKey api key' 23 | } 24 | 25 | reporter.session.post.assert_called_once_with(post_url, data=to_json([mock_report], separators=(',', ':')), 26 | headers=headers, timeout=constants.DEFAULT_REPORT_TIMEOUT) 27 | reporter.session.post.return_value.status_code = 200 28 | for response in responses: 29 | assert response.status_code == 200 30 | 31 | 32 | @mock.patch('thundra.reporter.requests') 33 | def test_send_report(mock_requests, mock_invocation_report): 34 | test_session = mock_requests.Session() 35 | reporter = Reporter('unauthorized api key', session=test_session) 36 | responses = reporter.send_reports([mock_invocation_report]) 37 | 38 | assert reporter.session.post.call_count == 1 39 | test_session.post.return_value.status_code = 401 40 | for response in responses: 41 | assert response.status_code == 401 42 | 43 | 44 | def test_get_report_batches(mock_report): 45 | ConfigProvider.set(config_names.THUNDRA_REPORT_REST_COMPOSITE_BATCH_SIZE, '2') 46 | 47 | reporter = Reporter('api key') 48 | batches = reporter.get_report_batches([mock_report] * 3) 49 | 50 | assert len(batches) == 2 51 | assert batches[0] == [mock_report, mock_report] 52 | assert batches[1] == [mock_report] 53 | 54 | 55 | def test_prepare_report_json(mock_report, mock_report_with_byte_field): 56 | reporter = Reporter('api key') 57 | 58 | reports = reporter.prepare_report_json([mock_report, mock_report_with_byte_field]) 59 | reports = json.loads(reports[0]) 60 | 61 | assert len(reports) == 2 62 | assert reports[0].get('type') != 'bytes' 63 | assert reports[1].get('type') == 'bytes' 64 | 65 | 66 | def test_prepare_report_json_batch(mock_report): 67 | ConfigProvider.set(config_names.THUNDRA_REPORT_REST_COMPOSITE_BATCH_SIZE, '1') 68 | 69 | reporter = Reporter('api key') 70 | 71 | batched_reports = reporter.prepare_report_json([mock_report] * 2) 72 | assert len(batched_reports) == 2 73 | 74 | reports = json.loads(batched_reports[0]) 75 | assert len(reports) == 1 76 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from thundra import utils, constants 3 | 4 | 5 | def test_get_default_timeout_margin(monkeypatch): 6 | monkeypatch.setitem(os.environ, constants.AWS_REGION, 'us-west-2') 7 | timeout_margin = utils.get_default_timeout_margin() 8 | assert timeout_margin == 200 9 | 10 | monkeypatch.setitem(os.environ, constants.AWS_REGION, 'us-east-2') 11 | 12 | timeout_margin = utils.get_default_timeout_margin() 13 | assert timeout_margin == 600 14 | 15 | monkeypatch.setitem(os.environ, constants.AWS_REGION, 'eu-west-1') 16 | 17 | timeout_margin = utils.get_default_timeout_margin() 18 | assert timeout_margin == 1000 19 | 20 | monkeypatch.setitem(os.environ, constants.AWS_REGION, 'eu-west-1') 21 | monkeypatch.setitem(os.environ, constants.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, '1536') 22 | 23 | timeout_margin = utils.get_default_timeout_margin() 24 | assert timeout_margin == 1000 25 | 26 | monkeypatch.setitem(os.environ, constants.AWS_REGION, 'eu-west-1') 27 | monkeypatch.setitem(os.environ, constants.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, '128') 28 | 29 | timeout_margin = utils.get_default_timeout_margin() 30 | assert timeout_margin == 3000 31 | 32 | 33 | def test_get_nearest_collector(monkeypatch): 34 | regions = [ 'us-west-2', 'us-west-1', 'us-east-2', 'us-east-1', 'ca-central-1', 'sa-east-1', 35 | 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-south-1', 36 | 'ap-south-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 37 | 'ap-east-1', 'af-south-1', 'me-south-1'] 38 | 39 | for region in regions: 40 | monkeypatch.setitem(os.environ, constants.AWS_REGION, region) 41 | collector = utils.get_nearest_collector() 42 | assert collector == "{}.collector.thundra.io".format(region) 43 | 44 | monkeypatch.delitem(os.environ, constants.AWS_REGION) 45 | collector = utils.get_nearest_collector() 46 | assert collector == "collector.thundra.io" 47 | -------------------------------------------------------------------------------- /tests/testdemo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/testdemo/__init__.py -------------------------------------------------------------------------------- /tests/testdemo/movie/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/testdemo/movie/__init__.py -------------------------------------------------------------------------------- /tests/testdemo/movie/movie.py: -------------------------------------------------------------------------------- 1 | class Movie: 2 | def __init__(self, name='', director='', year=0): 3 | self.name = name 4 | self.director = director 5 | self.year = year 6 | 7 | -------------------------------------------------------------------------------- /tests/testdemo/movie/movie_repository.py: -------------------------------------------------------------------------------- 1 | from .movie import Movie 2 | 3 | # throw exceptions whose names are starts with "Demo" if you do not want to be notified 4 | class DemoException(Exception): 5 | pass 6 | 7 | class MovieRepository: 8 | 9 | def __init__(self): 10 | self.movies = { 11 | 0: Movie("The Shawshank Redemption ", "Frank Darabont", 1994), 12 | 1: Movie("The Godfather", "Francis Ford Coppola", 1972), 13 | 2: Movie("The Dark Knight", "Christopher Nolan", 2008), 14 | 3: Movie("The Godfather: Part II", "Francis Ford Coppola", 1974), 15 | 4: Movie("Pulp Fiction", "Quentin Tarantino", 1994), 16 | 5: Movie("Schindler's List", "Steven Spielberg", 1993), 17 | 6: Movie("The Lord of the Rings: The Return of the King", "Peter Jackson", 2003), 18 | 7: Movie("The Good, the Bad and the Ugly", "Sergio Leone", 1966), 19 | 8: Movie("12 Angry Men", "Sidney Lumet", 1957), 20 | 9: Movie("Forrest Gump", "Robert Zemeckis", 1994) 21 | } 22 | 23 | self.number_of_movies = 10 24 | 25 | def find_movie(self, id): 26 | if id >= 10: 27 | raise DemoException("No movie is found") 28 | return self.movies[id] if id in self.movies else None -------------------------------------------------------------------------------- /tests/testdemo/movie/movie_service.py: -------------------------------------------------------------------------------- 1 | from .movie_repository import MovieRepository 2 | 3 | class MovieService: 4 | 5 | def __init__(self): 6 | self.movie_repository = MovieRepository() 7 | 8 | def get_movie(self, id): 9 | return self.movie_repository.find_movie(id) -------------------------------------------------------------------------------- /tests/wrappers/django/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/tests/wrappers/django/app/__init__.py -------------------------------------------------------------------------------- /tests/wrappers/django/app/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = 'test_secret_key' 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = ['*'] 10 | 11 | INSTALLED_APPS = [ 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | ] 19 | 20 | MIDDLEWARE = [ 21 | 'django.middleware.security.SecurityMiddleware', 22 | 'django.contrib.sessions.middleware.SessionMiddleware', 23 | 'django.middleware.common.CommonMiddleware', 24 | 'django.middleware.csrf.CsrfViewMiddleware', 25 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 26 | 'django.contrib.messages.middleware.MessageMiddleware', 27 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 28 | ] 29 | 30 | ROOT_URLCONF = 'app.urls' 31 | 32 | TEMPLATES = [ 33 | { 34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 35 | 'DIRS': [], 36 | 'APP_DIRS': True, 37 | 'OPTIONS': { 38 | 'context_processors': [ 39 | 'django.template.context_processors.debug', 40 | 'django.template.context_processors.request', 41 | 'django.contrib.auth.context_processors.auth', 42 | 'django.contrib.messages.context_processors.messages', 43 | ], 44 | }, 45 | }, 46 | ] 47 | 48 | WSGI_APPLICATION = 'app.wsgi.application' 49 | 50 | DATABASES = { 51 | 'default': { 52 | 'ENGINE': 'django.db.backends.sqlite3', 53 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 54 | } 55 | } 56 | 57 | # Internationalization 58 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 59 | 60 | LANGUAGE_CODE = 'en-us' 61 | TIME_ZONE = 'UTC' 62 | USE_I18N = True 63 | USE_L10N = True 64 | USE_TZ = True 65 | STATIC_URL = '/static/' 66 | 67 | THUNDRA = { 68 | "thundra.apiKey": "test", 69 | "thundra.agent.debug.enable": True, 70 | "thundra.agent.application.name": "test", 71 | "thundra.agent.application.region": "eu-west-1", 72 | "thundra.agent.log.disable": False, 73 | "thundra.agent.log.console.disable": False 74 | } 75 | -------------------------------------------------------------------------------- /tests/wrappers/django/app/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.http import HttpResponse 3 | from django.urls import path, re_path 4 | 5 | from . import views 6 | 7 | 8 | urlpatterns = [ 9 | url(r"^$", views.index), 10 | re_path(r"re-path.*/", views.repath_view), 11 | path("error/", views.view_with_error), 12 | ] 13 | -------------------------------------------------------------------------------- /tests/wrappers/django/app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("Test!", status=200) 6 | 7 | 8 | def view_with_error(request): 9 | raise Exception("Error") 10 | 11 | 12 | def repath_view(request): 13 | return HttpResponse(status=200) 14 | -------------------------------------------------------------------------------- /tests/wrappers/django/app/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /tests/wrappers/django/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | import pytest 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def setup(): 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.wrappers.django.app.settings") 10 | django.setup() 11 | -------------------------------------------------------------------------------- /tests/wrappers/django/test_django.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import Mock 3 | 4 | from django.test import Client 5 | from django.test import RequestFactory 6 | 7 | from tests.wrappers.django.app.views import index 8 | from thundra import constants 9 | from thundra.wrappers.django.django_wrapper import DjangoWrapper 10 | 11 | c = Client() 12 | 13 | 14 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.process_exception') 15 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.after_request') 16 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.before_request') 17 | def test_django_middleware_wrapper_calls(mock_before_request, mock_after_request, mock_process_exception): 18 | response = c.get('/') 19 | 20 | assert response.status_code == 200 21 | assert response.content == b"Test!" 22 | assert mock_before_request.called 23 | assert mock_after_request.called 24 | assert not mock_process_exception.called 25 | 26 | 27 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.process_exception') 28 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.after_request') 29 | @mock.patch('thundra.wrappers.django.django_wrapper.DjangoWrapper.before_request') 30 | def test_erroneous_view(mock_before_request, mock_after_request, mock_process_exception): 31 | try: 32 | c.get('/error/') 33 | except: 34 | "Error thrown in view" 35 | 36 | assert mock_before_request.called 37 | assert mock_after_request.called 38 | assert mock_process_exception.called 39 | 40 | 41 | def test_wrapper(): 42 | wrapper = DjangoWrapper() 43 | wrapper.reporter = Mock() 44 | factory = RequestFactory() 45 | request = factory.get('/', {'bar': 'baz'}) 46 | execution_context = wrapper.before_request(request) 47 | assert execution_context.root_span.operation_name == '/' 48 | assert execution_context.root_span.get_tag('http.method') == 'GET' 49 | assert execution_context.root_span.get_tag('http.host') == 'testserver' 50 | assert execution_context.root_span.get_tag('http.query_params').get('bar') == 'baz' 51 | assert execution_context.root_span.get_tag('http.path') == '/' 52 | assert execution_context.root_span.class_name == constants.ClassNames['DJANGO'] 53 | assert execution_context.root_span.domain_name == 'API' 54 | 55 | assert execution_context.tags.get(constants.SpanTags['TRIGGER_OPERATION_NAMES']) == ['testserver/'] 56 | assert execution_context.tags.get(constants.SpanTags['TRIGGER_DOMAIN_NAME']) == 'API' 57 | assert execution_context.tags.get(constants.SpanTags['TRIGGER_CLASS_NAME']) == 'HTTP' 58 | 59 | response = index(request) 60 | 61 | wrapper.after_request(response) 62 | 63 | assert execution_context.response == response 64 | assert execution_context.invocation_data['applicationId'] == 'python:Django:eu-west-1:test' 65 | 66 | -------------------------------------------------------------------------------- /tests/wrappers/fastapi/conftest.py: -------------------------------------------------------------------------------- 1 | from main import app 2 | import pytest 3 | from starlette.testclient import TestClient 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def test_app(): 8 | client = TestClient(app) 9 | yield client # testing happens here -------------------------------------------------------------------------------- /tests/wrappers/fastapi/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, Response 2 | from fastapi.responses import JSONResponse 3 | 4 | app = FastAPI() 5 | 6 | 7 | @app.get("/{param}") 8 | def root(param: int): 9 | return JSONResponse({ "hello_world": param}) 10 | 11 | 12 | @app.get("/error") 13 | def error(): 14 | raise RuntimeError('Test Error') 15 | -------------------------------------------------------------------------------- /tests/wrappers/fastapi/test_fastapi.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.context.execution_context_manager import ExecutionContextManager 3 | from thundra.wrappers.fastapi.fastapi_wrapper import FastapiWrapper 4 | from thundra.context.tracing_execution_context_provider import TracingExecutionContextProvider 5 | from thundra.context.global_execution_context_provider import GlobalExecutionContextProvider 6 | from thundra.wrappers import wrapper_utils 7 | import pytest 8 | 9 | 10 | def test_fastapi_hooks_called(test_app, monkeypatch): 11 | 12 | def mock_before_request(self, request, req_body): 13 | ExecutionContextManager.set_provider(TracingExecutionContextProvider()) 14 | execution_context = wrapper_utils.create_execution_context() 15 | execution_context.platform_data["request"] = request 16 | execution_context.platform_data["request"]["body"] = req_body 17 | 18 | self.plugin_context.request_count += 1 19 | self.execute_hook("before:invocation", execution_context) 20 | 21 | assert execution_context.root_span.operation_name == '/1' 22 | assert execution_context.root_span.get_tag('http.method') == 'GET' 23 | assert execution_context.root_span.get_tag('http.host') == 'testserver' 24 | assert execution_context.root_span.get_tag('http.query_params') == b'' 25 | assert execution_context.root_span.get_tag('http.path') == '/1' 26 | assert execution_context.root_span.class_name == constants.ClassNames['FASTAPI'] 27 | assert execution_context.root_span.domain_name == 'API' 28 | 29 | return execution_context 30 | 31 | def mock_after_request(self, execution_context): 32 | assert execution_context.response.body == b'{"hello_world":1}' 33 | assert execution_context.response.status_code == 200 34 | self.prepare_and_send_reports_async(execution_context) 35 | ExecutionContextManager.clear() 36 | 37 | monkeypatch.setattr(FastapiWrapper, "before_request", mock_before_request) 38 | monkeypatch.setattr(FastapiWrapper, "after_request", mock_after_request) 39 | response = test_app.get('/1') 40 | 41 | def test_fastapi_errornous(test_app, monkeypatch): 42 | try: 43 | 44 | def mock_error_handler(self, error): 45 | execution_context = ExecutionContextManager.get() 46 | if error: 47 | execution_context.error = error 48 | 49 | self.prepare_and_send_reports_async(execution_context) 50 | assert error.type == "RuntimeError" 51 | assert error.message == "Test Error" 52 | 53 | monkeypatch.setattr(FastapiWrapper, "error_handler", mock_error_handler) 54 | 55 | test_app.get('/error') 56 | except: 57 | "Error thrown in endpoint" -------------------------------------------------------------------------------- /tests/wrappers/tornado/hello.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | 3 | class MainHandler(tornado.web.RequestHandler): 4 | def get(self): 5 | self.write("Hello, world") 6 | 7 | class QueryHandler(tornado.web.RequestHandler): 8 | def get(self): 9 | query_arg = self.get_query_argument("foo") 10 | self.write(query_arg) 11 | 12 | def make_app(): 13 | return tornado.web.Application([ 14 | (r"/", MainHandler), 15 | (r"/query", QueryHandler), 16 | ]) -------------------------------------------------------------------------------- /tests/wrappers/tornado/test_tornado.py: -------------------------------------------------------------------------------- 1 | from tornado.testing import AsyncHTTPTestCase 2 | from thundra.context.tracing_execution_context_provider import TracingExecutionContextProvider 3 | from thundra.context.execution_context_manager import ExecutionContextManager 4 | from thundra.wrappers import wrapper_utils 5 | from thundra.wrappers.tornado.tornado_wrapper import TornadoWrapper 6 | from thundra import constants 7 | from unittest import mock 8 | from tornado.httputil import url_concat 9 | 10 | 11 | import tests.wrappers.tornado.hello as hello 12 | 13 | 14 | class TestHelloApp(AsyncHTTPTestCase): 15 | def get_app(self): 16 | return hello.make_app() 17 | 18 | @mock.patch('thundra.wrappers.tornado.tornado_wrapper.TornadoWrapper.after_request') 19 | @mock.patch('thundra.wrappers.tornado.tornado_wrapper.TornadoWrapper.before_request') 20 | def test_homepage(self, mock_before_request, mock_after_request): 21 | response = self.fetch('/') 22 | self.assertEqual(response.code, 200) 23 | self.assertEqual(response.body, b'Hello, world') 24 | assert mock_before_request.called 25 | assert mock_after_request.called 26 | 27 | @mock.patch('thundra.wrappers.web_wrapper_utils.finish_trace') 28 | def test_successful_view(self, finish_trace): 29 | query_string={'foo': 'baz'} 30 | response = self.fetch(url_concat("/query", query_string)) 31 | execution_context = ExecutionContextManager.get() 32 | self.assertEqual(execution_context.root_span.operation_name, '/query') 33 | self.assertEqual(execution_context.root_span.get_tag('http.method'), 'GET') 34 | self.assertEqual(execution_context.root_span.get_tag('http.host'), '127.0.0.1') 35 | self.assertEqual(execution_context.root_span.get_tag('http.query_params'), 'foo=baz') 36 | self.assertEqual(execution_context.root_span.get_tag('http.path'), '/query') 37 | self.assertEqual(execution_context.root_span.class_name, constants.ClassNames['TORNADO']) 38 | self.assertEqual(execution_context.root_span.domain_name, 'API') 39 | self.assertEqual(execution_context.tags.get(constants.SpanTags['TRIGGER_OPERATION_NAMES']), ['127.0.0.1/query']) 40 | self.assertEqual(execution_context.tags.get(constants.SpanTags['TRIGGER_DOMAIN_NAME']), 'API') 41 | self.assertEqual(execution_context.tags.get(constants.SpanTags['TRIGGER_CLASS_NAME']), 'HTTP') 42 | self.assertEqual(execution_context.response.status_code, 200) 43 | self.assertEqual(response.body, b'baz') 44 | ExecutionContextManager.clear() 45 | -------------------------------------------------------------------------------- /thundra/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.0.6" -------------------------------------------------------------------------------- /thundra/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/application/__init__.py -------------------------------------------------------------------------------- /thundra/application/application_info.py: -------------------------------------------------------------------------------- 1 | class ApplicationInfo: 2 | 3 | def __init__(self, application_id = None, application_instance_id = None, application_domain_name = None, 4 | application_class_name = None, application_name = None, application_version = None, 5 | application_stage = None, application_runtime = None, 6 | application_runtime_version = None, application_tags = None): 7 | 8 | self.application_id = application_id 9 | self.application_instance_id = application_instance_id 10 | self.application_domain_name = application_domain_name 11 | self.application_class_name = application_class_name 12 | self.application_name = application_name 13 | self.application_version = application_version 14 | self.application_stage = application_stage 15 | self.application_runtime = application_runtime 16 | self.application_runtime_version = application_runtime_version 17 | self.applicationTags = application_tags 18 | 19 | def to_json(self): 20 | return { 21 | 'applicationId': self.application_id, 22 | 'applicationInstanceId': self.application_instance_id, 23 | 'applicationDomainName': self.application_domain_name, 24 | 'applicationClassName': self.application_class_name, 25 | 'applicationName': self.application_name, 26 | 'applicationVersion': self.application_version, 27 | 'applicationStage': self.application_stage, 28 | 'applicationRuntime': self.application_runtime, 29 | 'applicationRuntimeVersion': self.application_runtime_version, 30 | 'applicationTags': self.applicationTags 31 | } -------------------------------------------------------------------------------- /thundra/application/application_info_provider.py: -------------------------------------------------------------------------------- 1 | import abc, sys 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | 6 | ABC = abc.ABCMeta('ABC', (object,), {}) 7 | 8 | 9 | class ApplicationInfoProvider(ABC): 10 | 11 | APPLICATION_RUNTIME = "python" 12 | APPLICATION_RUNTIME_VERSION = str(sys.version_info[0]) 13 | 14 | @abc.abstractmethod 15 | def get_application_info(self): 16 | pass 17 | 18 | @staticmethod 19 | def parse_application_tags(): 20 | application_tags = {} 21 | prefix_length = len(config_names.THUNDRA_APPLICATION_TAG_PREFIX) + 1 22 | for key in ConfigProvider.configs: 23 | if key.startswith(config_names.THUNDRA_APPLICATION_TAG_PREFIX) and len(key) >= prefix_length: 24 | app_tag_key = key[prefix_length:] 25 | val = ConfigProvider.get(key) 26 | application_tags[app_tag_key] = val 27 | return application_tags 28 | -------------------------------------------------------------------------------- /thundra/application/global_application_info_provider.py: -------------------------------------------------------------------------------- 1 | from thundra.application.application_info_provider import ApplicationInfoProvider 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | 5 | 6 | class GlobalApplicationInfoProvider(ApplicationInfoProvider): 7 | def __init__(self, application_info_provider=None): 8 | self.application_info = {} 9 | self.application_info_provider = application_info_provider 10 | if self.application_info_provider: 11 | self.application_info = self.application_info_provider.get_application_info() 12 | 13 | app_info_from_config = self.get_application_info_from_config() 14 | 15 | self.update(app_info_from_config) 16 | 17 | def get_application_info(self): 18 | return self.application_info 19 | 20 | def get_application_tags(self): 21 | return self.application_info.get('applicationTags', {}).copy() 22 | 23 | @staticmethod 24 | def get_application_info_from_config(): 25 | return { 26 | 'applicationId': ConfigProvider.get(config_names.THUNDRA_APPLICATION_ID), 27 | 'applicationInstanceId': ConfigProvider.get(config_names.THUNDRA_APPLICATION_INSTANCE_ID), 28 | 'applicationDomainName': ConfigProvider.get(config_names.THUNDRA_APPLICATION_DOMAIN_NAME), 29 | 'applicationClassName': ConfigProvider.get(config_names.THUNDRA_APPLICATION_CLASS_NAME), 30 | 'applicationName': ConfigProvider.get(config_names.THUNDRA_APPLICATION_NAME), 31 | 'applicationVersion': ConfigProvider.get(config_names.THUNDRA_APPLICATION_VERSION, ''), 32 | 'applicationStage': ConfigProvider.get(config_names.THUNDRA_APPLICATION_STAGE, ''), 33 | 'applicationRegion': ConfigProvider.get(config_names.THUNDRA_APPLICATION_REGION, ''), 34 | 'applicationRuntime': ApplicationInfoProvider.APPLICATION_RUNTIME, 35 | 'applicationRuntimeVersion': ApplicationInfoProvider.APPLICATION_RUNTIME_VERSION, 36 | 'applicationTags': ApplicationInfoProvider.parse_application_tags() 37 | } 38 | 39 | def update(self, opts): 40 | filtered_opts = {k: v for k, v in opts.items() if v is not None} 41 | self.application_info.update(filtered_opts) 42 | -------------------------------------------------------------------------------- /thundra/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | _ver = sys.version_info 4 | 5 | # Python 2.x 6 | PY2 = (_ver[0] == 2) 7 | 8 | # Python 3.x 9 | PY3 = (_ver[0] == 3) 10 | 11 | # Python 2.7.x 12 | PY27 = (PY2 and _ver[1] == 7) 13 | 14 | # Python 3.6.x 15 | PY36 = (PY3 and _ver[1] == 6) 16 | 17 | # Python 3.7.x 18 | PY37 = (PY3 and _ver[1] == 7) 19 | 20 | # Python 3.8.x 21 | PY38 = (PY3 and _ver[1] == 8) 22 | 23 | class TimeoutError(Exception): 24 | def __init__(self, msg="Task timed out"): 25 | super(TimeoutError, self).__init__(msg) 26 | 27 | if PY2: 28 | from urlparse import urlparse 29 | 30 | builtin_str = str 31 | bytes = str 32 | str = unicode 33 | basestring = basestring 34 | 35 | import Queue 36 | queue = Queue 37 | 38 | 39 | elif PY3: 40 | from urllib.parse import urlparse 41 | 42 | builtin_str = str 43 | str = str 44 | bytes = bytes 45 | basestring = (str, bytes) 46 | numeric_types = (int, float) 47 | 48 | import queue 49 | -------------------------------------------------------------------------------- /thundra/composite.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thundra import constants 4 | 5 | _common_fields_list = [ 6 | 'agentVersion', 7 | 'dataModelVersion', 8 | 'applicationId', 9 | 'applicationDomainName', 10 | 'applicationClassName', 11 | 'applicationName', 12 | 'applicationVersion', 13 | 'applicationStage', 14 | 'applicationRuntime', 15 | 'applicationRuntimeVersion', 16 | 'applicationTags' 17 | ] 18 | 19 | 20 | def init_composite_data_common_fields(data): 21 | return {field: data.get(field) for field in _common_fields_list} 22 | 23 | 24 | def remove_common_fields(data): 25 | without_common_fields = data.copy() 26 | for field in _common_fields_list: 27 | try: 28 | del without_common_fields[field] 29 | except (KeyError, TypeError): 30 | pass 31 | return without_common_fields 32 | 33 | 34 | def get_composite_data(all_monitoring_data, api_key, data): 35 | composite_data = { 36 | "type": "Composite", "dataModelVersion": constants.DATA_FORMAT_VERSION, 37 | "apiKey": api_key, 38 | 'data': data 39 | } 40 | composite_data['data']['id'] = str(uuid.uuid4()) 41 | composite_data['data']['type'] = "Composite" 42 | composite_data['data']['allMonitoringData'] = all_monitoring_data 43 | return composite_data 44 | -------------------------------------------------------------------------------- /thundra/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/config/__init__.py -------------------------------------------------------------------------------- /thundra/context/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/context/__init__.py -------------------------------------------------------------------------------- /thundra/context/context_provider.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | ABC = abc.ABCMeta('ABC', (object,), {}) 4 | 5 | 6 | class ContextProvider(ABC): 7 | @abc.abstractmethod 8 | def get(self): 9 | raise Exception("should be implemented") 10 | 11 | @abc.abstractmethod 12 | def set(self, execution_context): 13 | raise Exception("should be implemented") 14 | 15 | @abc.abstractmethod 16 | def clear(self): 17 | raise Exception("should be implemented") 18 | -------------------------------------------------------------------------------- /thundra/context/execution_context.py: -------------------------------------------------------------------------------- 1 | from thundra.opentracing.recorder import ThundraRecorder 2 | 3 | 4 | class ExecutionContext: 5 | """ 6 | Represents the scope of execution (request, invocation, etc ...) 7 | and holds scope specific data. 8 | """ 9 | 10 | def __init__(self, **opts): 11 | self.start_timestamp = opts.get('start_timestamp', 0) 12 | self.finish_timestamp = opts.get('finish_timestamp', 0) 13 | self.recorder = opts.get('recorder', ThundraRecorder()) 14 | self.reports = opts.get('reports', []) 15 | self.transaction_id = opts.get('transaction_id', '') 16 | self.span_id = opts.get('span_id', '') 17 | self.trace_id = opts.get('trace_id') 18 | self.root_span = opts.get('root_span') 19 | self.scope = opts.get('scope') 20 | self.invocation_data = opts.get('invocation_data') 21 | self.user_tags = opts.get('user_tags', {}) 22 | self.tags = opts.get('tags', {}) 23 | self.error = opts.get('error') 24 | self.user_error = opts.get('user_error') 25 | self.platform_data = opts.get('platform_data', {}) 26 | self.response = opts.get('response', {}) 27 | self.incoming_trace_links = opts.get('incoming_trace_links', []) 28 | self.outgoing_trace_links = opts.get('outgoing_trace_links', []) 29 | self.timeout = opts.get('timeout', False) 30 | self.logs = opts.get('logs', []) 31 | self.metrics = opts.get('metrics', {}) 32 | self.capture_log = False 33 | self.trigger_operation_name = '' 34 | self.application_resource_name = '' 35 | 36 | def report(self, data): 37 | """ 38 | Adds data to be reported 39 | @param data data to be reported 40 | """ 41 | if isinstance(data, list): 42 | for report in data: 43 | self.reports.append(report) 44 | else: 45 | self.reports.append(data) 46 | 47 | def get_operation_name(self): 48 | return '' 49 | 50 | def get_additional_start_tags(self): 51 | return {} 52 | 53 | def get_additional_finish_tags(self): 54 | return {} -------------------------------------------------------------------------------- /thundra/context/execution_context_manager.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context import ExecutionContext 2 | from thundra.context.global_execution_context_provider import GlobalExecutionContextProvider 3 | 4 | 5 | class ExecutionContextManager: 6 | context_provider = None 7 | 8 | @staticmethod 9 | def set_provider(provider): 10 | ExecutionContextManager.context_provider = provider 11 | 12 | @staticmethod 13 | def set(context): 14 | ExecutionContextManager.context_provider.set(context) 15 | 16 | @staticmethod 17 | def clear(): 18 | ExecutionContextManager.context_provider.clear() 19 | 20 | @staticmethod 21 | def get(): 22 | if not ExecutionContextManager.context_provider: 23 | return ExecutionContext() 24 | execution_context = ExecutionContextManager.context_provider.get() 25 | if not execution_context: 26 | return ExecutionContext() 27 | return execution_context 28 | -------------------------------------------------------------------------------- /thundra/context/global_execution_context_provider.py: -------------------------------------------------------------------------------- 1 | from thundra.context.context_provider import ContextProvider 2 | from thundra.context.execution_context import ExecutionContext 3 | 4 | _execution_context = ExecutionContext() 5 | 6 | 7 | class GlobalExecutionContextProvider(ContextProvider): 8 | def get(self): 9 | return _execution_context 10 | 11 | def set(self, execution_context): 12 | global _execution_context 13 | _execution_context = execution_context 14 | 15 | def clear(self): 16 | global _execution_context 17 | _execution_context = ExecutionContext() 18 | -------------------------------------------------------------------------------- /thundra/context/plugin_context.py: -------------------------------------------------------------------------------- 1 | class PluginContext: 2 | 3 | def __init__(self, **opts): 4 | self.application_info = opts.get('application_info') 5 | self.request_count = opts.get('request_count', 0) 6 | self.api_key = opts.get('api_key') 7 | self.executor = opts.get('executor') 8 | -------------------------------------------------------------------------------- /thundra/context/tracing_execution_context_provider.py: -------------------------------------------------------------------------------- 1 | from thundra.context.context_provider import ContextProvider 2 | from thundra.context.execution_context import ExecutionContext 3 | from thundra.opentracing.tracer import ThundraTracer 4 | 5 | 6 | class TracingExecutionContextProvider(ContextProvider): 7 | def __init__(self): 8 | self.tracer = ThundraTracer.get_instance() 9 | 10 | def get(self): 11 | execution_context = None 12 | active_span = self.tracer.get_active_span() 13 | if active_span and hasattr(active_span, 'execution_context'): 14 | execution_context = active_span.execution_context 15 | if not execution_context: 16 | return ExecutionContext() 17 | return execution_context 18 | 19 | def set(self, execution_context): 20 | active_span = self.tracer.get_active_span() 21 | if active_span and hasattr(active_span, 'execution_context'): 22 | active_span.execution_context = execution_context 23 | 24 | def clear(self): 25 | active_span = self.tracer.get_active_span() 26 | if active_span and hasattr(active_span, 'execution_context'): 27 | active_span.execution_context = None 28 | 29 | -------------------------------------------------------------------------------- /thundra/debug/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/debug/__init__.py -------------------------------------------------------------------------------- /thundra/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class JSONEncoder(json.JSONEncoder): 5 | def default(self, z): 6 | try: 7 | if isinstance(z, bytes): 8 | return z.decode('utf-8', errors='ignore') 9 | elif "to_json" in dir(z): 10 | return z.to_json() 11 | else: 12 | return super(JSONEncoder, self).default(z) 13 | except Exception as e: 14 | print(e) 15 | 16 | 17 | def to_json(data, separators=None): 18 | return json.dumps(data, separators=separators, cls=JSONEncoder) 19 | -------------------------------------------------------------------------------- /thundra/handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import imp 4 | import os 5 | 6 | from thundra.config.config_provider import ConfigProvider 7 | from thundra.config import config_names 8 | from thundra.thundra_agent import Thundra 9 | 10 | thundra = Thundra() 11 | 12 | handler_found = False 13 | user_handler = None 14 | 15 | handler_path = ConfigProvider.get(config_names.THUNDRA_LAMBDA_HANDLER, None) 16 | if handler_path is None: 17 | raise ValueError( 18 | "No handler specified for \'thundra_agent_lambda_handler\' environment variable" 19 | ) 20 | else: 21 | handler_found = True 22 | (module_name, handler_name) = handler_path.rsplit('.', 1) 23 | file_handle, pathname, desc = None, None, None 24 | try: 25 | for segment in module_name.split('.'): 26 | if pathname is not None: 27 | pathname = [pathname] 28 | file_handle, pathname, desc = imp.find_module(segment, pathname) 29 | user_module = imp.load_module(module_name, file_handle, pathname, desc) 30 | if file_handle is None: 31 | module_type = desc[2] 32 | if module_type == imp.C_BUILTIN: 33 | raise ImportError("Cannot use built-in module {} as a handler module".format(module_name)) 34 | user_handler = getattr(user_module, handler_name) 35 | finally: 36 | if file_handle: 37 | file_handle.close() 38 | 39 | 40 | def wrapper(event, context): 41 | global user_handler 42 | if handler_found and user_handler: 43 | if not hasattr(user_handler, "thundra_wrapper"): 44 | user_handler = thundra(user_handler) 45 | return user_handler(event, context) 46 | -------------------------------------------------------------------------------- /thundra/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/integrations/__init__.py -------------------------------------------------------------------------------- /thundra/integrations/aiohttp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/integrations/aiohttp/__init__.py -------------------------------------------------------------------------------- /thundra/integrations/django.py: -------------------------------------------------------------------------------- 1 | import thundra.constants as constants 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.integrations.base_integration import BaseIntegration 5 | from thundra.integrations.rdb_base import RdbBaseIntegration 6 | 7 | 8 | class DjangoORMIntegration(BaseIntegration, RdbBaseIntegration): 9 | CLASS_TYPE = 'django_orm' 10 | 11 | def __init__(self): 12 | pass 13 | 14 | def get_operation_name(self, wrapped, instance, args, kwargs): 15 | try: 16 | host = args[3]['cursor'].db.settings_dict.get('HOST') 17 | except: 18 | return '' 19 | return host 20 | 21 | def before_call(self, scope, execute, instance, _args, _kwargs, response, exception): 22 | scope.span.domain_name = constants.DomainNames['DB'] 23 | scope.span.class_name = constants.ClassNames['RDB'] 24 | 25 | try: 26 | db = _args[3]['cursor'].db 27 | settings = db.settings_dict 28 | vendor = db.vendor.upper() 29 | scope.span.class_name = constants.ClassNames.get(vendor, constants.ClassNames['RDB']) 30 | except: 31 | settings = {} 32 | vendor = '' 33 | 34 | query = '' 35 | operation = '' 36 | try: 37 | query = _args[0] 38 | if len(query) > 0: 39 | operation = query.split()[0].strip("\"").lower() 40 | except: 41 | pass 42 | 43 | tags = { 44 | constants.SpanTags['OPERATION_TYPE']: DjangoORMIntegration._OPERATION_TO_TYPE.get(operation, ''), 45 | constants.SpanTags['DB_INSTANCE']: settings.get('NAME'), 46 | constants.SpanTags['DB_HOST']: settings.get('HOST'), 47 | constants.SpanTags['DB_TYPE']: vendor, 48 | constants.SpanTags['DB_STATEMENT_TYPE']: operation.upper(), 49 | constants.SpanTags['TOPOLOGY_VERTEX']: True, 50 | } 51 | 52 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_STATEMENT_MASK): 53 | tags[constants.DBTags['DB_STATEMENT']] = query 54 | 55 | scope.span.tags = tags 56 | -------------------------------------------------------------------------------- /thundra/integrations/elasticsearch.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.integrations.base_integration import BaseIntegration 5 | 6 | 7 | class ElasticsearchIntegration(BaseIntegration): 8 | CLASS_TYPE = 'elasticsearch' 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def get_hosts(self, instance): 14 | try: 15 | hosts = [con.host for con in instance.connection_pool.connections] 16 | return hosts 17 | except Exception: 18 | return [] 19 | 20 | def get_normalized_path(self, es_uri): 21 | path_depth = ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_ELASTICSEARCH_PATH_DEPTH) 22 | 23 | path_seperator_count = 0 24 | normalized_path = '' 25 | prev_c = '' 26 | for c in es_uri: 27 | if c == '/' and prev_c != '/': 28 | path_seperator_count += 1 29 | 30 | if path_seperator_count > path_depth: 31 | break 32 | 33 | normalized_path += c 34 | prev_c = c 35 | return normalized_path 36 | 37 | def get_operation_name(self, wrapped, instance, args, kwargs): 38 | try: 39 | es_uri = args[1] 40 | return self.get_normalized_path(es_uri) 41 | except KeyError: 42 | return '' 43 | 44 | def before_call(self, scope, wrapped, instance, args, kwargs, response, exception): 45 | scope.span.class_name = constants.ClassNames['ELASTICSEARCH'] 46 | scope.span.domain_name = constants.DomainNames['DB'] 47 | 48 | operation_name = self.get_operation_name(wrapped, instance, args, kwargs) 49 | hosts = self.get_hosts(instance) 50 | 51 | http_method, es_path = args 52 | es_body = kwargs.get('body', {}) 53 | es_params = kwargs.get('params', {}) 54 | 55 | tags = { 56 | constants.ESTags['ES_HOSTS']: hosts, 57 | constants.ESTags['ES_URI']: es_path, 58 | constants.ESTags['ES_NORMALIZED_URI']: operation_name, 59 | constants.ESTags['ES_METHOD']: http_method, 60 | constants.ESTags['ES_PARAMS']: es_params, 61 | constants.DBTags['DB_TYPE']: 'elasticsearch', 62 | constants.SpanTags['OPERATION_TYPE']: http_method, 63 | constants.SpanTags['TOPOLOGY_VERTEX']: True, 64 | } 65 | 66 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_ELASTICSEARCH_BODY_MASK): 67 | tags[constants.ESTags['ES_BODY']] = es_body 68 | 69 | scope.span.tags = tags 70 | -------------------------------------------------------------------------------- /thundra/integrations/handler_wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import traceback 5 | from importlib import import_module 6 | 7 | MODULES = {} 8 | IGNORE_MODULES = ('__init__',) 9 | PYTHON_EXTENSIONS = ('.py', '.pyc') 10 | 11 | for module_name in os.listdir(os.path.dirname(__file__)): 12 | filename, ext = os.path.splitext(module_name) 13 | if filename in IGNORE_MODULES or ext not in PYTHON_EXTENSIONS: 14 | continue 15 | try: 16 | imported = import_module(".{}".format(filename), __name__) 17 | MODULES[filename] = imported 18 | except ImportError: 19 | traceback.print_exc() 20 | 21 | 22 | def _import_exists(module_name): 23 | try: 24 | import_module(module_name) 25 | return True 26 | except ImportError: 27 | return False 28 | 29 | 30 | def patch_modules(thundra_instance): 31 | for module_name, module in MODULES.items(): 32 | if _import_exists(module_name): 33 | module.patch(thundra_instance) 34 | -------------------------------------------------------------------------------- /thundra/integrations/handler_wrappers/chalice.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from importlib import import_module 3 | 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.config import config_names 6 | import wrapt 7 | 8 | _thundra_instance = None 9 | 10 | 11 | def _wrapper(wrapped, _, args, kwargs): 12 | wrapped = _thundra_instance(wrapped) 13 | return wrapped(*args, **kwargs) 14 | 15 | 16 | def patch(thundra_instance): 17 | if (not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_CHALICE_DISABLE)) and thundra_instance: 18 | global _thundra_instance 19 | _thundra_instance = thundra_instance 20 | try: 21 | import_module("chalice") 22 | wrapt.wrap_function_wrapper("chalice", "Chalice.__call__", _wrapper) 23 | except: 24 | pass 25 | -------------------------------------------------------------------------------- /thundra/integrations/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import traceback 4 | from importlib import import_module 5 | 6 | MODULES = {} 7 | IGNORE_MODULES = ('__init__',) 8 | PYTHON_EXTENSIONS = ('.py', '.pyc') 9 | 10 | for module_name in os.listdir(os.path.dirname(__file__)): 11 | filename, ext = os.path.splitext(module_name) 12 | if filename in IGNORE_MODULES or ext not in PYTHON_EXTENSIONS: 13 | continue 14 | try: 15 | imported = import_module(".{}".format(filename), __name__) 16 | MODULES[filename] = imported 17 | except ImportError: 18 | traceback.print_exc() 19 | -------------------------------------------------------------------------------- /thundra/integrations/modules/aiohttp.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | 6 | 7 | def _wrapper(wrapped, instance, args, kwargs): 8 | try: 9 | from thundra.integrations.aiohttp.client import ThundraTraceConfig 10 | trace_configs = kwargs.get('trace_configs', []) 11 | trace_configs.append(ThundraTraceConfig()) 12 | kwargs['trace_configs'] = trace_configs 13 | except: 14 | pass 15 | wrapped(*args, **kwargs) 16 | 17 | 18 | def patch(): 19 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_HTTP_DISABLE): 20 | try: 21 | import aiohttp 22 | wrapt.wrap_function_wrapper( 23 | 'aiohttp', 24 | 'ClientSession.__init__', 25 | _wrapper 26 | ) 27 | except ImportError: 28 | pass 29 | -------------------------------------------------------------------------------- /thundra/integrations/modules/botocore.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.integrations.base_integration import BaseIntegration 6 | from thundra.integrations.modules.requests import _wrapper as request_wrapper 7 | 8 | import thundra.integrations.botocore 9 | 10 | INTEGRATIONS = { 11 | class_obj.CLASS_TYPE: class_obj() 12 | for class_obj in BaseIntegration.__subclasses__() 13 | } 14 | 15 | 16 | def _wrapper(wrapped, instance, args, kwargs): 17 | integration_name = instance.__class__.__name__.lower() 18 | 19 | if integration_name in INTEGRATIONS: 20 | return INTEGRATIONS[integration_name].run_and_trace( 21 | wrapped, 22 | instance, 23 | args, 24 | kwargs 25 | ) 26 | 27 | return INTEGRATIONS['default'].run_and_trace( 28 | wrapped, 29 | instance, 30 | args, 31 | kwargs 32 | ) 33 | 34 | 35 | def patch(): 36 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_AWS_DISABLE): 37 | wrapt.wrap_function_wrapper( 38 | 'botocore.client', 39 | 'BaseClient._make_api_call', 40 | _wrapper 41 | ) 42 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_HTTP_DISABLE): 43 | try: 44 | wrapt.wrap_function_wrapper( 45 | 'botocore.vendored.requests', 46 | 'Session.send', 47 | request_wrapper 48 | ) 49 | except Exception: 50 | # Vendored version of requests is removed from botocore 51 | pass 52 | -------------------------------------------------------------------------------- /thundra/integrations/modules/db.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | 4 | class CursorWrapper(wrapt.ObjectProxy): 5 | 6 | def __init__(self, cursor, connection_wrapper, integration): 7 | super(CursorWrapper, self).__init__(cursor) 8 | self._self_connection = connection_wrapper 9 | self.integration = integration 10 | 11 | def execute(self, *args, **kwargs): 12 | return self.integration.run_and_trace( 13 | self.__wrapped__.execute, 14 | self._self_connection, 15 | args, 16 | kwargs, 17 | ) 18 | 19 | def callproc(self, *args, **kwargs): 20 | return self.integration.run_and_trace( 21 | self.__wrapped__.callproc, 22 | self._self_connection, 23 | args, 24 | kwargs, 25 | ) 26 | 27 | def __enter__(self): 28 | # raise appropriate error if api not supported (should reach the user) 29 | self.__wrapped__.__enter__ 30 | return self 31 | 32 | 33 | class ConnectionWrapper(wrapt.ObjectProxy): 34 | def __init__(self, conn, integration=None): 35 | self.integration = integration 36 | 37 | def cursor(self, *args, **kwargs): 38 | cursor = self.__wrapped__.cursor(*args, **kwargs) 39 | return CursorWrapper(cursor, self, integration=self.integration) 40 | -------------------------------------------------------------------------------- /thundra/integrations/modules/django.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import wrapt 3 | 4 | from thundra import utils, constants 5 | from thundra.config import config_names 6 | from thundra.config.config_provider import ConfigProvider 7 | from thundra.integrations.django import DjangoORMIntegration 8 | 9 | try: 10 | from django.conf import settings 11 | except ImportError: 12 | settings = None 13 | 14 | THUNDRA_MIDDLEWARE = "thundra.wrappers.django.middleware.ThundraMiddleware" 15 | 16 | django_orm_integration = DjangoORMIntegration() 17 | 18 | 19 | def _wrapper(wrapped, instance, args, kwargs): 20 | try: 21 | if getattr(settings, 'MIDDLEWARE', None): 22 | if THUNDRA_MIDDLEWARE in settings.MIDDLEWARE: 23 | return wrapped(*args, **kwargs) 24 | 25 | if isinstance(settings.MIDDLEWARE, tuple): 26 | settings.MIDDLEWARE = (THUNDRA_MIDDLEWARE,) + settings.MIDDLEWARE 27 | elif isinstance(settings.MIDDLEWARE, list): 28 | settings.MIDDLEWARE = [THUNDRA_MIDDLEWARE] + settings.MIDDLEWARE 29 | elif getattr(settings, 'MIDDLEWARE_CLASSES', None): 30 | if THUNDRA_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: 31 | return wrapped(*args, **kwargs) 32 | 33 | if isinstance(settings.MIDDLEWARE_CLASSES, tuple): 34 | settings.MIDDLEWARE = (THUNDRA_MIDDLEWARE,) + settings.MIDDLEWARE_CLASSES 35 | elif isinstance(settings.MIDDLEWARE_CLASSES, list): 36 | settings.MIDDLEWARE = [THUNDRA_MIDDLEWARE] + settings.MIDDLEWARE_CLASSES 37 | 38 | except Exception: 39 | pass 40 | return wrapped(*args, **kwargs) 41 | 42 | 43 | def db_execute_wrapper(execute, sql, params, many, context): 44 | return django_orm_integration.run_and_trace( 45 | execute, 46 | None, 47 | [sql, 48 | params, 49 | many, 50 | context], 51 | {} 52 | ) 53 | 54 | 55 | def install_db_execute_wrapper(connection, **kwargs): 56 | if db_execute_wrapper not in connection.execute_wrappers: 57 | connection.execute_wrappers.insert(0, db_execute_wrapper) 58 | 59 | 60 | def patch(): 61 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_DJANGO_DISABLE) and ( 62 | not utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_NAME)): 63 | wrapt.wrap_function_wrapper( 64 | 'django.core.handlers.base', 65 | 'BaseHandler.load_middleware', 66 | _wrapper 67 | ) 68 | 69 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_DJANGO_ORM_DISABLE): 70 | try: 71 | from django import VERSION 72 | from django.db import connections 73 | from django.db.backends.signals import connection_created 74 | 75 | if VERSION >= (2, 0): 76 | for connection in connections.all(): 77 | install_db_execute_wrapper(connection) 78 | 79 | connection_created.connect(install_db_execute_wrapper) 80 | except: 81 | pass 82 | -------------------------------------------------------------------------------- /thundra/integrations/modules/elasticsearch.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.integrations.elasticsearch import ElasticsearchIntegration 6 | 7 | es_integration = ElasticsearchIntegration() 8 | 9 | 10 | def _wrapper(wrapped, instance, args, kwargs): 11 | return es_integration.run_and_trace( 12 | wrapped, 13 | instance, 14 | args, 15 | kwargs 16 | ) 17 | 18 | 19 | def patch(): 20 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_ES_DISABLE): 21 | wrapt.wrap_function_wrapper( 22 | 'elasticsearch', 23 | 'transport.Transport.perform_request', 24 | _wrapper 25 | ) 26 | -------------------------------------------------------------------------------- /thundra/integrations/modules/fastapi.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra import utils, constants 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | 7 | def _wrapper(wrapped, instance, args, kwargs): 8 | """Set middleware to trace Fast api. Fastapi has been built on starlett and pydantic frameworks. 9 | Request and response flow has been handled by starlette that is a lightweight ASGI framework. Fastapi 10 | has an class called APIRouter that extends starlett Router class which is used to handle connections by starlette. 11 | The middleware should be an ASGI Middleware. Thus, the __call__(scope, receive, send) function should be implemented. 12 | By default, starlette add two middleware except user defined middlewares which are ServerErrorMiddleware and ExceptionMiddleware. 13 | Middleware list seems like [ServerErrorMiddleware, user_defined_middlewares, ExceptionMiddleware]. This list added in 14 | reversed order. Therefore, when we add our FastapiMiddleware to the zero index, it is placed top of middleware hierarchy. 15 | 16 | Args: 17 | wrapped (module): Wrapped module 18 | instance (function): Module enter point 19 | args (list): Wrapped function list of arguments 20 | kwargs (dict): Wrapped function key:value arguments 21 | """ 22 | from fastapi.middleware import Middleware 23 | from thundra.wrappers.fastapi.middleware import ThundraMiddleware 24 | from thundra.wrappers.fastapi.fastapi_wrapper import FastapiWrapper 25 | middlewares = kwargs.pop("middleware", []) 26 | middlewares.insert(0, Middleware(ThundraMiddleware, wrapper=FastapiWrapper.get_instance())) 27 | kwargs.update({"middleware": middlewares}) 28 | wrapped(*args, **kwargs) 29 | 30 | 31 | def patch(): 32 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_FASTAPI_DISABLE) and \ 33 | not utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_NAME): 34 | wrapt.wrap_function_wrapper( 35 | "fastapi.applications", 36 | "FastAPI.__init__", 37 | _wrapper 38 | ) -------------------------------------------------------------------------------- /thundra/integrations/modules/flask.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra import utils, constants 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.wrappers.flask.middleware import ThundraMiddleware 7 | 8 | 9 | def _wrapper(wrapped, instance, args, kwargs): 10 | response = wrapped(*args, **kwargs) 11 | try: 12 | thundra_middleware = ThundraMiddleware() 13 | thundra_middleware.set_app(instance) 14 | except: 15 | pass 16 | return response 17 | 18 | 19 | def patch(): 20 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_FLASK_DISABLE) and ( 21 | not utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_NAME)): 22 | wrapt.wrap_function_wrapper( 23 | 'flask', 24 | 'Flask.__init__', 25 | _wrapper 26 | ) 27 | -------------------------------------------------------------------------------- /thundra/integrations/modules/mysql.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.integrations.mysql import MysqlIntegration 6 | 7 | mysql_integration = MysqlIntegration() 8 | 9 | 10 | class MysqlCursorWrapper(wrapt.ObjectProxy): 11 | 12 | def __init__(self, cursor, connection_wrapper): 13 | super(MysqlCursorWrapper, self).__init__(cursor) 14 | self._self_connection = connection_wrapper 15 | 16 | def execute(self, *args, **kwargs): 17 | return mysql_integration.run_and_trace( 18 | self.__wrapped__.execute, 19 | self._self_connection, 20 | args, 21 | kwargs, 22 | ) 23 | 24 | def callproc(self, *args, **kwargs): 25 | return mysql_integration.run_and_trace( 26 | self.__wrapped__.callproc, 27 | self._self_connection, 28 | args, 29 | kwargs, 30 | ) 31 | 32 | def __enter__(self): 33 | # raise appropriate error if api not supported (should reach the user) 34 | self.__wrapped__.__enter__ 35 | return self 36 | 37 | 38 | class MysqlConnectionWrapper(wrapt.ObjectProxy): 39 | def cursor(self, *args, **kwargs): 40 | cursor = self.__wrapped__.cursor(*args, **kwargs) 41 | return MysqlCursorWrapper(cursor, self) 42 | 43 | 44 | def _wrapper(wrapped, instance, args, kwargs): 45 | connection = wrapped(*args, **kwargs) 46 | return MysqlConnectionWrapper(connection) 47 | 48 | 49 | def patch(): 50 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_DISABLE): 51 | wrapt.wrap_function_wrapper( 52 | 'mysql.connector', 53 | 'connect', 54 | _wrapper) 55 | -------------------------------------------------------------------------------- /thundra/integrations/modules/psycopg2.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import wrapt 4 | 5 | from thundra.config import config_names 6 | from thundra.config.config_provider import ConfigProvider 7 | from thundra.integrations.postgre import PostgreIntegration 8 | 9 | postgre_integration = PostgreIntegration() 10 | 11 | 12 | class PostgreCursorWrapper(wrapt.ObjectProxy): 13 | 14 | def __init__(self, cursor, connection_wrapper): 15 | super(PostgreCursorWrapper, self).__init__(cursor) 16 | self._self_connection = connection_wrapper 17 | 18 | def execute(self, *args, **kwargs): 19 | return postgre_integration.run_and_trace( 20 | self.__wrapped__.execute, 21 | self._self_connection, 22 | args, 23 | kwargs, 24 | ) 25 | 26 | def callproc(self, *args, **kwargs): 27 | return postgre_integration.run_and_trace( 28 | self.__wrapped__.callproc, 29 | self._self_connection, 30 | args, 31 | kwargs, 32 | ) 33 | 34 | def __enter__(self): 35 | # raise appropriate error if api not supported (should reach the user) 36 | value = self.__wrapped__.__enter__() 37 | if value is not self.__wrapped__: 38 | return value 39 | return self 40 | 41 | 42 | class PostgreConnectionWrapper(wrapt.ObjectProxy): 43 | def cursor(self, *args, **kwargs): 44 | cursor = self.__wrapped__.cursor(*args, **kwargs) 45 | return PostgreCursorWrapper(cursor, self) 46 | 47 | 48 | def _wrapper(wrapped, instance, args, kwargs): 49 | connection = wrapped(*args, **kwargs) 50 | return PostgreConnectionWrapper(connection) 51 | 52 | 53 | def _wrapper_register_type(wrapped, instance, args, kwargs): 54 | _args = list(copy.copy(args)) 55 | if len(_args) == 2 and isinstance(_args[1], (PostgreConnectionWrapper, PostgreCursorWrapper)): 56 | _args[1] = _args[1].__wrapped__ 57 | 58 | return wrapped(*_args, **kwargs) 59 | 60 | 61 | def patch_extensions(): 62 | _extensions = [ 63 | ('psycopg2.extensions', 'register_type', _wrapper_register_type), 64 | ('psycopg2.extensions', 'quote_ident', _wrapper_register_type), 65 | ('psycopg2._psycopg', 'register_type', _wrapper_register_type) 66 | ] 67 | 68 | try: 69 | import psycopg2 70 | if getattr(psycopg2, '_json', None): 71 | _extensions.append(('psycopg2._json', 'register_type', _wrapper_register_type)) 72 | except ImportError: 73 | pass 74 | 75 | for ext in _extensions: 76 | wrapt.wrap_function_wrapper(ext[0], ext[1], ext[2]) 77 | 78 | 79 | def patch(): 80 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_DISABLE): 81 | patch_extensions() 82 | wrapt.wrap_function_wrapper( 83 | 'psycopg2', 84 | 'connect', 85 | _wrapper) 86 | -------------------------------------------------------------------------------- /thundra/integrations/modules/pymongo.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.integrations.mongodb import CommandTracer 6 | 7 | 8 | def _wrapper(wrapped, instance, args, kwargs): 9 | event_listeners = list(kwargs.pop('event_listeners', [])) 10 | event_listeners.insert(0, CommandTracer()) 11 | kwargs['event_listeners'] = event_listeners 12 | wrapped(*args, **kwargs) 13 | 14 | 15 | def patch(): 16 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_MONGO_DISABLE): 17 | try: 18 | import pymongo.monitoring 19 | from bson.json_util import dumps 20 | wrapt.wrap_function_wrapper( 21 | 'pymongo', 22 | 'MongoClient.__init__', 23 | _wrapper 24 | ) 25 | except: 26 | pass 27 | -------------------------------------------------------------------------------- /thundra/integrations/modules/redis.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra import constants 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.integrations.redis import RedisIntegration 7 | 8 | redis_integration = RedisIntegration() 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | return redis_integration.run_and_trace( 13 | wrapped, 14 | instance, 15 | args, 16 | kwargs 17 | ) 18 | 19 | 20 | def patch(): 21 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_REDIS_DISABLE): 22 | for method in map(str.lower, constants.RedisCommandTypes.keys()): 23 | try: 24 | wrapt.wrap_function_wrapper( 25 | 'redis.client', 26 | 'Redis.' + method, 27 | _wrapper 28 | ) 29 | except: 30 | pass 31 | -------------------------------------------------------------------------------- /thundra/integrations/modules/requests.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra import utils 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.integrations.requests import RequestsIntegration 7 | 8 | request_integration = RequestsIntegration() 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | prepared_request = args[0] 13 | 14 | if utils.is_excluded_url(prepared_request.url): 15 | return wrapped(*args, **kwargs) 16 | 17 | return request_integration.run_and_trace( 18 | wrapped, 19 | instance, 20 | args, 21 | kwargs, 22 | ) 23 | 24 | 25 | def patch(): 26 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_HTTP_DISABLE): 27 | wrapt.wrap_function_wrapper( 28 | 'requests', 29 | 'Session.send', 30 | _wrapper 31 | ) 32 | -------------------------------------------------------------------------------- /thundra/integrations/modules/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.integrations.sqlalchemy import SqlAlchemyIntegration 6 | 7 | 8 | def _wrapper(wrapped, instance, args, kwargs): 9 | engine = wrapped(*args, **kwargs) 10 | SqlAlchemyIntegration(engine) 11 | return engine 12 | 13 | 14 | def patch(): 15 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_SQLALCHEMY_DISABLE): 16 | try: 17 | from sqlalchemy.event import listen 18 | from sqlalchemy.engine.interfaces import ExecutionContext 19 | wrapt.wrap_function_wrapper( 20 | 'sqlalchemy', 21 | 'create_engine', 22 | _wrapper 23 | ) 24 | 25 | wrapt.wrap_function_wrapper( 26 | 'sqlalchemy.engine', 27 | 'create_engine', 28 | _wrapper 29 | ) 30 | except: 31 | pass 32 | -------------------------------------------------------------------------------- /thundra/integrations/modules/sqlite3.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra.integrations.sqlite3 import SQLiteIntegration 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | 7 | sqlite_integration = SQLiteIntegration() 8 | 9 | 10 | class SqliteCursorWrapper(wrapt.ObjectProxy): 11 | 12 | def __init__(self, cursor, connection_wrapper): 13 | super(SqliteCursorWrapper, self).__init__(cursor) 14 | self._self_connection = connection_wrapper 15 | 16 | def execute(self, *args, **kwargs): 17 | return sqlite_integration.run_and_trace( 18 | self.__wrapped__.execute, 19 | self._self_connection, 20 | args, 21 | kwargs, 22 | ) 23 | 24 | def __enter__(self): 25 | # raise appropriate error if api not supported (should reach the user) 26 | self.__wrapped__.__enter__ 27 | return self 28 | 29 | 30 | class SqliteConnectionWrapper(wrapt.ObjectProxy): 31 | db_name = None 32 | host = None 33 | 34 | def __init__(self, connection, db_name, host='localhost'): 35 | super(SqliteConnectionWrapper, self).__init__(connection) 36 | self.db_name = db_name 37 | self.host = host 38 | 39 | def cursor(self): 40 | cursor = self.__wrapped__.cursor() 41 | return SqliteCursorWrapper(cursor, self) 42 | 43 | def execute(self, *args, **kwargs): 44 | return self.cursor().execute(*args, **kwargs) 45 | 46 | 47 | def _wrapper(wrapped, instance, args, kwargs): 48 | db_name = args[0] if args and len(args) > 0 else None 49 | connection = wrapped(*args, **kwargs) 50 | return SqliteConnectionWrapper(connection, db_name) 51 | 52 | 53 | def patch(): 54 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_DISABLE): 55 | wrapt.wrap_function_wrapper( 56 | 'sqlite3', 57 | 'connect', 58 | _wrapper) 59 | wrapt.wrap_function_wrapper( 60 | 'sqlite3.dbapi2', 61 | 'connect', 62 | _wrapper) 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /thundra/integrations/modules/tornado.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from thundra import utils, constants 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.wrappers.tornado.middleware import ThundraMiddleware 7 | 8 | 9 | def _init_wrapper(_wrapped, _application, args, kwargs): 10 | _wrapped(*args, **kwargs) 11 | 12 | middleware = _application.settings.get('_thundra_middleware') 13 | if middleware is None: 14 | _application.settings['_thundra_middleware'] = ThundraMiddleware() 15 | 16 | 17 | async def _execute_wrapper(_wrapped, _handler, args, kwargs): 18 | middleware = _handler.settings.get('_thundra_middleware') 19 | middleware.execute(_handler) 20 | return await _wrapped(*args, **kwargs) 21 | 22 | 23 | def _on_finish_wrapper(_wrapped, _handler, args, kwargs): 24 | middleware = _handler.settings.get('_thundra_middleware') 25 | middleware.finish(_handler) 26 | return _wrapped(*args, **kwargs) 27 | 28 | 29 | def _log_exception_wrapper(_wrapped, _handler, args, kwargs): 30 | value = args[1] if len(args) == 3 else None 31 | if value is None: 32 | return _wrapped(*args, **kwargs) 33 | 34 | middleware = _handler.settings.get('_thundra_middleware') 35 | try: 36 | from tornado.web import HTTPError 37 | if not isinstance(value, HTTPError) or 500 <= value.status_code <= 599: 38 | middleware.finish(_handler, error=value) 39 | except ImportError: 40 | error = type('', (object,), {"status_code": 500})() 41 | middleware.finish(_handler, error=error) 42 | 43 | return _wrapped(*args, **kwargs) 44 | 45 | 46 | def patch(): 47 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_TORNADO_DISABLE) and ( 48 | not utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_NAME)): 49 | 50 | wrapt.wrap_function_wrapper( 51 | 'tornado.web', 52 | 'Application.__init__', 53 | _init_wrapper 54 | ) 55 | wrapt.wrap_function_wrapper( 56 | 'tornado.web', 57 | 'RequestHandler._execute', 58 | _execute_wrapper 59 | ) 60 | wrapt.wrap_function_wrapper( 61 | 'tornado.web', 62 | 'RequestHandler.on_finish', 63 | _on_finish_wrapper 64 | ) 65 | wrapt.wrap_function_wrapper( 66 | 'tornado.web', 67 | 'RequestHandler.log_exception', 68 | _log_exception_wrapper 69 | ) -------------------------------------------------------------------------------- /thundra/integrations/mysql.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.integrations.base_integration import BaseIntegration 5 | from thundra.integrations.rdb_base import RdbBaseIntegration 6 | 7 | 8 | class MysqlIntegration(BaseIntegration, RdbBaseIntegration): 9 | CLASS_TYPE = 'mysql' 10 | 11 | def __init__(self): 12 | pass 13 | 14 | def get_operation_name(self, wrapped, instance, args, kwargs): 15 | return instance.database 16 | 17 | def before_call(self, scope, cursor, connection, _args, _kwargs, response, exception): 18 | span = scope.span 19 | span.domain_name = constants.DomainNames['DB'] 20 | span.class_name = constants.ClassNames['MYSQL'] 21 | 22 | query = '' 23 | operation = '' 24 | try: 25 | query = _args[0] 26 | if len(query) > 0: 27 | operation = query.split()[0].strip("\"").lower() 28 | except Exception: 29 | pass 30 | 31 | tags = { 32 | constants.SpanTags['OPERATION_TYPE']: MysqlIntegration._OPERATION_TO_TYPE.get(operation, ''), 33 | constants.SpanTags['DB_INSTANCE']: connection._database, 34 | constants.SpanTags['DB_HOST']: connection._host, 35 | constants.SpanTags['DB_TYPE']: "mysql", 36 | constants.SpanTags['DB_STATEMENT_TYPE']: operation.upper(), 37 | constants.SpanTags['TOPOLOGY_VERTEX']: True, 38 | } 39 | 40 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_STATEMENT_MASK): 41 | tags[constants.DBTags['DB_STATEMENT']] = query 42 | 43 | span.tags = tags 44 | -------------------------------------------------------------------------------- /thundra/integrations/postgre.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.integrations.rdb_base import RdbBaseIntegration 3 | from thundra.integrations.base_integration import BaseIntegration 4 | from thundra.config.config_provider import ConfigProvider 5 | from thundra.config import config_names 6 | 7 | try: 8 | from psycopg2.extensions import parse_dsn 9 | except ImportError: 10 | def parse_dsn(dsn): 11 | return dict( 12 | attribute.split("=") for attribute in dsn.split() 13 | if "=" in attribute 14 | ) 15 | 16 | 17 | class PostgreIntegration(BaseIntegration, RdbBaseIntegration): 18 | CLASS_TYPE = 'postgresql' 19 | 20 | def __init__(self): 21 | pass 22 | 23 | def get_operation_name(self, wrapped, instance, args, kwargs): 24 | return parse_dsn(instance.dsn).get('dbname', '') 25 | 26 | def before_call(self, scope, cursor, connection, _args, _kwargs, response, exception): 27 | span = scope.span 28 | span.domain_name = constants.DomainNames['DB'] 29 | span.class_name = constants.ClassNames['POSTGRESQL'] 30 | 31 | dsn = parse_dsn(connection.dsn) 32 | 33 | query = '' 34 | operation = '' 35 | try: 36 | query = _args[0] 37 | if len(query) > 0: 38 | operation = query.split()[0].strip("\"").lower() 39 | except Exception: 40 | pass 41 | 42 | tags = { 43 | constants.SpanTags['OPERATION_TYPE']: PostgreIntegration._OPERATION_TO_TYPE.get(operation, ''), 44 | constants.SpanTags['DB_INSTANCE']: dsn.get('dbname', ''), 45 | constants.SpanTags['DB_HOST']: dsn.get('host', ''), 46 | constants.SpanTags['DB_TYPE']: "postgresql", 47 | constants.SpanTags['DB_STATEMENT_TYPE']: operation.upper(), 48 | constants.SpanTags['TRIGGER_CLASS_NAME']: "API", 49 | constants.SpanTags['TOPOLOGY_VERTEX']: True 50 | } 51 | 52 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_STATEMENT_MASK): 53 | tags[constants.DBTags['DB_STATEMENT']] = query 54 | 55 | span.tags = tags 56 | 57 | -------------------------------------------------------------------------------- /thundra/integrations/rdb_base.py: -------------------------------------------------------------------------------- 1 | 2 | class RdbBaseIntegration(): 3 | _OPERATION_TO_TABLE_NAME_KEYWORD = { 4 | 'select': 'from', 5 | 'insert': 'into', 6 | 'update': 'update', 7 | 'delete': 'from', 8 | 'create': 'table' 9 | } 10 | 11 | _OPERATION_TO_TYPE = { 12 | 'select': 'READ', 13 | 'insert': 'WRITE', 14 | 'update': 'WRITE', 15 | 'delete': 'DELETE' 16 | } 17 | -------------------------------------------------------------------------------- /thundra/integrations/redis.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.integrations.base_integration import BaseIntegration 5 | 6 | 7 | class RedisIntegration(BaseIntegration): 8 | CLASS_TYPE = 'redis' 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def get_operation_name(self, wrapped, instance, args, kwargs): 14 | connection_kwargs = instance.connection_pool.connection_kwargs 15 | return connection_kwargs.get('host', 'Redis') 16 | 17 | def before_call(self, scope, wrapped, instance, args, kwargs, response, exception): 18 | connection_kwargs = instance.connection_pool.connection_kwargs 19 | host = connection_kwargs.get('host', '') 20 | port = connection_kwargs.get('port', '6379') 21 | command_type = wrapped.__name__.upper() or "" 22 | operation_type = constants.RedisCommandTypes.get(command_type, '') 23 | command = '{} {}'.format(command_type, ' '.join([str(arg) for arg in args])) 24 | 25 | scope.span.domain_name = constants.DomainNames['CACHE'] 26 | scope.span.class_name = constants.ClassNames['REDIS'] 27 | 28 | tags = { 29 | constants.SpanTags['OPERATION_TYPE']: operation_type, 30 | constants.DBTags['DB_INSTANCE']: host, 31 | constants.DBTags['DB_STATEMENT_TYPE']: operation_type, 32 | constants.DBTags['DB_TYPE']: 'redis', 33 | constants.RedisTags['REDIS_HOST']: host, 34 | constants.RedisTags['REDIS_PORT']: port, 35 | constants.RedisTags['REDIS_COMMAND_TYPE']: command_type, 36 | constants.SpanTags['TOPOLOGY_VERTEX']: True, 37 | } 38 | 39 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_REDIS_COMMAND_MASK): 40 | tags[constants.DBTags['DB_STATEMENT']] = command 41 | tags[constants.RedisTags['REDIS_COMMAND']] = command 42 | 43 | scope.span.tags = tags 44 | -------------------------------------------------------------------------------- /thundra/integrations/sqlite3.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.config import config_names 3 | from thundra.config.config_provider import ConfigProvider 4 | from thundra.integrations.base_integration import BaseIntegration 5 | from thundra.integrations.rdb_base import RdbBaseIntegration 6 | 7 | 8 | class SQLiteIntegration(BaseIntegration, RdbBaseIntegration): 9 | CLASS_TYPE = 'sqlite' 10 | 11 | def __init__(self): 12 | super(SQLiteIntegration, self) 13 | 14 | def get_operation_name(self, wrapped, instance, args, kwargs): 15 | return instance.db_name 16 | 17 | def before_call(self, scope, cursor, connection, _args, _kwargs, response, exception): 18 | span = scope.span 19 | span.domain_name = constants.DomainNames['DB'] 20 | span.class_name = constants.ClassNames['SQLITE'] 21 | 22 | query = '' 23 | operation = '' 24 | try: 25 | query = _args[0] 26 | if len(query) > 0: 27 | operation = query.split()[0].strip("\"").lower() 28 | except Exception: 29 | pass 30 | 31 | tags = { 32 | constants.SpanTags['OPERATION_TYPE']: SQLiteIntegration._OPERATION_TO_TYPE.get(operation, ''), 33 | constants.SpanTags['DB_INSTANCE']: connection.db_name, 34 | constants.SpanTags['DB_HOST']: connection.host, 35 | constants.SpanTags['DB_TYPE']: self.CLASS_TYPE, 36 | constants.SpanTags['DB_STATEMENT_TYPE']: operation.upper(), 37 | constants.SpanTags['TOPOLOGY_VERTEX']: True, 38 | } 39 | 40 | if not ConfigProvider.get(config_names.THUNDRA_TRACE_INTEGRATIONS_RDB_STATEMENT_MASK): 41 | tags[constants.DBTags['DB_STATEMENT']] = query 42 | 43 | span.tags = tags 44 | -------------------------------------------------------------------------------- /thundra/listeners/__init__.py: -------------------------------------------------------------------------------- 1 | from thundra.listeners.thundra_span_listener import ThundraSpanListener 2 | from thundra.listeners.error_injector_span_listener import ErrorInjectorSpanListener 3 | from thundra.listeners.latency_injector_span_listener import LatencyInjectorSpanListener 4 | from thundra.listeners.filtering_span_listener import FilteringSpanListener 5 | from thundra.listeners.tag_injector_span_listener import TagInjectorSpanListener 6 | from thundra.listeners.security_aware_span_listener import SecurityAwareSpanListener 7 | -------------------------------------------------------------------------------- /thundra/listeners/composite_span_filter.py: -------------------------------------------------------------------------------- 1 | from thundra.listeners.thundra_span_filterer import SpanFilter 2 | 3 | 4 | class CompositeSpanFilter(SpanFilter): 5 | 6 | def __init__(self, is_all=False, filters=None): 7 | if filters is None: 8 | filters = [] 9 | self.all = is_all 10 | self.filters = filters 11 | 12 | def accept(self, span): 13 | if (not self.filters) or len(self.filters) == 0: 14 | return True 15 | 16 | result = self.all 17 | 18 | if self.all: 19 | for span_filter in self.filters: 20 | result = result and span_filter.accept(span) 21 | else: 22 | for span_filter in self.filters: 23 | result = result or span_filter.accept(span) 24 | return result 25 | 26 | def set_filters(self, filters): 27 | self.filters = filters 28 | -------------------------------------------------------------------------------- /thundra/listeners/tag_injector_span_listener.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | from thundra.listeners.thundra_span_listener import ThundraSpanListener 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | try: 10 | import builtins 11 | except: 12 | import __builtin__ as builtins 13 | 14 | 15 | class TagInjectorSpanListener(ThundraSpanListener): 16 | def __init__(self, inject_on_finish=False, tags_to_inject=None): 17 | self.inject_on_finish = inject_on_finish 18 | self.tags_to_inject = tags_to_inject 19 | 20 | def on_span_started(self, span): 21 | if not self.inject_on_finish: 22 | self._inject_tags(span) 23 | 24 | def on_span_finished(self, span): 25 | if self.inject_on_finish: 26 | self._inject_tags(span) 27 | 28 | def _inject_tags(self, span): 29 | if self.tags_to_inject: 30 | for tag in self.tags_to_inject: 31 | span.set_tag(tag, self.tags_to_inject.get(tag)) 32 | 33 | @staticmethod 34 | def from_config(config): 35 | kwargs = {} 36 | inject_on_finish = config.get('injectOnFinish') 37 | tags_to_inject = config.get('tags') 38 | if inject_on_finish is not None: 39 | kwargs['inject_on_finish'] = inject_on_finish 40 | if tags_to_inject is not None: 41 | kwargs['tags_to_inject'] = tags_to_inject 42 | 43 | return TagInjectorSpanListener(**kwargs) 44 | 45 | @staticmethod 46 | def should_raise_exceptions(): 47 | return False 48 | -------------------------------------------------------------------------------- /thundra/listeners/thundra_span_listener.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | ABC = abc.ABCMeta('ABC', (object,), {}) 4 | 5 | 6 | class ThundraSpanListener(ABC): 7 | @abc.abstractmethod 8 | def on_span_started(self, operation_name): 9 | raise Exception("should be implemented") 10 | 11 | @abc.abstractmethod 12 | def on_span_finished(self, span): 13 | raise Exception("should be implemented") 14 | 15 | @staticmethod 16 | @abc.abstractmethod 17 | def from_config(config): 18 | raise Exception("should be implemented") 19 | 20 | @staticmethod 21 | @abc.abstractmethod 22 | def should_raise_exceptions(): 23 | raise Exception("should be implemented") 24 | -------------------------------------------------------------------------------- /thundra/opentracing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/opentracing/__init__.py -------------------------------------------------------------------------------- /thundra/opentracing/propagation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/opentracing/propagation/__init__.py -------------------------------------------------------------------------------- /thundra/opentracing/propagation/http.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | 3 | from thundra.opentracing.propagation.text import TextMapPropagator 4 | from thundra.opentracing.span_context import ThundraSpanContext 5 | 6 | 7 | class HTTPPropagator(TextMapPropagator): 8 | 9 | @staticmethod 10 | def extract_value_from_header(header_name, headers): 11 | for header, value in headers.items(): 12 | if header.lower() == header_name.lower(): 13 | return value 14 | 15 | def extract(self, carrier): 16 | try: 17 | trace_id = HTTPPropagator.extract_value_from_header(constants.THUNDRA_TRACE_ID_KEY, carrier) 18 | span_id = HTTPPropagator.extract_value_from_header(constants.THUNDRA_SPAN_ID_KEY, carrier) 19 | transaction_id = HTTPPropagator.extract_value_from_header(constants.THUNDRA_TRANSACTION_ID_KEY, carrier) 20 | except: 21 | return None 22 | 23 | if not (trace_id and span_id and transaction_id): 24 | return None 25 | 26 | # Extract baggage items 27 | baggage = {} 28 | for key in carrier: 29 | if isinstance(key, str): 30 | if key.startswith(constants.THUNDRA_BAGGAGE_PREFIX): 31 | baggage[key[len(constants.THUNDRA_BAGGAGE_PREFIX):]] = carrier[key] 32 | elif isinstance(key, tuple): 33 | if key[0].startswith(constants.THUNDRA_BAGGAGE_PREFIX): 34 | baggage[key[0][len(constants.THUNDRA_BAGGAGE_PREFIX):]] = key[1] 35 | 36 | span_context = ThundraSpanContext(trace_id=trace_id, span_id=span_id, transaction_id=transaction_id, 37 | baggage=baggage) 38 | 39 | return span_context 40 | -------------------------------------------------------------------------------- /thundra/opentracing/propagation/propagator.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | ABC = ABCMeta('ABC', (object,), {'__slots__': ()}) 4 | 5 | 6 | class Propagator(ABC): 7 | 8 | @abstractmethod 9 | def inject(self, span_context, carrier): 10 | pass 11 | 12 | @abstractmethod 13 | def extract(self, carrier): 14 | pass 15 | -------------------------------------------------------------------------------- /thundra/opentracing/propagation/text.py: -------------------------------------------------------------------------------- 1 | from opentracing import InvalidCarrierException 2 | 3 | from thundra import constants 4 | from thundra.opentracing.propagation.propagator import Propagator 5 | from thundra.opentracing.span_context import ThundraSpanContext 6 | 7 | 8 | class TextMapPropagator(Propagator): 9 | 10 | def inject(self, span_context, carrier): 11 | if span_context.trace_id: 12 | carrier[constants.THUNDRA_TRACE_ID_KEY] = span_context.trace_id 13 | 14 | if span_context.span_id: 15 | carrier[constants.THUNDRA_SPAN_ID_KEY] = span_context.span_id 16 | 17 | if span_context.transaction_id: 18 | carrier[constants.THUNDRA_TRANSACTION_ID_KEY] = span_context.transaction_id 19 | 20 | # Inject baggage items 21 | if span_context.baggage is not None: 22 | for key in span_context.baggage: 23 | carrier[constants.THUNDRA_BAGGAGE_PREFIX + key] = span_context.baggage[key] 24 | 25 | def extract(self, carrier): 26 | try: 27 | trace_id = carrier[constants.THUNDRA_TRACE_ID_KEY] 28 | span_id = carrier[constants.THUNDRA_SPAN_ID_KEY] 29 | transaction_id = carrier[constants.THUNDRA_TRANSACTION_ID_KEY] 30 | except: 31 | return None 32 | 33 | if not (trace_id and span_id and transaction_id): 34 | return None 35 | 36 | # Extract baggage items 37 | baggage = {} 38 | for key in carrier: 39 | if key.startswith(constants.THUNDRA_BAGGAGE_PREFIX): 40 | baggage[key[len(constants.THUNDRA_BAGGAGE_PREFIX):]] = carrier[key] 41 | 42 | span_context = ThundraSpanContext(trace_id=trace_id, span_id=span_id, transaction_id=transaction_id, 43 | baggage=baggage) 44 | 45 | return span_context 46 | -------------------------------------------------------------------------------- /thundra/opentracing/recorder.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from threading import Lock 3 | 4 | 5 | class ThundraRecorder: 6 | 7 | def __init__(self): 8 | self._lock = Lock() 9 | self._spans = [] 10 | 11 | def record(self, span): 12 | with self._lock: 13 | self._spans.append(span) 14 | 15 | def get_spans(self): 16 | return copy.copy(self._spans) 17 | 18 | def clear(self): 19 | self._spans = [] 20 | -------------------------------------------------------------------------------- /thundra/opentracing/span_context.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import opentracing 4 | 5 | 6 | class ThundraSpanContext(opentracing.SpanContext): 7 | 8 | def __init__(self, 9 | trace_id=None, 10 | transaction_id=None, 11 | span_id=None, 12 | parent_span_id=None, 13 | baggage=None): 14 | self._trace_id = trace_id 15 | self._transaction_id = transaction_id 16 | self._span_id = span_id 17 | self._parent_span_id = parent_span_id 18 | self._baggage = baggage or opentracing.SpanContext.EMPTY_BAGGAGE 19 | 20 | @property 21 | def trace_id(self): 22 | return self._trace_id 23 | 24 | @property 25 | def transaction_id(self): 26 | return self._transaction_id 27 | 28 | @property 29 | def span_id(self): 30 | return self._span_id 31 | 32 | @span_id.setter 33 | def span_id(self, value): 34 | self._span_id = value 35 | 36 | @property 37 | def parent_span_id(self): 38 | return self._parent_span_id 39 | 40 | @property 41 | def baggage(self): 42 | return self._baggage 43 | 44 | def context_with_baggage_item(self, key, value): 45 | new_baggage_item = copy.copy(self.baggage) 46 | new_baggage_item[key] = value 47 | return ThundraSpanContext(trace_id=self.trace_id, 48 | transaction_id=self.transaction_id, 49 | span_id=self.span_id, 50 | parent_span_id=self.parent_span_id, 51 | baggage=new_baggage_item) 52 | -------------------------------------------------------------------------------- /thundra/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/plugins/__init__.py -------------------------------------------------------------------------------- /thundra/plugins/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/plugins/config/__init__.py -------------------------------------------------------------------------------- /thundra/plugins/config/base_plugin_config.py: -------------------------------------------------------------------------------- 1 | 2 | class BasePluginConfig(object): 3 | def __init__(self, enabled=False): 4 | self.enabled = enabled 5 | -------------------------------------------------------------------------------- /thundra/plugins/config/log_config.py: -------------------------------------------------------------------------------- 1 | from thundra.plugins.config.base_plugin_config import BasePluginConfig 2 | 3 | 4 | class LogConfig(BasePluginConfig): 5 | def __init__(self, enabled=False, sampler=None): 6 | super(LogConfig, self).__init__(enabled=enabled) 7 | self.sampler = sampler 8 | -------------------------------------------------------------------------------- /thundra/plugins/config/metric_config.py: -------------------------------------------------------------------------------- 1 | from thundra.plugins.config.base_plugin_config import BasePluginConfig 2 | 3 | 4 | class MetricConfig(BasePluginConfig): 5 | def __init__(self, opts=None): 6 | if opts is None: 7 | opts = {} 8 | super(MetricConfig, self).__init__(enabled=opts.get('enabled', True)) 9 | self.sampler = opts.get('sampler') 10 | -------------------------------------------------------------------------------- /thundra/plugins/config/thundra_config.py: -------------------------------------------------------------------------------- 1 | from thundra.plugins.config.log_config import LogConfig 2 | from thundra.plugins.config.metric_config import MetricConfig 3 | from thundra.plugins.config.trace_config import TraceConfig 4 | 5 | 6 | class ThundraConfig: 7 | def __init__(self, opts=None): 8 | if opts is None: 9 | opts = {} 10 | 11 | self.api_key = opts.get('apiKey') 12 | self.disable_thundra = opts.get('disableThundra') 13 | self.trace_config = TraceConfig(opts.get('traceConfig')) 14 | self.metric_config = MetricConfig(opts.get('metricConfig')) 15 | self.log_config = LogConfig(opts.get('logConfig')) 16 | -------------------------------------------------------------------------------- /thundra/plugins/config/trace_config.py: -------------------------------------------------------------------------------- 1 | from thundra.plugins.config.base_plugin_config import BasePluginConfig 2 | 3 | 4 | class TraceConfig(BasePluginConfig): 5 | def __init__(self, opts=None): 6 | if opts is None: 7 | opts = {} 8 | super(TraceConfig, self).__init__(enabled=opts.get('enabled', True)) 9 | self.sampler = opts.get('sampler') 10 | -------------------------------------------------------------------------------- /thundra/plugins/invocation/__init__.py: -------------------------------------------------------------------------------- 1 | from . import invocation_support 2 | from . import invocation_trace_support 3 | -------------------------------------------------------------------------------- /thundra/plugins/invocation/invocation_plugin.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from thundra.encoder import to_json 4 | from thundra import constants 5 | 6 | 7 | class InvocationPlugin: 8 | 9 | def __init__(self, plugin_context=None): 10 | self.hooks = { 11 | 'before:invocation': self.before_invocation, 12 | 'after:invocation': self.after_invocation 13 | } 14 | self.plugin_context = plugin_context 15 | 16 | def before_invocation(self, execution_context): 17 | executor = self.plugin_context.executor 18 | if executor: 19 | executor.start_invocation(self.plugin_context, execution_context) 20 | 21 | def after_invocation(self, execution_context): 22 | executor = self.plugin_context.executor 23 | if executor: 24 | executor.finish_invocation(execution_context) 25 | report_data = { 26 | 'apiKey': self.plugin_context.api_key, 27 | 'type': 'Invocation', 28 | 'dataModelVersion': constants.DATA_FORMAT_VERSION, 29 | 'data': execution_context.invocation_data 30 | } 31 | execution_context.report(json.loads(to_json(report_data))) 32 | -------------------------------------------------------------------------------- /thundra/plugins/invocation/invocation_support.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context_manager import ExecutionContextManager 2 | 3 | 4 | def set_agent_tag(key, value): 5 | execution_context = ExecutionContextManager.get() 6 | execution_context.tags[key] = value 7 | 8 | 9 | def set_many_agent(tags): 10 | execution_context = ExecutionContextManager.get() 11 | execution_context.tags.update(tags) 12 | 13 | 14 | def get_agent_tag(key): 15 | execution_context = ExecutionContextManager.get() 16 | if key in execution_context.tags: 17 | return execution_context.tags[key] 18 | return None 19 | 20 | 21 | def get_agent_tags(): 22 | execution_context = ExecutionContextManager.get() 23 | return execution_context.tags.copy() 24 | 25 | 26 | def remove_agent_tag(key): 27 | execution_context = ExecutionContextManager.get() 28 | return execution_context.tags.pop(key, None) 29 | 30 | 31 | def set_tag(key, value): 32 | execution_context = ExecutionContextManager.get() 33 | execution_context.user_tags[key] = value 34 | 35 | 36 | def set_tags(tags): 37 | execution_context = ExecutionContextManager.get() 38 | execution_context.user_tags.update(tags) 39 | 40 | 41 | def set_many(tags): 42 | execution_context = ExecutionContextManager.get() 43 | execution_context.user_tags.update(tags) 44 | 45 | 46 | def get_tag(key): 47 | execution_context = ExecutionContextManager.get() 48 | if key in execution_context.user_tags: 49 | return execution_context.user_tags[key] 50 | return None 51 | 52 | 53 | def get_tags(): 54 | execution_context = ExecutionContextManager.get() 55 | return execution_context.user_tags.copy() 56 | 57 | 58 | def remove_tag(key): 59 | execution_context = ExecutionContextManager.get() 60 | return execution_context.user_tags.pop(key, None) 61 | 62 | 63 | def clear(): 64 | execution_context = ExecutionContextManager.get() 65 | execution_context.user_tags.clear() 66 | execution_context.tags.clear() 67 | 68 | 69 | def clear_error(): 70 | execution_context = ExecutionContextManager.get() 71 | execution_context.user_error = None 72 | 73 | 74 | def set_error(err): 75 | execution_context = ExecutionContextManager.get() 76 | execution_context.user_error = err 77 | 78 | 79 | def get_error(): 80 | execution_context = ExecutionContextManager.get() 81 | return execution_context.user_error 82 | 83 | 84 | def set_application_resource_name(resource_name): 85 | execution_context = ExecutionContextManager.get() 86 | execution_context.application_resource_name = resource_name 87 | -------------------------------------------------------------------------------- /thundra/plugins/log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/plugins/log/__init__.py -------------------------------------------------------------------------------- /thundra/plugins/log/log_support.py: -------------------------------------------------------------------------------- 1 | _sampler = None 2 | 3 | 4 | def get_sampler(): 5 | return _sampler 6 | 7 | 8 | def set_sampler(sampler): 9 | global _sampler 10 | _sampler = sampler 11 | -------------------------------------------------------------------------------- /thundra/plugins/log/thundra_log_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | 4 | from thundra.context.execution_context_manager import ExecutionContextManager 5 | 6 | 7 | class ThundraLogHandler(logging.Handler): 8 | 9 | def __init__(self): 10 | logging.Handler.__init__(self) 11 | 12 | def emit(self, record): 13 | formatted_message = self.format(record) 14 | execution_context = ExecutionContextManager.get() 15 | if execution_context and execution_context.capture_log: 16 | log = { 17 | 'id': str(uuid.uuid4()), 18 | 'spanId': execution_context.span_id if execution_context is not None else '', 19 | 'logMessage': formatted_message, 20 | 'logContextName': record.name, 21 | 'logTimestamp': int(record.created * 1000), 22 | 'logLevel': record.levelname, 23 | 'logLevelCode': int(record.levelno / 10) 24 | } 25 | execution_context.logs.append(log) 26 | 27 | 28 | logging.ThundraLogHandler = ThundraLogHandler 29 | -------------------------------------------------------------------------------- /thundra/plugins/log/thundra_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from thundra.config import config_names 4 | from thundra.config.config_provider import ConfigProvider 5 | 6 | loggers = {} 7 | 8 | 9 | class StreamToLogger(object): 10 | def __init__(self, logger, stdout): 11 | self.logger = logger 12 | self.stdout = stdout 13 | 14 | def write(self, buf): 15 | for line in buf.rstrip().splitlines(): 16 | self.logger.info(line.rstrip()) 17 | self.stdout.write(buf) 18 | 19 | def flush(self): 20 | self.stdout.flush() 21 | 22 | 23 | def get_logger(name): 24 | global loggers 25 | if loggers.get(name): 26 | return loggers.get(name) 27 | else: 28 | format = "%(asctime)s - %(levelname)s - %(name)s - %(message)s" 29 | if name is None: 30 | logger = logging.getLogger(__name__) 31 | else: 32 | logger = logging.getLogger(name) 33 | logger.setLevel(logging.DEBUG) 34 | console_handler = logging.StreamHandler() 35 | console_handler.setLevel(logging.DEBUG) 36 | ch_format = logging.Formatter(format) 37 | console_handler.setFormatter(ch_format) 38 | logger.addHandler(console_handler) 39 | loggers[name] = logger 40 | return logger 41 | 42 | 43 | def log_to_console(message, handler): 44 | logger = get_logger(handler) 45 | logging.getLogger().handlers = [] 46 | logger.debug(message) 47 | 48 | 49 | def debug_logger(msg, handler=None): 50 | if ConfigProvider.get(config_names.THUNDRA_DEBUG_ENABLE): 51 | if hasattr(msg, '__dict__'): 52 | log_to_console(msg, handler) 53 | display = vars(msg) 54 | log_to_console(display, handler) 55 | for key, _ in display.items(): 56 | debug_logger_helper(getattr(msg, key), handler) 57 | else: 58 | log_to_console(msg, handler) 59 | 60 | 61 | def debug_logger_helper(msg, handler): 62 | if hasattr(msg, '__dict__'): 63 | log_to_console(msg, handler) 64 | display = vars(msg) 65 | log_to_console(display, handler) 66 | for key, _ in display.items(): 67 | debug_logger_helper(getattr(msg, key), handler) 68 | -------------------------------------------------------------------------------- /thundra/plugins/metric/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/plugins/metric/__init__.py -------------------------------------------------------------------------------- /thundra/plugins/metric/metric_support.py: -------------------------------------------------------------------------------- 1 | from thundra.samplers import ( 2 | TimeAwareSampler, CountAwareSampler, 3 | CompositeSampler 4 | ) 5 | 6 | _sampler = CompositeSampler( 7 | samplers=[ 8 | TimeAwareSampler(), 9 | CountAwareSampler() 10 | ], 11 | operator='or' 12 | ) 13 | 14 | 15 | def get_sampler(): 16 | return _sampler 17 | 18 | 19 | def set_sampler(sampler): 20 | global _sampler 21 | _sampler = sampler 22 | -------------------------------------------------------------------------------- /thundra/plugins/trace/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/plugins/trace/__init__.py -------------------------------------------------------------------------------- /thundra/plugins/trace/trace_aware_wrapper.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from thundra.opentracing.tracer import ThundraTracer 3 | 4 | 5 | class TraceAwareWrapper: 6 | 7 | def __init__(self, active_span=None): 8 | if active_span is None: 9 | active_scope = ThundraTracer.get_instance().scope_manager.active 10 | active_span = active_scope.span if active_scope is not None else None 11 | self._parent_span = active_span 12 | 13 | @property 14 | def parent_span(self): 15 | return self._parent_span 16 | 17 | def __call__(self, original_func): 18 | @wraps(original_func) 19 | def wrapper(*args, **kwargs): 20 | ThundraTracer.get_instance().scope_manager.activate(self.parent_span, True) 21 | return original_func(*args, **kwargs) 22 | return wrapper 23 | -------------------------------------------------------------------------------- /thundra/samplers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_sampler import BaseSampler 2 | from .duration_aware_sampler import DurationAwareSampler 3 | from .error_aware_sampler import ErrorAwareSampler 4 | 5 | from .time_aware_sampler import TimeAwareSampler 6 | from .count_aware_sampler import CountAwareSampler 7 | from .composite_sampler import CompositeSampler 8 | -------------------------------------------------------------------------------- /thundra/samplers/base_sampler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | ABC = ABCMeta('ABC', (object,), {}) 4 | 5 | 6 | class BaseSampler(ABC): 7 | 8 | @abstractmethod 9 | def is_sampled(self, args): 10 | raise Exception("should be implemented") 11 | -------------------------------------------------------------------------------- /thundra/samplers/composite_sampler.py: -------------------------------------------------------------------------------- 1 | from thundra.samplers.base_sampler import BaseSampler 2 | 3 | default_operator = 'or' 4 | available_operators = ['and', 'or'] 5 | 6 | 7 | class CompositeSampler(BaseSampler): 8 | 9 | def __init__(self, samplers=None, operator=default_operator): 10 | if samplers is None: 11 | samplers = [] 12 | self.samplers = samplers 13 | if operator in available_operators: 14 | self.operator = operator 15 | else: 16 | self.operator = default_operator 17 | 18 | def is_sampled(self, args=None): 19 | if len(self.samplers) == 0: 20 | return False 21 | 22 | if self.operator == 'or': 23 | sampled = False 24 | for sampler in self.samplers: 25 | sampled = sampler.is_sampled(args) or sampled 26 | return sampled 27 | elif self.operator == 'and': 28 | sampled = True 29 | for sampler in self.samplers: 30 | sampled = sampler.is_sampled(args) and sampled 31 | return sampled 32 | -------------------------------------------------------------------------------- /thundra/samplers/count_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | from thundra import constants 4 | from thundra.config import config_names 5 | from thundra.config.config_provider import ConfigProvider 6 | from thundra.samplers.base_sampler import BaseSampler 7 | 8 | 9 | class CountAwareSampler(BaseSampler): 10 | 11 | def __init__(self, count_freq=None): 12 | freq_from_env = ConfigProvider.get(config_names.THUNDRA_SAMPLER_COUNTAWARE_COUNTFREQ, -1) 13 | if freq_from_env > 0: 14 | self.count_freq = freq_from_env 15 | elif count_freq is not None: 16 | self.count_freq = count_freq 17 | else: 18 | self.count_freq = constants.DEFAULT_METRIC_SAMPLING_COUNT_FREQ 19 | 20 | self._counter = -1 21 | self._lock = Lock() 22 | 23 | def is_sampled(self, args=None): 24 | return self._increment_and_get_counter() % self.count_freq == 0 25 | 26 | def _increment_and_get_counter(self): 27 | with self._lock: 28 | self._counter += 1 29 | 30 | return self._counter 31 | -------------------------------------------------------------------------------- /thundra/samplers/duration_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from thundra.samplers.base_sampler import BaseSampler 2 | 3 | 4 | class DurationAwareSampler(BaseSampler): 5 | 6 | def __init__(self, duration, longer_than=False): 7 | self.duration = duration 8 | self.longer_than = longer_than 9 | 10 | def is_sampled(self, span=None): 11 | if not span: 12 | return False 13 | 14 | if self.longer_than: 15 | return span.get_duration() > self.duration 16 | 17 | return span.get_duration() <= self.duration 18 | -------------------------------------------------------------------------------- /thundra/samplers/error_aware_sampler.py: -------------------------------------------------------------------------------- 1 | from thundra.samplers.base_sampler import BaseSampler 2 | 3 | 4 | class ErrorAwareSampler(BaseSampler): 5 | 6 | def __init__(self): 7 | pass 8 | 9 | def is_sampled(self, span=None): 10 | if not span: 11 | return False 12 | 13 | return span.get_tag("error") 14 | -------------------------------------------------------------------------------- /thundra/samplers/time_aware_sampler.py: -------------------------------------------------------------------------------- 1 | import time 2 | from threading import Lock 3 | 4 | from thundra import constants 5 | from thundra.samplers.base_sampler import BaseSampler 6 | 7 | from thundra.config.config_provider import ConfigProvider 8 | from thundra.config import config_names 9 | 10 | 11 | class TimeAwareSampler(BaseSampler): 12 | 13 | def __init__(self, time_freq=None): 14 | freq_from_env = ConfigProvider.get(config_names.THUNDRA_SAMPLER_TIMEAWARE_TIMEFREQ, -1) 15 | if freq_from_env > 0: 16 | self.time_freq = freq_from_env 17 | elif time_freq is not None: 18 | self.time_freq = time_freq 19 | else: 20 | self.time_freq = constants.DEFAULT_METRIC_SAMPLING_TIME_FREQ 21 | self._latest_time = 0 22 | self._lock = Lock() 23 | 24 | def is_sampled(self, args=None): 25 | sampled = False 26 | with self._lock: 27 | current_time = 1000 * time.time() 28 | if current_time > self._latest_time + self.time_freq: 29 | self._latest_time = current_time 30 | sampled = True 31 | return sampled 32 | -------------------------------------------------------------------------------- /thundra/serializable.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | ABC = abc.ABCMeta('ABC', (object,), {}) 4 | 5 | 6 | class Serializable(ABC): 7 | 8 | @abc.abstractmethod 9 | def serialize(self): 10 | return self.__dict__ 11 | -------------------------------------------------------------------------------- /thundra/thundra_agent.py: -------------------------------------------------------------------------------- 1 | from thundra.wrappers.aws_lambda.lambda_wrapper import LambdaWrapper 2 | 3 | Thundra = LambdaWrapper 4 | -------------------------------------------------------------------------------- /thundra/timeout.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import sys 3 | import threading 4 | 5 | 6 | class ThreadTimeout(object): 7 | def __init__(self, seconds, handler, execution_context): 8 | self.seconds = seconds 9 | self.timer = None 10 | self.handler = handler 11 | self.execution_context = execution_context 12 | 13 | def __enter__(self): 14 | self.timer = threading.Timer(self.seconds, self.handler, [self.execution_context]) 15 | if self.seconds > 0: 16 | self.timer.start() 17 | return self 18 | 19 | def __exit__(self, exc_type, exc_val, exc_tb): 20 | if self.timer: 21 | self.timer.cancel() 22 | return False 23 | 24 | 25 | class SignalTimeout(object): 26 | def __init__(self, seconds, handler, execution_context): 27 | self.seconds = seconds 28 | self.handler = handler 29 | self.execution_context = execution_context 30 | 31 | def __enter__(self): 32 | if threading.current_thread().__class__.__name__ == '_MainThread': 33 | signal.signal(signal.SIGALRM, self._timeout) 34 | if self.seconds > 0: 35 | signal.setitimer(signal.ITIMER_REAL, self.seconds) 36 | return self 37 | 38 | def __exit__(self, exc_type, exc_val, exc_tb): 39 | if threading.current_thread().__class__.__name__ == '_MainThread': 40 | signal.setitimer(signal.ITIMER_REAL, 0) 41 | return False 42 | 43 | def _timeout(self, signum, frame): 44 | current_thread = threading.current_thread().__class__.__name__ 45 | if current_thread == '_MainThread' and signum == signal.SIGALRM: 46 | self.handler(self.execution_context) 47 | 48 | 49 | if sys.platform == "win32": 50 | Timeout = ThreadTimeout 51 | else: 52 | Timeout = SignalTimeout 53 | -------------------------------------------------------------------------------- /thundra/wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/wrappers/__init__.py -------------------------------------------------------------------------------- /thundra/wrappers/aws_lambda/__init__.py: -------------------------------------------------------------------------------- 1 | from .lambda_application_info_provider import LambdaApplicationInfoProvider 2 | -------------------------------------------------------------------------------- /thundra/wrappers/aws_lambda/handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import imp 4 | import os 5 | 6 | from thundra.thundra_agent import Thundra 7 | 8 | thundra = Thundra() 9 | 10 | handler_found = False 11 | user_handler = None 12 | handler_path = os.environ.get('thundra_agent_lambda_handler', None) 13 | if handler_path is None: 14 | raise ValueError( 15 | "No handler specified for \'thundra_agent_lambda_handler\' environment variable" 16 | ) 17 | else: 18 | handler_found = True 19 | (module_name, handler_name) = handler_path.rsplit('.', 1) 20 | file_handle, pathname, desc = None, None, None 21 | try: 22 | for segment in module_name.split('.'): 23 | if pathname is not None: 24 | pathname = [pathname] 25 | file_handle, pathname, desc = imp.find_module(segment, pathname) 26 | user_module = imp.load_module(module_name, file_handle, pathname, desc) 27 | if file_handle is None: 28 | module_type = desc[2] 29 | if module_type == imp.C_BUILTIN: 30 | raise ImportError("Cannot use built-in module {} as a handler module".format(module_name)) 31 | user_handler = getattr(user_module, handler_name) 32 | finally: 33 | if file_handle: 34 | file_handle.close() 35 | 36 | 37 | def wrapper(event, context): 38 | global user_handler 39 | if handler_found and user_handler: 40 | if not hasattr(user_handler, "_thundra_wrapped"): 41 | user_handler = thundra(user_handler) 42 | return user_handler(event, context) 43 | -------------------------------------------------------------------------------- /thundra/wrappers/aws_lambda/lambda_application_info_provider.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thundra import constants, utils 4 | from thundra.application.application_info_provider import ApplicationInfoProvider 5 | 6 | 7 | class LambdaApplicationInfoProvider(ApplicationInfoProvider): 8 | 9 | def __init__(self): 10 | log_stream_name = utils.get_env_variable(constants.AWS_LAMBDA_LOG_STREAM_NAME) 11 | function_version = utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_VERSION) 12 | function_name = utils.get_env_variable(constants.AWS_LAMBDA_FUNCTION_NAME) 13 | region = utils.get_env_variable(constants.AWS_REGION, default='') 14 | 15 | application_instance_id = str(uuid.uuid4()) 16 | if log_stream_name and len(log_stream_name.split(']')) >= 2: 17 | application_instance_id = log_stream_name.split(']')[1] 18 | 19 | self.application_info = { 20 | 'applicationId': '', 21 | 'applicationInstanceId': application_instance_id, 22 | 'applicationName': function_name, 23 | 'applicationVersion': function_version, 24 | 'applicationRegion': region 25 | } 26 | 27 | def get_application_info(self): 28 | return self.application_info 29 | 30 | def get_application_tags(self): 31 | return self.application_info.get('applicationTags', {}).copy() 32 | 33 | @staticmethod 34 | def get_application_id(context, application_name=None): 35 | arn = getattr(context, constants.CONTEXT_INVOKED_FUNCTION_ARN, '') 36 | region = utils.get_aws_region_from_arn(arn) 37 | if not region: 38 | region = 'local' 39 | account_no = 'sam_local' if utils.sam_local_debugging() else utils.get_aws_account_no(arn) 40 | function_name = application_name if application_name else utils.get_aws_function_name(arn) 41 | application_id_template = 'aws:lambda:{region}:{account_no}:{function_name}' 42 | 43 | return application_id_template.format(region=region, account_no=account_no, function_name=function_name) 44 | 45 | def update(self, opts): 46 | filtered_opts = {k: v for k, v in opts.items() if v is not None} 47 | self.application_info.update(filtered_opts) 48 | -------------------------------------------------------------------------------- /thundra/wrappers/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/wrappers/django/__init__.py -------------------------------------------------------------------------------- /thundra/wrappers/django/django_executor.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.wrappers import wrapper_utils, web_wrapper_utils 3 | 4 | 5 | def start_trace(plugin_context, execution_context, tracer): 6 | request = execution_context.platform_data['request'] 7 | import sys 8 | if sys.version_info[0] >= 3: 9 | request_route_path = str( 10 | request.resolver_match.route) if request.resolver_match and request.resolver_match.route else None 11 | request = { 12 | 'method': request.method, 13 | 'host': request.get_host().split(':')[0], 14 | 'query_params': request.GET, 15 | 'body': request.body, 16 | 'headers': request.headers, 17 | 'path': request.path 18 | } 19 | else: 20 | request_route_path = str( 21 | request.resolver_match.url_name) if request.resolver_match and request.resolver_match.url_name else None 22 | request = { 23 | 'method': request.method, 24 | 'host': request.get_host().split(':')[0], 25 | 'query_params': request.GET, 26 | 'body': request.body, 27 | 'headers': request.META, 28 | 'path': request.path 29 | } 30 | web_wrapper_utils.start_trace(execution_context, tracer, 'Django', 'API', request, request_route_path) 31 | 32 | 33 | def finish_trace(execution_context): 34 | root_span = execution_context.root_span 35 | if execution_context.response: 36 | root_span.set_tag(constants.HttpTags['HTTP_STATUS'], execution_context.response.status_code) 37 | if execution_context.trigger_operation_name: 38 | execution_context.response[ 39 | constants.TRIGGER_RESOURCE_NAME_KEY] = execution_context.trigger_operation_name 40 | web_wrapper_utils.finish_trace(execution_context) 41 | 42 | 43 | def start_invocation(plugin_context, execution_context): 44 | execution_context.invocation_data = wrapper_utils.create_invocation_data(plugin_context, execution_context) 45 | 46 | 47 | def finish_invocation(execution_context): 48 | wrapper_utils.finish_invocation(execution_context) 49 | 50 | # Set response status code 51 | wrapper_utils.set_response_status(execution_context, get_response_status(execution_context)) 52 | 53 | 54 | def get_response_status(execution_context): 55 | try: 56 | status_code = execution_context.response.status_code 57 | except: 58 | return None 59 | return status_code 60 | -------------------------------------------------------------------------------- /thundra/wrappers/django/middleware.py: -------------------------------------------------------------------------------- 1 | from thundra.context.execution_context_manager import ExecutionContextManager 2 | from thundra.wrappers.django.django_wrapper import DjangoWrapper, logger 3 | from thundra.wrappers.web_wrapper_utils import process_request_route 4 | 5 | 6 | class ThundraMiddleware(object): 7 | def __init__(self, get_response=None): 8 | super(ThundraMiddleware, self).__init__() 9 | self.get_response = get_response 10 | self._wrapper = DjangoWrapper() 11 | 12 | def __call__(self, request): 13 | setattr(request, '_thundra_wrapped', True) 14 | # Code to be executed for each request before 15 | # the view (and later middleware) are called. 16 | before_done = False 17 | try: 18 | self._wrapper.before_request(request) 19 | before_done = True 20 | except Exception as e: 21 | logger.error("Error during the before part of Thundra: {}".format(e)) 22 | 23 | response = self.get_response(request) 24 | 25 | # Code to be executed for each request/response after 26 | # the view is called. 27 | if before_done: 28 | try: 29 | self._wrapper.after_request(response) 30 | except Exception as e: 31 | logger.error("Error during the after part of Thundra: {}".format(e)) 32 | 33 | return response 34 | 35 | def process_view(self, request, view_func, view_args, view_kwargs): 36 | execution_context = ExecutionContextManager.get() 37 | if request.resolver_match: 38 | request_host = request.get_host().split(':')[0] 39 | import sys 40 | if sys.version_info[0] >= 3: 41 | process_request_route(execution_context, request.resolver_match.route, request_host) 42 | else: 43 | process_request_route(execution_context, request.resolver_match.url_name, request_host) 44 | 45 | def process_exception(self, request, exception): 46 | self._wrapper.process_exception(exception) 47 | -------------------------------------------------------------------------------- /thundra/wrappers/fastapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/wrappers/fastapi/__init__.py -------------------------------------------------------------------------------- /thundra/wrappers/fastapi/fastapi_executor.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.wrappers import wrapper_utils, web_wrapper_utils 3 | 4 | from thundra.wrappers.fastapi.fastapi_utils import extract_headers 5 | 6 | import json 7 | 8 | def start_trace(plugin_context, execution_context, tracer): 9 | request = execution_context.platform_data["request"] 10 | 11 | request = { 12 | "method": request.get("method"), 13 | "host": request.get("server")[0], 14 | "query_params": request.get("query_string"), 15 | "headers": extract_headers(request), 16 | "body": request.get("body"), 17 | "path": request.get("path") 18 | } 19 | 20 | web_wrapper_utils.start_trace(execution_context, tracer, "Fastapi", "API", request) 21 | 22 | 23 | def finish_trace(execution_context): 24 | root_span = execution_context.root_span 25 | if execution_context.response: 26 | status_code = get_response_status(execution_context) 27 | if status_code: 28 | root_span.set_tag(constants.HttpTags['HTTP_STATUS'], status_code) 29 | if execution_context.trigger_operation_name: 30 | if isinstance(execution_context.response, dict): 31 | if execution_context.response.get('headers'): 32 | execution_context.response.get('headers')[ 33 | constants.TRIGGER_RESOURCE_NAME_KEY] = execution_context.trigger_operation_name 34 | else: 35 | if hasattr(execution_context.response, 'headers'): 36 | execution_context.response.headers[ 37 | constants.TRIGGER_RESOURCE_NAME_KEY] = execution_context.trigger_operation_name 38 | web_wrapper_utils.finish_trace(execution_context) 39 | 40 | 41 | def start_invocation(plugin_context, execution_context): 42 | execution_context.invocation_data = wrapper_utils.create_invocation_data(plugin_context, execution_context) 43 | 44 | 45 | def finish_invocation(execution_context): 46 | wrapper_utils.finish_invocation(execution_context) 47 | 48 | wrapper_utils.set_response_status(execution_context, get_response_status(execution_context)) 49 | 50 | 51 | def get_response_status(execution_context): 52 | try: 53 | status_code = execution_context.response.get("status_code") or execution_context.response.get("status") 54 | except: 55 | return None 56 | return status_code -------------------------------------------------------------------------------- /thundra/wrappers/fastapi/fastapi_utils.py: -------------------------------------------------------------------------------- 1 | 2 | def bytes_to_str(value): 3 | """Convert byte to string 4 | 5 | Args: 6 | value (byte]): 7 | 8 | Returns: 9 | [str]: value if value is str o.w str(value) 10 | """ 11 | if isinstance(value, bytes): 12 | return value.decode("utf-8") 13 | return value 14 | 15 | 16 | def extract_headers(connection_obj): 17 | """Convert nested list headers in request/response object to dict 18 | 19 | Args: 20 | connection_obj (obj): request or response object 21 | 22 | Returns: 23 | dict: request or response headers dict version 24 | """ 25 | headers = connection_obj.get("headers") 26 | if headers: 27 | return dict((bytes_to_str(k), bytes_to_str(v)) for (k,v) in headers) 28 | return {} -------------------------------------------------------------------------------- /thundra/wrappers/flask/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/wrappers/flask/__init__.py -------------------------------------------------------------------------------- /thundra/wrappers/flask/middleware.py: -------------------------------------------------------------------------------- 1 | from thundra.wrappers.flask.flask_wrapper import FlaskWrapper, logger 2 | from thundra.utils import Singleton 3 | 4 | class ThundraMiddleware(Singleton): 5 | def __init__(self): 6 | self._wrapper = FlaskWrapper() 7 | 8 | def set_app(self, app): 9 | app.before_request(self.before_request) 10 | app.after_request(self.after_request) 11 | app.teardown_request(self.teardown_request) 12 | 13 | def before_request(self): 14 | try: 15 | from flask import request 16 | setattr(request, '_thundra_wrapped', True) 17 | self._wrapper.before_request(request) 18 | except Exception as e: 19 | logger.error("Error during the before part of Thundra: {}".format(e)) 20 | 21 | def after_request(self, response): 22 | try: 23 | self._wrapper.after_request(response) 24 | except Exception as e: 25 | logger.error("Error setting response to context for Thundra: {}".format(e)) 26 | return response 27 | 28 | def teardown_request(self, exception): 29 | try: 30 | self._wrapper.teardown_request(exception) 31 | except Exception as e: 32 | logger.error("Error during the request teardown of Thundra: {}".format(e)) 33 | -------------------------------------------------------------------------------- /thundra/wrappers/tornado/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thundra-io/thundra-agent-python/3451551d1a91d46959a98cb4b5a3588be63d84dc/thundra/wrappers/tornado/__init__.py -------------------------------------------------------------------------------- /thundra/wrappers/tornado/middleware.py: -------------------------------------------------------------------------------- 1 | from thundra.wrappers.tornado.tornado_wrapper import TornadoWrapper, logger 2 | 3 | 4 | class ThundraMiddleware(object): 5 | def __init__(self): 6 | super(ThundraMiddleware, self).__init__() 7 | self._wrapper = TornadoWrapper() 8 | 9 | def execute(self, handler): 10 | try: 11 | request = handler.request 12 | self._wrapper.before_request(request) 13 | except Exception as e: 14 | logger.error("Error during the before part of Thundra: {}".format(e)) 15 | 16 | def finish(self, handler, error=None): 17 | try: 18 | http_response = type('', (object,), {"status_code": handler.get_status()})() 19 | self._wrapper.after_request(http_response, error) 20 | except Exception as e: 21 | logger.error("Error during the after part of Thundra: {}".format(e)) 22 | -------------------------------------------------------------------------------- /thundra/wrappers/tornado/tornado_executor.py: -------------------------------------------------------------------------------- 1 | from thundra import constants 2 | from thundra.wrappers import wrapper_utils, web_wrapper_utils 3 | 4 | 5 | def start_trace(plugin_context, execution_context, tracer): 6 | request = execution_context.platform_data['request'] 7 | request_route_path = str(request.path) if request.path else None 8 | 9 | _request = { 10 | 'method': request.method, 11 | 'host': request.host.split(':')[0], 12 | 'query_params': request.query, 13 | 'body': request.body, 14 | 'headers': request.headers, 15 | 'path': request.path 16 | } 17 | 18 | web_wrapper_utils.start_trace(execution_context, tracer, 'Tornado', 'API', _request, request_route_path) 19 | 20 | 21 | def finish_trace(execution_context): 22 | root_span = execution_context.root_span 23 | if execution_context.response: 24 | status_code = get_response_status(execution_context) 25 | if status_code: 26 | root_span.set_tag(constants.HttpTags['HTTP_STATUS'], status_code) 27 | if execution_context.trigger_operation_name and execution_context.response and hasattr( 28 | execution_context.response, 'headers'): 29 | execution_context.response.headers[ 30 | constants.TRIGGER_RESOURCE_NAME_KEY] = execution_context.trigger_operation_name 31 | web_wrapper_utils.finish_trace(execution_context) 32 | 33 | 34 | def start_invocation(plugin_context, execution_context): 35 | execution_context.invocation_data = wrapper_utils.create_invocation_data(plugin_context, execution_context) 36 | 37 | 38 | def finish_invocation(execution_context): 39 | wrapper_utils.finish_invocation(execution_context) 40 | 41 | # Set response status code 42 | wrapper_utils.set_response_status(execution_context, get_response_status(execution_context)) 43 | 44 | 45 | def get_response_status(execution_context): 46 | try: 47 | status_code = execution_context.response.status_code 48 | except: 49 | return None 50 | return status_code 51 | -------------------------------------------------------------------------------- /thundra/wrappers/wrapper_factory.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | 4 | class WrapperFactory: 5 | lock = Lock() 6 | wrappers = {} 7 | 8 | @staticmethod 9 | def get_or_create(wrapper_class): 10 | with WrapperFactory.lock: 11 | if wrapper_class in WrapperFactory.wrappers: 12 | return WrapperFactory.wrappers[wrapper_class] 13 | else: 14 | WrapperFactory.wrappers[wrapper_class] = wrapper_class() 15 | return WrapperFactory.wrappers[wrapper_class] 16 | --------------------------------------------------------------------------------