├── .github └── CODEOWNERS ├── .gitignore ├── .pylintrc ├── .releaserc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── acceptance ├── acceptance.py ├── lambda-handlers │ ├── .gitignore │ ├── bad_events.json │ ├── handler.py │ ├── requirements.txt │ └── serverless.yml └── run.sh ├── epsagon ├── __init__.py ├── cacert.pem ├── common.py ├── constants.py ├── event.py ├── events │ ├── __init__.py │ ├── azure.py │ ├── botocore.py │ ├── celery.py │ ├── dbapi.py │ ├── greengrasssdk.py │ ├── httplib2.py │ ├── kafka.py │ ├── pymongo.py │ ├── pynamodb.py │ ├── pyqldb.py │ ├── qcloud_cos.py │ ├── redis.py │ ├── requests.py │ ├── sqlalchemy.py │ ├── tornado_client.py │ ├── urllib.py │ └── urllib3.py ├── handler.py ├── http_filters.py ├── modules │ ├── MySQLdb.py │ ├── __init__.py │ ├── azure.py │ ├── botocore.py │ ├── celery.py │ ├── db_wrapper.py │ ├── django.py │ ├── fastapi.py │ ├── flask.py │ ├── general_wrapper.py │ ├── greengrasssdk.py │ ├── gunicorn.py │ ├── httplib2.py │ ├── kafka.py │ ├── logging.py │ ├── pg8000.py │ ├── psycopg2.py │ ├── pymongo.py │ ├── pymysql.py │ ├── pynamodb.py │ ├── pyqldb.py │ ├── qcloud_cos.py │ ├── redis.py │ ├── requests.py │ ├── sqlalchemy.py │ ├── tornado.py │ ├── urllib.py │ └── urllib3.py ├── patcher.py ├── runners │ ├── __init__.py │ ├── aws_lambda.py │ ├── azure_function.py │ ├── celery.py │ ├── django.py │ ├── fastapi.py │ ├── flask.py │ ├── gcp_function.py │ ├── python_function.py │ ├── tencent_function.py │ └── tornado.py ├── trace.py ├── trace_encoder.py ├── trace_transports.py ├── triggers │ ├── __init__.py │ ├── aws_lambda.py │ ├── azure_function.py │ ├── http.py │ └── tencent_function.py ├── utils.py └── wrappers │ ├── __init__.py │ ├── aws_lambda.py │ ├── azure_function.py │ ├── chalice.py │ ├── custom.py │ ├── django.py │ ├── fastapi.py │ ├── flask.py │ ├── gcp_function.py │ ├── python_function.py │ └── tencent_function.py ├── examples ├── aws_lambda.py ├── custom_labels.py ├── custom_python.py └── fastapi_example.py ├── package-lock.json ├── package.json ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── publish.sh ├── publish_layer.sh ├── publish_package.py ├── run_acceptance_tests.sh ├── run_lint.sh ├── run_tests.sh ├── semantic_release.sh └── set_version.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── events │ ├── __init__.py │ ├── test_eventbridge.py │ ├── test_kafka.py │ ├── test_requests_event.py │ ├── test_secretsmanager.py │ └── test_tornado_client.py ├── modules │ ├── __init__.py │ ├── test_greengrasssdk.py │ ├── test_logging.py │ ├── test_requests.py │ └── test_sqlalchemy.py ├── test_epsagon_init.py ├── test_modules.py ├── test_patcher.py ├── test_trace.py ├── test_transports.py ├── test_utils.py └── wrappers │ ├── __init__.py │ ├── common.py │ ├── django_test │ ├── db.sqlite3 │ ├── django_test │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── polls │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ └── stress.py │ ├── test_custom.py │ ├── test_django_wrapper.py │ ├── test_fastapi_wrapper.py │ ├── test_flask_wrapper.py │ ├── test_lambda_wrapper.py │ └── test_python_function.py ├── tox.ini └── trace.png /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | * @epsagon/the-fabulous-team 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | .pypirc 4 | .coverage 5 | .cache 6 | dist 7 | .eggs 8 | .pytest_cache 9 | build 10 | epsagon.egg-info 11 | .tox 12 | venv/ 13 | .vscode 14 | .env -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/ci_build/pylintrc 2 | 3 | [MASTER] 4 | 5 | # Profiled execution. 6 | profile=no 7 | 8 | # Don't pickle collected data for later comparisons. 9 | persistent=no 10 | 11 | # List of plugins (as comma separated values of python modules names) to load, 12 | # usually to register additional checkers. 13 | load-plugins=pylint_quotes 14 | 15 | 16 | [MESSAGES CONTROL] 17 | 18 | disable=duplicate-code,too-few-public-methods,too-many-arguments,fixme,too-many-instance-attributes,bad-continuation,too-many-locals,logging-format-interpolation,too-many-branches,useless-object-inheritance,assignment-from-no-return,useless-import-alias 19 | 20 | # Ignore no member when source is unavailable 21 | extension-pkg-whitelist=ujson 22 | 23 | [REPORT] 24 | 25 | msg-template='{abspath}:{line}: [{msg_id}({symbol}) {obj}] {msg}' 26 | 27 | output-format=parseable 28 | 29 | # Tells whether to display a full report or only the messages 30 | reports=no 31 | 32 | # Python expression which should return a note less than 10 (10 is the highest 33 | # note). You have access to the variables errors warning, statement which 34 | # respectively contain the number of errors / warnings messages and the total 35 | # number of statements analyzed. This is used by the global evaluation report 36 | # (RP0004). 37 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 38 | 39 | # Add a comment according to your evaluation note. This is used by the global 40 | # evaluation report (RP0004). 41 | comment=no 42 | 43 | 44 | [BASIC] 45 | 46 | # Required attributes for module, separated by a comma 47 | required-attributes= 48 | 49 | # List of builtins function names that should not be used, separated by a comma 50 | bad-functions=apply,input,reduce 51 | 52 | # Regular expression which should only match correct module names 53 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 54 | 55 | # Regular expression which should only match correct module level names 56 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 57 | 58 | # Regular expression which should only match correct class names 59 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$ 60 | 61 | # Regular expression which should only match correct function names 62 | function-rgx=^(?:(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ 63 | 64 | # Regular expression which should only match correct method names 65 | method-rgx=^(?:(?P__[a-z0-9_]+__|next)|(?P_{0,2}[A-Z][a-zA-Z0-9]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ 66 | 67 | # Regular expression which should only match correct instance attribute names 68 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ 69 | 70 | # Regular expression which should only match correct argument names 71 | argument-rgx=^[_a-z][a-z0-9_]*$ 72 | 73 | # Regular expression which should only match correct variable names 74 | variable-rgx=^[a-z][a-z0-9_]*$ 75 | 76 | # Regular expression which should only match correct attribute names in class 77 | # bodies 78 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 79 | 80 | # Regular expression which should only match correct list comprehension / 81 | # generator expression variable names 82 | inlinevar-rgx=^[a-z][a-z0-9_]*$ 83 | 84 | # Good variable names which should always be accepted, separated by a comma 85 | good-names=main,_ 86 | 87 | # Regular expression which should only match function or class names that do 88 | # not require a docstring. 89 | no-docstring-rgx=(__.*__|main) 90 | 91 | # Minimum line length for functions/classes that require docstrings, shorter 92 | # ones are exempt. 93 | docstring-min-length=10 94 | 95 | 96 | [FORMAT] 97 | # Maximum number of characters on a single line. 98 | max-line-length=80 99 | 100 | 101 | # Maximum number of lines in a module 102 | max-module-lines=500 103 | 104 | # Allow the body of an if to be on the same line as the test if there is no 105 | # else. 106 | single-line-if-stmt=y 107 | 108 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 109 | # tab). 110 | indent-string=' ' 111 | 112 | # Set the linting for string quotes 113 | string-quote=single 114 | triple-quote=double 115 | docstring-quote=double 116 | 117 | 118 | [TOKENS] 119 | 120 | # Number of spaces of indent required when the last token on the preceding line 121 | # is an open (, [, or {. 122 | indent-after-paren=4 123 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/github", 6 | ["@semantic-release/exec", { 7 | "prepareCmd" : "python ./scripts/set_version.py ${nextRelease.version}", 8 | "publishCmd" : "python ./scripts/publish_package.py" 9 | }] 10 | ] 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | - "3.8" 5 | - "3.9" 6 | 7 | # for semantic-release 8 | before_install: 9 | - nvm install 12 10 | - nvm use 12 11 | 12 | install: 13 | - pip install -U importlib-metadata 14 | - pip install -U pluggy 15 | - pip install -r requirements-dev.txt 16 | 17 | script: 18 | - ./scripts/run_lint.sh 19 | - ./scripts/run_tests.sh 20 | # - 'if [ $AWS_ACCESS_KEY_ID ]; then ./scripts/run_acceptance_tests.sh; fi' 21 | 22 | jobs: 23 | include: 24 | - stage: build-and-deploy 25 | provider: script 26 | python: 27 | - "3.8" 28 | nodejs: 29 | - "12" 30 | edge: true 31 | script: 32 | - ./scripts/publish.sh 33 | 34 | stages: 35 | - Test 36 | - name: build-and-deploy 37 | if: branch = master AND type = push 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@epsagon.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Epsagon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py *.md *.txt LICENSE 2 | recursive-include epsagon 3 | -------------------------------------------------------------------------------- /acceptance/acceptance.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pytest 4 | import boto3 5 | 6 | SERVICE_PREFIX = 'epsagon-acceptance-{}-{}'.format( 7 | os.environ.get('TRAVIS_BUILD_NUMBER', ''), 8 | os.environ.get('runtimeName', '') 9 | ) 10 | 11 | 12 | def invoke(name, payload): 13 | """ 14 | invokes a lambda 15 | :param name: the name of the lambda to invoke 16 | :param payload: the payload 17 | :return: the response 18 | """ 19 | lambda_client = boto3.client('lambda', region_name='us-east-1') 20 | return lambda_client.invoke( 21 | FunctionName='{}-{}'.format(SERVICE_PREFIX, name), 22 | InvocationType='RequestResponse', 23 | Payload=payload 24 | ) 25 | 26 | 27 | class TestLambdaWrapper: 28 | @pytest.mark.parametrize("input", [ 29 | '', 30 | '{afwe', 31 | [], 32 | [1, 2, 3], 33 | {}, 34 | {'test': 'test'}, 35 | {'test': 'test', 'more': [1, 2, '3']}, 36 | ]) 37 | def test_sanity_valid_input(self, input): 38 | response = invoke('sanity', json.dumps(input)) 39 | assert response['StatusCode'] == 200 40 | content = json.loads(response['Payload'].read()) 41 | assert content['statusCode'] == 200 42 | body = json.loads(content['body']) 43 | assert body['input'] == input 44 | 45 | @pytest.mark.parametrize("input", [ 46 | '', 47 | '{afwe', 48 | [], 49 | [1, 2, 3], 50 | {}, 51 | {'test': 'test'}, 52 | {'test': 'test', 'more': [1, 2, '3']}, 53 | ]) 54 | def test_labels(self, input): 55 | response = invoke('labels', json.dumps(input)) 56 | assert response['StatusCode'] == 200 57 | content = json.loads(response['Payload'].read()) 58 | assert content['statusCode'] == 200 59 | body = json.loads(content['body']) 60 | assert body['input'] == input 61 | 62 | @pytest.mark.parametrize("input", [ 63 | '', 64 | '{afwe', 65 | [], 66 | [1, 2, 3], 67 | {}, 68 | {'test': 'test'}, 69 | {'test': 'test', 'more': [1, 2, '3']}, 70 | ]) 71 | def test_logging(self, input): 72 | response = invoke('logging', json.dumps(input)) 73 | assert response['StatusCode'] == 200 74 | content = json.loads(response['Payload'].read()) 75 | assert content['statusCode'] == 200 76 | body = json.loads(content['body']) 77 | assert body['input'] == input 78 | -------------------------------------------------------------------------------- /acceptance/lambda-handlers/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | *.pyc 4 | env/ 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | 20 | # Serverless directories 21 | .serverless 22 | -------------------------------------------------------------------------------- /acceptance/lambda-handlers/bad_events.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": "" 3 | } -------------------------------------------------------------------------------- /acceptance/lambda-handlers/handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Epsagon Acceptance Tests 3 | """ 4 | import platform 5 | import json 6 | import logging 7 | import epsagon 8 | 9 | logging.getLogger().setLevel(logging.INFO) 10 | 11 | 12 | epsagon.init( 13 | token='acceptance-test', 14 | app_name='acceptance', 15 | ) 16 | 17 | 18 | @epsagon.lambda_wrapper 19 | def sanity(event, _): 20 | """ 21 | Basic test, using the Epsagon lambda-wrapper 22 | :param event: events args 23 | :param _: context, unused 24 | :return: Success indication 25 | """ 26 | body = { 27 | 'message': 'Epsagon: General Acceptance Test (py {})'.format( 28 | platform.python_version() 29 | ), 30 | 'input': event 31 | } 32 | 33 | response = { 34 | 'statusCode': 200, 35 | 'body': json.dumps(body) 36 | } 37 | 38 | return response 39 | 40 | 41 | @epsagon.lambda_wrapper 42 | def labels(event, _): 43 | """ 44 | Test the usage of labels 45 | :param event: event args 46 | :param _: context, unused 47 | :return: Success indication 48 | """ 49 | body = { 50 | 'message': 'Epsagon: Labels Acceptance Test (py {})'.format( 51 | platform.python_version() 52 | ), 53 | 'input': event 54 | } 55 | 56 | response = { 57 | 'statusCode': 200, 58 | 'body': json.dumps(body) 59 | } 60 | 61 | epsagon.label('label-key', 'label-value') 62 | epsagon.label(None, None) 63 | epsagon.label('label-key', 12) 64 | epsagon.label(12, 12) 65 | epsagon.label(12, None) 66 | epsagon.label('12', None) 67 | 68 | return response 69 | 70 | 71 | @epsagon.lambda_wrapper 72 | def logging_test(event, _): 73 | """ 74 | Basic test, using the Epsagon lambda-wrapper 75 | :param event: events args 76 | :param _: context, unused 77 | :return: Success indication 78 | """ 79 | body = { 80 | 'message': 'Epsagon: General Acceptance Test (py {})'.format( 81 | platform.python_version() 82 | ), 83 | 'input': event 84 | } 85 | logging.info(event) 86 | 87 | response = { 88 | 'statusCode': 200, 89 | 'body': json.dumps(body) 90 | } 91 | 92 | return response 93 | -------------------------------------------------------------------------------- /acceptance/lambda-handlers/requirements.txt: -------------------------------------------------------------------------------- 1 | file:../../../epsagon-python -------------------------------------------------------------------------------- /acceptance/lambda-handlers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: epsagon-acceptance 2 | 3 | provider: 4 | name: aws 5 | runtime: ${opt:runtime} 6 | region: ${opt:region, 'us-east-1'} 7 | stage: ${self:custom.buildNumber}-${opt:runtimeName} 8 | environment: 9 | STAGE: dev 10 | EPSAGON_DEBUG: "TRUE" 11 | package: 12 | exclude: 13 | - './**' 14 | - 'node_modules/**' 15 | 16 | custom: 17 | buildNumber: ${opt:buildNumber} 18 | pythonRequirements: 19 | dockerizePip: non-linux 20 | dockerSsh: true 21 | 22 | functions: 23 | sanity: 24 | handler: handler.sanity 25 | labels: 26 | handler: handler.labels 27 | logging: 28 | handler: handler.logging_test 29 | 30 | plugins: 31 | - serverless-python-requirements 32 | -------------------------------------------------------------------------------- /acceptance/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd acceptance/lambda-handlers 3 | 4 | build_num=$1 5 | result=0 6 | version=$2 7 | function run_acceptance_test() { 8 | runtime=$1 9 | runtimeName=$2 10 | echo "deploying of ${runtime} [build: ${build_num}]" 11 | serverless deploy --runtime ${runtime} --runtimeName ${runtimeName} --buildNumber ${build_num} || { echo "deployment of ${runtime} [build: ${build_num}] failed" ; result=1; } 12 | TRAVIS_BUILD_NUMBER=${build_num} runtimeName=${runtimeName} pytest ../acceptance.py || { echo "tests ${runtime} [build: ${build_num}] failed" ; result=1; } 13 | serverless remove --runtime ${runtime} --runtimeName ${runtimeName} --buildNumber ${build_num} 14 | } 15 | 16 | run_acceptance_test python${version} py${version//.} 17 | 18 | cd - 19 | exit ${result} 20 | -------------------------------------------------------------------------------- /epsagon/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Epsagon's init. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import os 7 | from .utils import init, print_debug 8 | from .patcher import patch_all 9 | from .constants import __version__, EPSAGON_HANDLER 10 | from .trace import trace_factory 11 | from .wrappers.custom import measure 12 | 13 | if os.getenv(EPSAGON_HANDLER): 14 | from .handler import wrapper 15 | 16 | 17 | def auto_load(_): 18 | """ 19 | Called when setting `AUTOWRAPT_BOOTSTRAP=epsagon` (auto-tracing). 20 | """ 21 | print_debug('Initialized using auto-tracing') 22 | init() 23 | 24 | 25 | def dummy_wrapper(func): 26 | """ 27 | A dummy wrapper for when Epsagon is disabled 28 | :param func: The function to wrap 29 | :return: The same function, unchanged 30 | """ 31 | return func 32 | 33 | 34 | def dummy_python_wrapper(*args, **_kwargs): 35 | """ 36 | A dummy wrapper for when Epsagon is disabled. 37 | Used for general python functions 38 | :return: The same function, unchanged 39 | """ 40 | def _inner_wrapper(func): 41 | return func 42 | 43 | if len(args) == 1 and callable(args[0]): 44 | return _inner_wrapper(args[0]) 45 | 46 | return _inner_wrapper 47 | 48 | 49 | if ( 50 | os.getenv('DISABLE_EPSAGON') and 51 | os.getenv('DISABLE_EPSAGON').upper() == 'TRUE' 52 | ): 53 | os.environ['DISABLE_EPSAGON_PATCH'] = 'TRUE' 54 | lambda_wrapper = dummy_wrapper # pylint: disable=C0103 55 | step_lambda_wrapper = dummy_wrapper # pylint: disable=C0103 56 | chalice_wrapper = dummy_wrapper # pylint: disable=C0103 57 | azure_wrapper = dummy_wrapper # pylint: disable=C0103 58 | python_wrapper = dummy_python_wrapper # pylint: disable=C0103 59 | flask_wrapper = dummy_wrapper # pylint: disable=C0103 60 | gcp_wrapper = dummy_wrapper # pylint: disable=C0103 61 | tencent_function_wrapper = dummy_wrapper # pylint: disable=C0103 62 | else: 63 | # Environments. 64 | from .wrappers import ( 65 | lambda_wrapper, 66 | step_lambda_wrapper, 67 | chalice_wrapper, 68 | azure_wrapper, 69 | python_wrapper, 70 | gcp_wrapper, 71 | tencent_function_wrapper 72 | ) 73 | 74 | # Frameworks. 75 | try: 76 | from .wrappers.flask import FlaskWrapper as flask_wrapper 77 | except ImportError: 78 | flask_wrapper = dummy_wrapper 79 | 80 | 81 | # pylint: disable=C0103 82 | label = trace_factory.add_label 83 | error = trace_factory.set_error 84 | warning = trace_factory.set_warning 85 | disable = trace_factory.disable 86 | enable = trace_factory.enable 87 | get_trace_url = trace_factory.get_trace_url 88 | 89 | 90 | __all__ = [ 91 | 'lambda_wrapper', 92 | 'azure_wrapper', 93 | 'python_wrapper', 94 | 'init', 95 | 'step_lambda_wrapper', 96 | 'flask_wrapper', 97 | 'wrapper', 98 | 'gcp_wrapper', 99 | 'tencent_function_wrapper', 100 | 'chalice_wrapper', 101 | 'auto_load', 102 | 'measure', 103 | ] 104 | 105 | 106 | # The modules are patched only if DISABLE_EPSAGON_PATCH variable is NOT 'TRUE' 107 | if ( 108 | not os.getenv('DISABLE_EPSAGON_PATCH') or 109 | os.getenv('DISABLE_EPSAGON_PATCH').upper() != 'TRUE' 110 | ): 111 | patch_all() 112 | -------------------------------------------------------------------------------- /epsagon/common.py: -------------------------------------------------------------------------------- 1 | """Common objects""" 2 | 3 | 4 | class ErrorCode(object): 5 | """ 6 | Error codes enum 7 | """ 8 | OK = 0 9 | ERROR = 1 10 | EXCEPTION = 2 11 | TIMEOUT = 3 12 | 13 | 14 | class EpsagonWarning(Warning): 15 | """ 16 | An Epsagon warning. 17 | """ 18 | -------------------------------------------------------------------------------- /epsagon/constants.py: -------------------------------------------------------------------------------- 1 | """General constants""" 2 | 3 | import os 4 | import time 5 | 6 | __version__ = '0.0.0' 7 | 8 | DEFAULT_REGION = 'us-east-1' 9 | REGION = os.getenv('AWS_REGION', DEFAULT_REGION) 10 | 11 | TRACE_COLLECTOR_URL = '{protocol}{region}.tc.epsagon.com' 12 | COLD_START = True 13 | COLD_START_TIME = time.time() 14 | 15 | DEBUG_MODE = ((os.getenv('EPSAGON_DEBUG') or '').upper() == 'TRUE') 16 | 17 | # Indicates whether to skip collection of http client response payload 18 | SKIP_HTTP_CLIENT_RESPONSE = ( 19 | os.getenv('EPSAGON_SKIP_HTTP_RESPONSE', 'false').lower() == 'true' 20 | ) 21 | 22 | # Customer original handler. 23 | EPSAGON_HANDLER = 'EPSAGON_HANDLER' 24 | 25 | DEFAULT_SEND_TIMEOUT_MS = 1000 26 | 27 | TIMEOUT_GRACE_TIME_MS = int(os.getenv( 28 | 'EPSAGON_LAMBDA_TIMEOUT_THRESHOLD_MS', 29 | str(DEFAULT_SEND_TIMEOUT_MS) 30 | )) 31 | # How long we try to send traces in seconds. 32 | TIMEOUT_ENV = float(os.getenv('EPSAGON_SEND_TIMEOUT_SEC', '0')) 33 | SEND_TIMEOUT = TIMEOUT_ENV if TIMEOUT_ENV else TIMEOUT_GRACE_TIME_MS / 1000.0 34 | 35 | MAX_LABEL_SIZE = 10 * 1024 36 | 37 | DEFAULT_SAMPLE_RATE = 1 38 | 39 | # User-defined HTTP minimum status code to be treated as an error. 40 | HTTP_ERR_CODE = int(os.getenv('EPSAGON_HTTP_ERR_CODE', '500')) 41 | 42 | # List of ignored endpoints for web frameworks. 43 | IGNORED_ENDPOINTS = [] 44 | 45 | # Indicates whether to skip collection of the exception frames part 46 | SHOULD_REMOVE_EXCEPTION_FRAMES = ( 47 | os.getenv('EPSAGON_REMOVE_EXCEPTION_FRAMES', 'false').lower() == 'true' 48 | ) 49 | 50 | EPSAGON_MARKER = '__EPSAGON' 51 | EPSAGON_HEADER = 'epsagon-trace-id' 52 | # In some web frameworks, there is an automated capitalization 53 | # for request headers 54 | EPSAGON_HEADER_TITLE = 'Epsagon-Trace-Id' 55 | 56 | STRONG_KEYS = [ 57 | 'key', 58 | 'request_id', 59 | 'requestid', 60 | 'request-id', 61 | 'steps_dict', 62 | 'message_id', 63 | 'etag', 64 | 'item_hash', 65 | 'sequence_number', 66 | 'trace_id', 67 | 'job_id', 68 | 'activation_id', 69 | 'hostname', 70 | 'virtual_host', 71 | 'region', 72 | 'aws_account', 73 | 'fragment_seq', 74 | 'labels', 75 | 'log_group_name', 76 | 'log_stream_name', 77 | 'cold_start', 78 | ] 79 | 80 | 81 | def is_strong_key(key): 82 | """ 83 | Checks if given key is a strong key 84 | :param key: key 85 | :return: is a strong key 86 | """ 87 | key = key.replace(' ', '_').lower() 88 | for strong_key in STRONG_KEYS: 89 | if strong_key in key: 90 | return True 91 | return False 92 | 93 | 94 | STEP_DICT_NAME = 'Epsagon' 95 | EPSAGON_EVENT_ID_KEY = '_epsagon_event_id' 96 | TRACE_URL_PREFIX = ( 97 | 'https://app.epsagon.com/trace/{id}?timestamp={start_time}' 98 | ) 99 | LAMBDA_TRACE_URL_PREFIX = ( 100 | 'https://app.epsagon.com/functions/{aws_account}/{region}/{function_name}' 101 | '?requestId={request_id}&requestTime={request_time}' 102 | ) 103 | -------------------------------------------------------------------------------- /epsagon/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/epsagon/events/__init__.py -------------------------------------------------------------------------------- /epsagon/events/greengrasssdk.py: -------------------------------------------------------------------------------- 1 | """ 2 | Greengrass events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | from uuid import uuid4 8 | 9 | from epsagon.utils import add_data_if_needed 10 | from ..trace import trace_factory 11 | from ..event import BaseEvent 12 | 13 | 14 | class GreengrassPublishEvent(BaseEvent): 15 | """ 16 | Represents Greengrass publish event. 17 | """ 18 | 19 | ORIGIN = 'greengrasssdk' 20 | RESOURCE_TYPE = 'greengrass' 21 | 22 | # pylint: disable=W0613 23 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 24 | exception): 25 | """ 26 | Initialize. 27 | :param wrapped: wrapt's wrapped 28 | :param instance: wrapt's instance 29 | :param args: wrapt's args 30 | :param kwargs: wrapt's kwargs 31 | :param start_time: Start timestamp (epoch) 32 | :param response: response data 33 | :param exception: Exception (if happened) 34 | """ 35 | super(GreengrassPublishEvent, self).__init__(start_time) 36 | 37 | self.event_id = 'greengrass-{}'.format(str(uuid4())) 38 | 39 | self.resource['name'] = kwargs.get('topic', 'N/A') 40 | self.resource['operation'] = 'publish' 41 | if kwargs.get('queueFullPolicy'): 42 | self.resource['metadata']['aws.greengrass.queueFullPolicy'] = ( 43 | kwargs.get('queueFullPolicy') 44 | ) 45 | 46 | add_data_if_needed( 47 | self.resource['metadata'], 48 | 'aws.greengrass.payload', 49 | kwargs.get('payload') 50 | ) 51 | 52 | if exception is not None: 53 | self.set_exception(exception, traceback.format_exc()) 54 | 55 | 56 | class GreengrassEventFactory(object): 57 | """ 58 | Factory class, generates Greengrass event. 59 | """ 60 | 61 | @staticmethod 62 | def create_event(wrapped, instance, args, kwargs, start_time, response, 63 | exception): 64 | """ 65 | Create an event according to the given api_name. 66 | """ 67 | event = GreengrassPublishEvent( 68 | wrapped, 69 | instance, 70 | args, 71 | kwargs, 72 | start_time, 73 | response, 74 | exception 75 | ) 76 | 77 | trace_factory.add_event(event) 78 | -------------------------------------------------------------------------------- /epsagon/events/httplib2.py: -------------------------------------------------------------------------------- 1 | """ 2 | httplib2 events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | try: 7 | from urllib.parse import urlparse 8 | except ImportError: 9 | from urlparse import urlparse 10 | import traceback 11 | from uuid import uuid4 12 | import json 13 | 14 | from epsagon.utils import add_data_if_needed 15 | from ..trace import trace_factory 16 | from ..event import BaseEvent 17 | from ..http_filters import ( 18 | is_blacklisted_url, 19 | is_payload_collection_blacklisted 20 | ) 21 | from ..utils import update_http_headers 22 | from ..constants import HTTP_ERR_CODE 23 | 24 | 25 | class Httplib2Event(BaseEvent): 26 | """ 27 | Represents base gttplib2 event. 28 | """ 29 | 30 | ORIGIN = 'httplib2' 31 | RESOURCE_TYPE = 'http' 32 | 33 | #pylint: disable=W0613 34 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 35 | exception): 36 | """ 37 | Initialize. 38 | :param wrapped: wrapt's wrapped 39 | :param instance: wrapt's instance 40 | :param args: wrapt's args 41 | :param kwargs: wrapt's kwargs 42 | :param start_time: Start timestamp (epoch) 43 | :param response: response data 44 | :param exception: Exception (if happened) 45 | """ 46 | 47 | super(Httplib2Event, self).__init__(start_time) 48 | self.event_id = 'httplib2-{}'.format(str(uuid4())) 49 | 50 | # Params can be set via args or kwargs. 51 | url, method, body, headers = Httplib2Event.unroller(*args, **kwargs) 52 | 53 | url_obj = urlparse(url) 54 | self.resource['name'] = url_obj.hostname 55 | self.resource['operation'] = method 56 | self.resource['metadata']['url'] = url 57 | 58 | if not is_payload_collection_blacklisted(url): 59 | if headers: 60 | add_data_if_needed( 61 | self.resource['metadata'], 62 | 'request_headers', 63 | headers 64 | ) 65 | 66 | try: 67 | if body: 68 | add_data_if_needed( 69 | self.resource['metadata'], 70 | 'request_body', 71 | json.loads(body) 72 | ) 73 | except (TypeError, ValueError): 74 | # Skip if it is not a JSON body 75 | pass 76 | 77 | if response is not None: 78 | self.update_response(response) 79 | 80 | if exception is not None: 81 | self.set_exception(exception, traceback.format_exc()) 82 | 83 | def update_response(self, response): 84 | """ 85 | Adds response data to event. 86 | :param response: Response from botocore 87 | :return: None 88 | """ 89 | 90 | response_headers, response_body = response 91 | 92 | self.resource['metadata']['status'] = int(response_headers['status']) 93 | self.resource = update_http_headers( 94 | self.resource, 95 | response_headers 96 | ) 97 | 98 | full_url = self.resource['metadata']['url'] 99 | 100 | if not is_payload_collection_blacklisted(full_url): 101 | add_data_if_needed( 102 | self.resource['metadata'], 103 | 'response_headers', 104 | dict(response_headers) 105 | ) 106 | 107 | # Extract only json responses 108 | try: 109 | if response_body: 110 | add_data_if_needed( 111 | self.resource['metadata'], 112 | 'response_body', 113 | json.loads(response_body) 114 | ) 115 | except (TypeError, ValueError): 116 | # Skip if it is not a JSON body 117 | pass 118 | 119 | # Detect errors based on status code 120 | if int(response_headers['status']) >= HTTP_ERR_CODE: 121 | self.set_error() 122 | 123 | @staticmethod 124 | def unroller(uri='N/A', method='N/A', body=None, headers=None): 125 | return uri, method, body, headers 126 | 127 | 128 | class Httplib2EventFactory(object): 129 | """ 130 | Factory class, generates Httplib2 event. 131 | """ 132 | 133 | @staticmethod 134 | def create_event(wrapped, instance, args, kwargs, start_time, response, 135 | exception): 136 | """ 137 | Create an event according to the given api_name. 138 | """ 139 | url, _, _, _ = Httplib2Event.unroller(*args, **kwargs) 140 | 141 | # Detect if URL is blacklisted, and ignore. 142 | if is_blacklisted_url(url): 143 | return 144 | 145 | event = Httplib2Event( 146 | wrapped, 147 | instance, 148 | args, 149 | kwargs, 150 | start_time, 151 | response, 152 | exception 153 | ) 154 | 155 | trace_factory.add_event(event) 156 | -------------------------------------------------------------------------------- /epsagon/events/kafka.py: -------------------------------------------------------------------------------- 1 | """ 2 | kafka-python events. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | from uuid import uuid4 8 | 9 | from epsagon.utils import add_data_if_needed 10 | from ..trace import trace_factory 11 | from ..event import BaseEvent 12 | from ..constants import EPSAGON_HEADER 13 | 14 | 15 | class KafkaEvent(BaseEvent): 16 | """ 17 | Represents base Kafka event. 18 | """ 19 | 20 | ORIGIN = 'kafka' 21 | RESOURCE_TYPE = 'kafka' 22 | 23 | # pylint: disable=W0613 24 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 25 | exception): 26 | """ 27 | Initialize. 28 | :param wrapped: wrapt's wrapped 29 | :param instance: wrapt's instance 30 | :param args: wrapt's args 31 | :param kwargs: wrapt's kwargs 32 | :param start_time: Start timestamp (epoch) 33 | :param response: response data 34 | :param exception: Exception (if happened) 35 | """ 36 | super(KafkaEvent, self).__init__(start_time) 37 | self.event_id = 'kafka-{}'.format(str(uuid4())) 38 | 39 | topic = args[0] 40 | headers = dict(kwargs['headers']) 41 | servers = instance.config['bootstrap_servers'] 42 | if servers and isinstance(servers, list): 43 | # Take the first server if it is a list 44 | servers = servers[0] 45 | 46 | self.resource['name'] = topic 47 | self.resource['operation'] = 'send' 48 | self.resource['metadata'] = { 49 | 'messaging.system': 'kafka', 50 | 'messaging.destination': topic, 51 | 'messaging.url': servers, 52 | 'messaging.message_payload_size_bytes': ( 53 | len(str(kwargs.get('value', ''))) 54 | ), 55 | } 56 | if instance.config.get('client_id'): 57 | self.resource['metadata']['messaging.kafka.client_id'] = ( 58 | instance.config['client_id'] 59 | ) 60 | if headers.get(EPSAGON_HEADER): 61 | self.resource['metadata'][EPSAGON_HEADER] = ( 62 | headers[EPSAGON_HEADER] 63 | ) 64 | if kwargs['key']: 65 | self.resource['metadata']['messaging.kafka.message_key'] = ( 66 | kwargs['key'] 67 | ) 68 | 69 | add_data_if_needed( 70 | self.resource['metadata'], 71 | 'messaging.headers', 72 | headers 73 | ) 74 | 75 | add_data_if_needed( 76 | self.resource['metadata'], 77 | 'messaging.message', 78 | kwargs['value'] 79 | ) 80 | 81 | if getattr(response, 'value', None) is not None: 82 | self.update_response(response.value) 83 | 84 | if exception is not None: 85 | self.set_exception(exception, traceback.format_exc()) 86 | 87 | def update_response(self, response): 88 | """ 89 | Adds response data to event. 90 | :param response: Response from botocore 91 | :return: None 92 | """ 93 | self.resource['metadata']['messaging.kafka.partition'] = ( 94 | response.partition 95 | ) 96 | 97 | 98 | class KafkaEventFactory(object): 99 | """ 100 | Factory class, generates a kafka event. 101 | """ 102 | 103 | @staticmethod 104 | def create_event(wrapped, instance, args, kwargs, start_time, response, 105 | exception): 106 | """Create an event""" 107 | event = KafkaEvent( 108 | wrapped, 109 | instance, 110 | args, 111 | kwargs, 112 | start_time, 113 | response, 114 | exception 115 | ) 116 | 117 | trace_factory.add_event(event) 118 | -------------------------------------------------------------------------------- /epsagon/events/pynamodb.py: -------------------------------------------------------------------------------- 1 | """ 2 | PynamoDB events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from uuid import uuid4 7 | import json 8 | from ..trace import trace_factory 9 | from .botocore import BotocoreDynamoDBEvent 10 | 11 | 12 | class NestedObject(object): 13 | """ 14 | Creating a nested object based on a dict. 15 | """ 16 | def __init__(self, **data): 17 | for k, v in data.items(): 18 | if isinstance(v, dict): 19 | self.__dict__[k] = NestedObject(**v) 20 | else: 21 | self.__dict__[k] = v 22 | 23 | 24 | class PynamoDBVendoredEventAdapter(object): 25 | """ 26 | Factory class, generates PynamoDB event from botocore vendored. 27 | """ 28 | 29 | @staticmethod 30 | def create_event(wrapped, _instance, args, kwargs, start_time, response, 31 | exception): 32 | """Creates DynamoDB event based on PynamoDB data""" 33 | new_args = ( 34 | args[0].headers['X-Amz-Target'].decode('utf-8').split('.')[1], 35 | json.loads(args[0].body.decode('utf-8')) 36 | ) 37 | 38 | new_instance = NestedObject(**{ 39 | 'meta': { 40 | 'region_name': args[0].url.split('.')[1] 41 | } 42 | }) 43 | 44 | new_response = { 45 | 'ResponseMetadata': { 46 | 'RequestId': response.headers['x-amzn-requestid'], 47 | 'HTTPStatusCode': response.status_code, 48 | 'RetryAttempts': None, 49 | }, 50 | } 51 | new_response.update(response.json()) 52 | event = BotocoreDynamoDBEvent( 53 | wrapped, 54 | new_instance, 55 | new_args, 56 | kwargs, 57 | start_time, 58 | new_response, 59 | exception 60 | ) 61 | event.origin = 'pynamodb' 62 | event.resource['metadata'].pop('Retry Attempts') 63 | 64 | trace_factory.add_event(event) 65 | 66 | 67 | class PynamoDBEventAdapter(object): 68 | """ 69 | Factory class, generates PynamoDB event from pynamodb module. 70 | """ 71 | 72 | @staticmethod 73 | def create_event(wrapped, instance, args, kwargs, start_time, response, 74 | exception): 75 | """Creates DynamoDB event based on PynamoDB data""" 76 | new_response = { 77 | 'ResponseMetadata': { 78 | 'RequestId': 'pynamodb-{}'.format(str(uuid4())), 79 | 'HTTPStatusCode': 200 if exception is None else 500, 80 | 'RetryAttempts': None, 81 | }, 82 | } 83 | if response: 84 | new_response.update(response) 85 | event = BotocoreDynamoDBEvent( 86 | wrapped, 87 | instance.client, 88 | args[:2], 89 | kwargs, 90 | start_time, 91 | new_response, 92 | exception 93 | ) 94 | event.origin = 'pynamodb' 95 | event.resource['metadata'].pop('Retry Attempts') 96 | 97 | trace_factory.add_event(event) 98 | -------------------------------------------------------------------------------- /epsagon/events/pyqldb.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyqldb events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from uuid import uuid4 7 | import traceback 8 | 9 | from epsagon.utils import add_data_if_needed 10 | from ..event import BaseEvent 11 | from ..trace import trace_factory 12 | 13 | 14 | class QldbEvent(BaseEvent): 15 | """ 16 | Represents base pyqldb event. 17 | """ 18 | 19 | ORIGIN = 'qldb' 20 | RESOURCE_TYPE = 'qldb' 21 | 22 | #pylint: disable=W0613 23 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 24 | exception): 25 | """ 26 | Initialize. 27 | :param wrapped: wrapt's wrapped 28 | :param instance: wrapt's instance 29 | :param args: wrapt's args 30 | :param kwargs: wrapt's kwargs 31 | :param start_time: Start timestamp (epoch) 32 | :param response: response data 33 | :param exception: Exception (if happened) 34 | """ 35 | super(QldbEvent, self).__init__(start_time) 36 | 37 | self.event_id = 'qldb-{}'.format(str(uuid4())) 38 | self.resource['name'] = \ 39 | getattr(instance.__getattribute__('_transaction')._session,# pylint: disable=W0212 40 | '_ledger_name') 41 | self.resource['operation'] = wrapped.__func__.__name__ 42 | 43 | self.resource['metadata']['query'] = args[0] 44 | add_data_if_needed(self.resource['metadata'], 'parameters', 45 | [args[i] for i in range(1, len(args))]) 46 | 47 | add_data_if_needed(self.resource['metadata'], 'transaction_id', 48 | getattr(instance, 'transaction_id')) 49 | 50 | if response is not None: 51 | self.update_response(response) 52 | 53 | if exception is not None: 54 | self.set_exception(exception, traceback.format_exc()) 55 | 56 | 57 | def update_response(self, response): 58 | """ 59 | Adds response data to event. 60 | :param response: Response from botocore 61 | :return: None 62 | """ 63 | 64 | self.resource['metadata']['Results'] = [str(x) for x in response] 65 | self.resource['metadata']['response.consumed_information'] = \ 66 | response.get_consumed_ios() 67 | self.resource['metadata']['response.timing_information'] = \ 68 | response.get_timing_information() 69 | 70 | 71 | 72 | class QldbEventFactory(object): 73 | """ 74 | Factory class, generates Qldb event. 75 | """ 76 | 77 | @staticmethod 78 | def create_event(wrapped, instance, args, kwargs, start_time, response, 79 | exception): 80 | """ 81 | Create a Qldb event. 82 | :param wrapped: 83 | :param instance: 84 | :param args: 85 | :param kwargs: 86 | :param start_time: 87 | :param response: 88 | :param exception: 89 | :return: 90 | """ 91 | event = QldbEvent( 92 | wrapped, 93 | instance, 94 | args, 95 | kwargs, 96 | start_time, 97 | response, 98 | exception 99 | ) 100 | trace_factory.add_event(event) 101 | -------------------------------------------------------------------------------- /epsagon/events/qcloud_cos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cloud Object Storage events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from uuid import uuid4 7 | import traceback 8 | 9 | from ..event import BaseEvent 10 | from ..trace import trace_factory 11 | 12 | 13 | class COSEvent(BaseEvent): 14 | """ 15 | Represents base Cloud Object Storage event. 16 | """ 17 | ORIGIN = 'tencent-cos' 18 | RESOURCE_TYPE = 'cos' 19 | 20 | # pylint: disable=W0613 21 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 22 | exception): 23 | """ 24 | Initialize the Cloud Object Storage event 25 | :param wrapped: wrapt's wrapped 26 | :param instance: wrapt's instance 27 | :param args: wrapt's args 28 | :param kwargs: wrapt's kwargs 29 | :param start_time: Start timestamp (epoch) 30 | :param response: response data 31 | :param exception: Exception (if happened) 32 | """ 33 | 34 | super(COSEvent, self).__init__(start_time) 35 | self.event_id = 'cos-{}'.format(str(uuid4())) 36 | self.resource['name'] = kwargs['bucket'] 37 | self.resource['operation'] = kwargs['method'] 38 | self.resource['metadata'] = { 39 | # pylint: disable=protected-access 40 | 'tencent.region': instance._conf._region, 41 | 'tencent.cos.object_key': kwargs['url'].split('myqcloud.com/')[-1], 42 | } 43 | if response: 44 | self.resource['metadata'].update({ 45 | 'tencent.cos.request_id': response.headers['x-cos-request-id'], 46 | 'tencent.status_code': response.status_code, 47 | }) 48 | 49 | if exception is not None: 50 | self.resource['metadata'].update({ 51 | 'tencent.cos.request_id': exception.get_request_id(), 52 | 'tencent.status_code': exception.get_status_code(), 53 | }) 54 | self.set_exception(exception, traceback.format_exc()) 55 | 56 | 57 | class COSEventFactory(object): 58 | """ 59 | Factory class, generates Cloud Object Storage event. 60 | """ 61 | @staticmethod 62 | def create_event(wrapped, instance, args, kwargs, start_time, response, 63 | exception): 64 | """ 65 | Create a Cloud Object Storage event. 66 | """ 67 | trace_factory.add_event(COSEvent( 68 | wrapped, 69 | instance, 70 | args, 71 | kwargs, 72 | start_time, 73 | response, 74 | exception 75 | )) 76 | -------------------------------------------------------------------------------- /epsagon/events/requests.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests events module. 3 | Currently it instruments only botocore.vendored.requests. 4 | For regular requests lib, we use urllib3 5 | """ 6 | 7 | from __future__ import absolute_import 8 | import traceback 9 | import json 10 | from uuid import uuid4 11 | 12 | from epsagon.utils import add_data_if_needed 13 | from ..trace import trace_factory 14 | from ..event import BaseEvent 15 | from ..http_filters import is_blacklisted_url 16 | from ..utils import update_http_headers, normalize_http_url 17 | from ..constants import ( 18 | HTTP_ERR_CODE, 19 | EPSAGON_HEADER, 20 | SKIP_HTTP_CLIENT_RESPONSE, 21 | ) 22 | 23 | 24 | class RequestsEvent(BaseEvent): 25 | """ 26 | Represents base requests event. 27 | """ 28 | 29 | ORIGIN = 'requests' 30 | RESOURCE_TYPE = 'http' 31 | 32 | # pylint: disable=W0613 33 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 34 | exception): 35 | """ 36 | Initialize. 37 | :param wrapped: wrapt's wrapped 38 | :param instance: wrapt's instance 39 | :param args: wrapt's args 40 | :param kwargs: wrapt's kwargs 41 | :param start_time: Start timestamp (epoch) 42 | :param response: response data 43 | :param exception: Exception (if happened) 44 | """ 45 | super(RequestsEvent, self).__init__(start_time) 46 | 47 | self.event_id = 'requests-{}'.format(str(uuid4())) 48 | 49 | prepared_request = args[0] 50 | self.resource['name'] = normalize_http_url(prepared_request.url) 51 | self.resource['operation'] = prepared_request.method 52 | self.resource['metadata']['url'] = prepared_request.url 53 | 54 | add_data_if_needed( 55 | self.resource['metadata'], 56 | 'request_headers', 57 | dict(prepared_request.headers) 58 | ) 59 | 60 | epsagon_trace_id = prepared_request.headers.get(EPSAGON_HEADER) 61 | # Make sure trace ID is present in case headers will be removed. 62 | if epsagon_trace_id: 63 | self.resource['metadata']['http_trace_id'] = epsagon_trace_id 64 | 65 | add_data_if_needed( 66 | self.resource['metadata'], 67 | 'request_body', 68 | prepared_request.body 69 | ) 70 | 71 | if response is not None: 72 | self.update_response(response, kwargs.get('stream', False)) 73 | 74 | if exception is not None: 75 | self.set_exception(exception, traceback.format_exc()) 76 | 77 | @staticmethod 78 | def _get_response_body(response, is_stream): 79 | """ 80 | Gets the response body from the response 81 | :param response: the Response object 82 | :param is_stream: the param value as given to the original request 83 | :return: the response body, None on failure 84 | """ 85 | try: 86 | if is_stream: 87 | data = response.raw.peek() 88 | else: 89 | data = response.content 90 | except Exception: # pylint: disable=broad-except 91 | return None 92 | 93 | if data: 94 | try: 95 | data = json.loads(data) 96 | except ValueError: 97 | if isinstance(data, bytes): 98 | try: 99 | data = data.decode('utf-8') 100 | except UnicodeDecodeError: 101 | data = str(data) 102 | return data 103 | 104 | def update_response(self, response, is_stream): 105 | """ 106 | Adds response data to event. 107 | :param response: the Response object 108 | :return: None 109 | """ 110 | 111 | self.resource['metadata']['status_code'] = response.status_code 112 | self.resource = update_http_headers( 113 | self.resource, 114 | response.headers 115 | ) 116 | 117 | add_data_if_needed( 118 | self.resource['metadata'], 119 | 'response_headers', 120 | dict(response.headers) 121 | ) 122 | if ( 123 | not trace_factory.metadata_only and 124 | not SKIP_HTTP_CLIENT_RESPONSE 125 | ): 126 | add_data_if_needed( 127 | self.resource['metadata'], 128 | 'response_body', 129 | type(self)._get_response_body(response, is_stream) 130 | ) 131 | 132 | # Detect errors based on status code 133 | if response.status_code >= HTTP_ERR_CODE: 134 | self.set_error() 135 | 136 | 137 | class RequestsEventFactory(object): 138 | """ 139 | Factory class, generates requests event. 140 | """ 141 | 142 | @staticmethod 143 | def create_event(wrapped, instance, args, kwargs, start_time, response, 144 | exception): 145 | """ 146 | Create an event according to the given api_name. 147 | """ 148 | prepared_request = args[0] 149 | # Detect if URL is blacklisted, and ignore. 150 | if is_blacklisted_url(prepared_request.url): 151 | return 152 | 153 | event = RequestsEvent( 154 | wrapped, 155 | instance, 156 | args, 157 | kwargs, 158 | start_time, 159 | response, 160 | exception 161 | ) 162 | 163 | trace_factory.add_event(event) 164 | -------------------------------------------------------------------------------- /epsagon/events/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | """ 2 | sqlalchemy events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | from uuid import uuid4 8 | 9 | from ..trace import trace_factory 10 | from ..event import BaseEvent 11 | from ..utils import database_connection_type 12 | 13 | 14 | class SqlAlchemyEvent(BaseEvent): 15 | """ 16 | Represents base SqlAlchemy event. 17 | """ 18 | 19 | ORIGIN = 'sqlalchemy' 20 | RESOURCE_TYPE = 'database' 21 | 22 | # pylint: disable=W0613 23 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 24 | exception, operation): 25 | """ 26 | Initialize. 27 | :param wrapped: wrapt's wrapped 28 | :param instance: wrapt's instance 29 | :param args: wrapt's args 30 | :param kwargs: wrapt's kwargs 31 | :param start_time: Start timestamp (epoch) 32 | :param response: response data 33 | :param exception: Exception (if happened) 34 | :param operation: sqlalchemy operation 35 | """ 36 | super(SqlAlchemyEvent, self).__init__(start_time) 37 | 38 | self.event_id = 'sqlalchemy-{}'.format(str(uuid4())) 39 | 40 | self.resource['name'] = ( 41 | instance.bind.url.database or instance.bind.url.host 42 | ) 43 | self.resource['operation'] = operation 44 | 45 | # override event type with the specific DB type 46 | self.resource['type'] = database_connection_type( 47 | instance.bind.url.host, 48 | self.RESOURCE_TYPE 49 | ) 50 | 51 | if exception is not None: 52 | self.set_exception(exception, traceback.format_exc()) 53 | 54 | 55 | class SqlAlchemyEventFactory(object): 56 | """ 57 | Factory class, generates sqlalchemy event. 58 | """ 59 | 60 | OPERATION_MAPPING = { 61 | '__init__': 'initialize', 62 | 'close': 'close', 63 | } 64 | 65 | @staticmethod 66 | def create_event(wrapped, instance, args, kwargs, start_time, response, 67 | exception): 68 | """ 69 | Create sqlalchemy initialize/close event. 70 | """ 71 | operation = SqlAlchemyEventFactory.OPERATION_MAPPING.get( 72 | getattr(wrapped, '__name__') 73 | ) 74 | 75 | if not operation: 76 | return 77 | 78 | event = SqlAlchemyEvent( 79 | wrapped, 80 | instance, 81 | args, 82 | kwargs, 83 | start_time, 84 | response, 85 | exception, 86 | operation 87 | ) 88 | 89 | trace_factory.add_event(event) 90 | -------------------------------------------------------------------------------- /epsagon/events/urllib.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests events module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | 7 | import traceback 8 | from uuid import uuid4 9 | 10 | from epsagon.utils import add_data_if_needed 11 | from ..trace import trace_factory 12 | from ..event import BaseEvent 13 | from ..http_filters import ( 14 | is_blacklisted_url, 15 | is_payload_collection_blacklisted 16 | ) 17 | from ..utils import update_http_headers, normalize_http_url 18 | from ..constants import HTTP_ERR_CODE 19 | 20 | 21 | class UrllibEvent(BaseEvent): 22 | """ 23 | Represents base requests event. 24 | """ 25 | 26 | ORIGIN = 'urllib' 27 | RESOURCE_TYPE = 'http' 28 | 29 | # pylint: disable=W0613 30 | def __init__(self, wrapped, instance, args, kwargs, start_time, response, 31 | exception): 32 | """ 33 | Initialize. 34 | :param wrapped: wrapt's wrapped 35 | :param instance: wrapt's instance 36 | :param args: wrapt's args 37 | :param kwargs: wrapt's kwargs 38 | :param start_time: Start timestamp (epoch) 39 | :param response: response data 40 | :param exception: Exception (if happened) 41 | """ 42 | 43 | super(UrllibEvent, self).__init__(start_time) 44 | 45 | self.event_id = 'urllib-{}'.format(str(uuid4())) 46 | 47 | prepared_request, data = args 48 | self.resource['name'] = normalize_http_url(prepared_request.full_url) 49 | self.resource['operation'] = prepared_request.get_method() 50 | self.resource['metadata']['url'] = prepared_request.full_url 51 | 52 | if not is_payload_collection_blacklisted(prepared_request.full_url): 53 | add_data_if_needed( 54 | self.resource['metadata'], 55 | 'request_headers', 56 | dict(prepared_request.headers) 57 | ) 58 | 59 | add_data_if_needed( 60 | self.resource['metadata'], 61 | 'request_body', 62 | data 63 | ) 64 | 65 | if response is not None: 66 | self.update_response(response) 67 | 68 | if exception is not None: 69 | self.set_exception(exception, traceback.format_exc()) 70 | 71 | def update_response(self, response): 72 | """ 73 | Adds response data to event. 74 | :param response: Response from botocore 75 | :return: None 76 | """ 77 | 78 | self.resource['metadata']['status_code'] = response.status 79 | headers = dict(response.getheaders()) 80 | self.resource = update_http_headers( 81 | self.resource, 82 | headers 83 | ) 84 | 85 | self.resource['metadata']['response_body'] = None 86 | full_url = self.resource['metadata']['url'] 87 | 88 | if not is_payload_collection_blacklisted(full_url): 89 | add_data_if_needed( 90 | self.resource['metadata'], 91 | 'response_headers', 92 | headers 93 | ) 94 | 95 | # Extract only json responses 96 | try: 97 | add_data_if_needed( 98 | self.resource['metadata'], 99 | 'response_body', 100 | str(response.peek()) 101 | ) 102 | except ValueError: 103 | pass 104 | 105 | # Detect errors based on status code 106 | if response.status >= HTTP_ERR_CODE: 107 | self.set_error() 108 | 109 | 110 | class UrllibEventFactory(object): 111 | """ 112 | Factory class, generates urllib event. 113 | """ 114 | 115 | @staticmethod 116 | def create_event(wrapped, instance, args, kwargs, start_time, response, 117 | exception): 118 | """ 119 | Create an event according to the given api_name. 120 | """ 121 | prepared_request = args[0] 122 | 123 | # Detect if URL is blacklisted, and ignore. 124 | if is_blacklisted_url(prepared_request.full_url): 125 | return 126 | 127 | event = UrllibEvent( 128 | wrapped, 129 | instance, 130 | args, 131 | kwargs, 132 | start_time, 133 | response, 134 | exception 135 | ) 136 | 137 | trace_factory.add_event(event) 138 | -------------------------------------------------------------------------------- /epsagon/handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Epsagon generic wrapper used only in Lambda environments. 3 | """ 4 | 5 | from .utils import init as epsagon_init, import_original_module 6 | from .wrappers import lambda_wrapper 7 | 8 | 9 | def init_module(): 10 | """ 11 | Initialize user's module handler. 12 | :return: wrapper handler. 13 | """ 14 | original_module, module_path, handler_name = import_original_module() 15 | try: 16 | handler = original_module 17 | for name in module_path.split('.')[1:] + [handler_name]: 18 | handler = getattr(handler, name) 19 | return handler 20 | except AttributeError: 21 | raise AttributeError( 22 | 'No handler {} in module {}'.format(handler_name, module_path) 23 | ) 24 | 25 | 26 | epsagon_init() 27 | wrapper = lambda_wrapper(init_module()) 28 | -------------------------------------------------------------------------------- /epsagon/http_filters.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utils for web frameworks request filters. 3 | """ 4 | 5 | from six.moves import urllib 6 | from epsagon.trace import trace_factory 7 | from epsagon.constants import IGNORED_ENDPOINTS 8 | 9 | # Ignored content types for web frameworks. 10 | IGNORED_CONTENT_TYPES = [ 11 | 'image', 12 | 'audio', 13 | 'video', 14 | 'font', 15 | 'zip', 16 | 'css', 17 | ] 18 | IGNORED_FILE_TYPES = [ 19 | '.js', 20 | '.jsx', 21 | '.woff', 22 | '.woff2', 23 | '.ttf', 24 | '.eot', 25 | '.ico', 26 | ] 27 | 28 | FILE_PREFIX = 'file://' 29 | 30 | # Method to URL dict. 31 | BLACKLIST_URLS = { 32 | str.endswith: [ 33 | 'tc.epsagon.com', 34 | '.amazonaws.com', 35 | '.myqcloud.com', 36 | ], 37 | str.__contains__: [ 38 | 'accounts.google.com', 39 | 'documents.azure.com', 40 | '169.254.170.2' # AWS Task Metadata Endpoint 41 | ], 42 | } 43 | WHITELIST_URL = { 44 | str.__contains__: [ 45 | '.execute-api.', 46 | '.elb.amazonaws.com', 47 | '.appsync-api.', 48 | ], 49 | } 50 | 51 | 52 | def is_blacklisted_url(url): 53 | """ 54 | Return whether the URL blacklisted or not. 55 | Using BLACKLIST_URLS methods against the URLs. 56 | :param url: url string 57 | :return: True if URL is blacklisted, else False 58 | """ 59 | if url.startswith(FILE_PREFIX): 60 | return True 61 | 62 | url = urllib.parse.urlparse(url).netloc 63 | for method in WHITELIST_URL: 64 | for whitelist_url in WHITELIST_URL[method]: 65 | if method(url, whitelist_url): 66 | return False 67 | 68 | for method in BLACKLIST_URLS: 69 | for blacklist_url in BLACKLIST_URLS[method]: 70 | if method(url, blacklist_url): 71 | return True 72 | 73 | return False 74 | 75 | 76 | def is_payload_collection_blacklisted(url): 77 | """ 78 | Return whether the payload should be collected according to the blacklisted 79 | urls list in the Trace. 80 | :param url: url string 81 | :return: True if URL is blacklisted, else False 82 | """ 83 | url = urllib.parse.urlparse(url).netloc 84 | if trace_factory.get_trace(): 85 | trace_blacklist_urls = trace_factory.get_trace().url_patterns_to_ignore 86 | else: 87 | trace_blacklist_urls = tuple() 88 | return any(blacklist_url in url for blacklist_url in trace_blacklist_urls) 89 | 90 | 91 | def ignore_request(content, path): 92 | """ 93 | Return true if HTTP request in web frameworks should be omitted. 94 | :param content: accept mimetype header 95 | :param path: request path 96 | :return: Bool 97 | """ 98 | 99 | return ( 100 | any([x in content for x in IGNORED_CONTENT_TYPES]) or 101 | any([path.endswith(x) for x in IGNORED_FILE_TYPES]) 102 | ) 103 | 104 | 105 | def add_ignored_endpoints(endpoints): 106 | """ 107 | add endpoints to the list of ignored ones.. 108 | :param endpoints: list of endpoints or None 109 | :return: None 110 | """ 111 | if endpoints: 112 | IGNORED_ENDPOINTS.extend(endpoints) 113 | 114 | 115 | def is_ignored_endpoint(endpoint): 116 | """ 117 | return true if endpoint should be ignored. 118 | :param endpoint: endpoint path 119 | :return: Bool 120 | """ 121 | return endpoint in IGNORED_ENDPOINTS 122 | -------------------------------------------------------------------------------- /epsagon/modules/MySQLdb.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0103 2 | """ 3 | MySQLdb patcher module 4 | """ 5 | from __future__ import absolute_import 6 | 7 | import wrapt 8 | from .db_wrapper import connect_wrapper 9 | 10 | 11 | def patch(): 12 | """ 13 | patch module. 14 | :return: None 15 | """ 16 | wrapt.wrap_function_wrapper( 17 | 'MySQLdb', 18 | 'connect', 19 | connect_wrapper 20 | ) 21 | wrapt.wrap_function_wrapper( 22 | 'MySQLdb', 23 | 'Connection', 24 | connect_wrapper 25 | ) 26 | wrapt.wrap_function_wrapper( 27 | 'MySQLdb', 28 | 'Connect', 29 | connect_wrapper 30 | ) 31 | -------------------------------------------------------------------------------- /epsagon/modules/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automatically imports all available modules for patch 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import os 7 | import sys 8 | from importlib import import_module 9 | 10 | MODULES = {} 11 | IGNORE_MODULES = ('__init__',) 12 | PYTHON_EXTENSIONS = ('.py', '.pyc') 13 | VERSION_DEPENDENCIES = { 14 | 'fastapi': (3, 5, 3), 15 | } 16 | 17 | for module_name in os.listdir(os.path.dirname(__file__)): 18 | filename, ext = os.path.splitext(module_name) 19 | if filename in IGNORE_MODULES or ext not in PYTHON_EXTENSIONS: 20 | continue 21 | 22 | # Verify that the loaded module meets the minimum Python version 23 | if filename in VERSION_DEPENDENCIES: 24 | if sys.version_info < VERSION_DEPENDENCIES[filename]: 25 | continue 26 | 27 | try: 28 | imported = import_module('.{}'.format(filename), __name__) 29 | MODULES[filename] = imported 30 | except ImportError: 31 | pass 32 | -------------------------------------------------------------------------------- /epsagon/modules/azure.py: -------------------------------------------------------------------------------- 1 | """ 2 | Azure sdk patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.azure import AzureEventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for Azure sdk instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | return wrapper(AzureEventFactory, wrapped, instance, args, kwargs) 21 | 22 | 23 | def patch(): 24 | """ 25 | Patch module. 26 | :return: None 27 | """ 28 | wrapt.wrap_function_wrapper( 29 | 'azure.cosmos.container', 30 | 'ContainerProxy.delete_item', 31 | _wrapper 32 | ) 33 | wrapt.wrap_function_wrapper( 34 | 'azure.cosmos.container', 35 | 'ContainerProxy.upsert_item', 36 | _wrapper 37 | ) 38 | wrapt.wrap_function_wrapper( 39 | 'azure.cosmos.container', 40 | 'ContainerProxy.query_items', 41 | _wrapper 42 | ) 43 | -------------------------------------------------------------------------------- /epsagon/modules/botocore.py: -------------------------------------------------------------------------------- 1 | """ 2 | botocore patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import json 7 | from uuid import uuid4 8 | import wrapt 9 | from epsagon.modules.general_wrapper import wrapper 10 | from epsagon.constants import STEP_DICT_NAME 11 | from ..events.botocore import ( 12 | BotocoreEventFactory, 13 | BotocoreStepFunctionEvent 14 | ) 15 | from .requests import _wrapper as _requests_wrapper 16 | 17 | 18 | def _wrapper(wrapped, instance, args, kwargs): 19 | """ 20 | General wrapper for botocore instrumentation. 21 | :param wrapped: wrapt's wrapped 22 | :param instance: wrapt's instance 23 | :param args: wrapt's args 24 | :param kwargs: wrapt's kwargs 25 | :return: None 26 | """ 27 | instance_type = instance.__class__.__name__.lower() 28 | if instance_type == BotocoreStepFunctionEvent.RESOURCE_TYPE: 29 | handle_stepfunc_args(args) 30 | 31 | return wrapper(BotocoreEventFactory, wrapped, instance, args, kwargs) 32 | 33 | 34 | def add_steps_dict_to_request(request_args, params_property_name): 35 | machine_input = json.loads(request_args[params_property_name]) 36 | machine_input[STEP_DICT_NAME] = {'id': str(uuid4()), 'step_num': -1} 37 | request_args[params_property_name] = json.dumps(machine_input) 38 | 39 | 40 | def handle_stepfunc_args(args): 41 | try: 42 | event_operation, request_args = args 43 | 44 | if event_operation == 'StartExecution': 45 | add_steps_dict_to_request(request_args, 'input') 46 | elif event_operation == 'SendTaskSuccess': 47 | add_steps_dict_to_request(request_args, 'output') 48 | except Exception: # pylint: disable=broad-except 49 | pass 50 | 51 | 52 | def patch(): 53 | """ 54 | Patch module. 55 | :return: None 56 | """ 57 | 58 | wrapt.wrap_function_wrapper( 59 | 'botocore.client', 60 | 'BaseClient._make_api_call', 61 | _wrapper 62 | ) 63 | 64 | # botocore no longer vendor requests in new version 65 | # https://github.com/boto/botocore/pull/1829 66 | wrapt.wrap_function_wrapper( 67 | 'botocore.vendored.requests', 68 | 'Session.send', 69 | _requests_wrapper 70 | ) 71 | -------------------------------------------------------------------------------- /epsagon/modules/celery.py: -------------------------------------------------------------------------------- 1 | """ 2 | Celery patcher module. 3 | This uses the built-in Signals in Celery to get signal for every event 4 | """ 5 | 6 | from __future__ import absolute_import 7 | from importlib import import_module 8 | from ..events.celery import ( 9 | wrap_prerun, 10 | wrap_postrun, 11 | wrap_before_publish, 12 | wrap_after_publish, 13 | wrap_retry, 14 | wrap_failure, 15 | ) 16 | 17 | 18 | def patch(): 19 | """ 20 | Patch module. 21 | :return: None 22 | """ 23 | signals = import_module('celery.signals') 24 | signals.before_task_publish.connect(wrap_before_publish, weak=False) 25 | signals.after_task_publish.connect(wrap_after_publish, weak=False) 26 | signals.task_prerun.connect(wrap_prerun, weak=False) 27 | signals.task_retry.connect(wrap_retry, weak=False) 28 | signals.task_failure.connect(wrap_failure, weak=False) 29 | signals.task_postrun.connect(wrap_postrun, weak=False) 30 | -------------------------------------------------------------------------------- /epsagon/modules/db_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for DB modules 3 | """ 4 | from __future__ import absolute_import 5 | import wrapt 6 | import epsagon.modules.general_wrapper 7 | from ..events.dbapi import DBAPIEventFactory 8 | 9 | 10 | # pylint: disable=abstract-method 11 | class CursorWrapper(wrapt.ObjectProxy): 12 | """ 13 | A dbapi cursor wrapper for tracing 14 | """ 15 | 16 | def __init__(self, cursor, connection_wrapper): 17 | super(CursorWrapper, self).__init__(cursor) 18 | self._self_connection = connection_wrapper 19 | 20 | @property 21 | def connection_wrapper(self): 22 | """ 23 | A property that holds that connection_wrapper. 24 | :return: the connection wrapper. 25 | """ 26 | return self._self_connection 27 | 28 | # NOTE: tracing other API calls currently not supported 29 | # (as 'executemany' and 'callproc') 30 | def execute(self, *args, **kwargs): 31 | """ 32 | Execute the query. 33 | :param args: args. 34 | :param kwargs: kwargs. 35 | """ 36 | epsagon.modules.general_wrapper.wrapper( 37 | DBAPIEventFactory, 38 | self.__wrapped__.execute, 39 | self, 40 | args, 41 | kwargs, 42 | ) 43 | 44 | def __enter__(self): 45 | # raise appropriate error if api not supported (should reach the user) 46 | self.__wrapped__.__enter__ # pylint: disable=W0104 47 | 48 | return self 49 | 50 | 51 | class ConnectionWrapper(wrapt.ObjectProxy): 52 | """ 53 | A dbapi connection wrapper for tracing. 54 | """ 55 | def __init__(self, connection, args, kwargs): 56 | super(ConnectionWrapper, self).__init__(connection) 57 | self._self_args = args 58 | self._self_kwargs = kwargs 59 | 60 | def cursor(self, *args, **kwargs): 61 | """ 62 | Return cursor wrapper. 63 | :param args: args. 64 | :param kwargs: kwargs. 65 | :return: Cursorwrapper. 66 | """ 67 | cursor = self.__wrapped__.cursor(*args, **kwargs) 68 | return CursorWrapper(cursor, self) 69 | 70 | @property 71 | def extract_hostname(self): 72 | """ 73 | A property that extract the host name 74 | :return: the host name 75 | """ 76 | return self._self_kwargs.get('host', 'local') 77 | 78 | @property 79 | def extract_dbname(self): 80 | """ 81 | A property that extract the db name 82 | :return: the db name 83 | """ 84 | return self._self_kwargs.get( 85 | 'db', 86 | self._self_kwargs.get( 87 | 'database', 88 | '' 89 | ) 90 | ) 91 | 92 | 93 | #pylint: disable=W0613 94 | def connect_wrapper(wrapped, instance, args, kwargs): 95 | """ 96 | connect wrapper for psycopg2 instrumentation 97 | :param wrapped: wrapt's wrapped 98 | :param instance: wrapt's instance 99 | :param args: wrapt's args 100 | :param kwargs: wrapt's kwargs 101 | :return: None 102 | """ 103 | connection = wrapped(*args, **kwargs) 104 | return ConnectionWrapper(connection, args, kwargs) 105 | -------------------------------------------------------------------------------- /epsagon/modules/django.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | import wrapt 8 | from ..utils import print_debug, is_lambda_env 9 | from ..trace import trace_factory 10 | try: 11 | from django.conf import settings 12 | except ImportError: 13 | settings = None # pylint: disable=invalid-name 14 | 15 | EPSAGON_MIDDLEWARE = 'epsagon.wrappers.django.DjangoMiddleware' 16 | 17 | 18 | def _wrapper(wrapped, _instance, args, kwargs): 19 | """ 20 | Adds `EPSAGON_MIDDLEWARE` into django.conf.settings. 21 | :param wrapped: wrapt's wrapped 22 | :param _instance: wrapt's instance 23 | :param args: wrapt's args 24 | :param kwargs: wrapt's kwargs 25 | """ 26 | 27 | # Skip on Lambda environment since it's not relevant and might be duplicate 28 | if is_lambda_env(): 29 | return wrapped(*args, **kwargs) 30 | 31 | try: 32 | # Extract middleware engine (varying between Django versions) 33 | if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: 34 | # Check if not instrumented already 35 | if EPSAGON_MIDDLEWARE in settings.MIDDLEWARE: 36 | return wrapped(*args, **kwargs) 37 | 38 | # Add Epsagon's middleware to the list 39 | if isinstance(settings.MIDDLEWARE, tuple): 40 | settings.MIDDLEWARE = ( 41 | (EPSAGON_MIDDLEWARE,) + settings.MIDDLEWARE 42 | ) 43 | elif isinstance(settings.MIDDLEWARE, list): 44 | settings.MIDDLEWARE = [EPSAGON_MIDDLEWARE] + settings.MIDDLEWARE 45 | elif ( 46 | hasattr(settings, 'MIDDLEWARE_CLASSES') and 47 | settings.MIDDLEWARE_CLASSES is not None 48 | ): 49 | # Check if not instrumented already 50 | if EPSAGON_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: 51 | return wrapped(*args, **kwargs) 52 | 53 | # Add Epsagon's middleware to the list 54 | if isinstance(settings.MIDDLEWARE_CLASSES, tuple): 55 | settings.MIDDLEWARE = ( 56 | (EPSAGON_MIDDLEWARE,) + settings.MIDDLEWARE_CLASSES 57 | ) 58 | elif isinstance(settings.MIDDLEWARE_CLASSES, list): 59 | settings.MIDDLEWARE = ( 60 | [EPSAGON_MIDDLEWARE] + settings.MIDDLEWARE_CLASSES 61 | ) 62 | except Exception: # pylint: disable=broad-except 63 | print_debug('Could not add Django middleware') 64 | return wrapped(*args, **kwargs) 65 | 66 | 67 | def gunicorn_sync_wrapper(wrapped, _instance, args, kwargs): 68 | """ 69 | Wraps the gunicorn sync worker handle request. 70 | Catches request handling errors and finally sending the trace. 71 | :param wrapped: wrapt's wrapped 72 | :param _instance: wrapt's instance 73 | :param args: wrapt's args 74 | :param kwargs: wrapt's kwargs 75 | """ 76 | 77 | # Skip on Lambda environment since it's not relevant and might be duplicate 78 | # also skip if given invalid number of arguments 79 | if is_lambda_env() or not args or len(args) != 4: 80 | return wrapped(*args, **kwargs) 81 | 82 | try: 83 | # creates the trace on the current thread 84 | trace_factory.switch_to_multiple_traces() 85 | trace_factory.get_or_create_trace() 86 | except Exception as error: # pylint: disable=broad-except 87 | pass 88 | try: 89 | return wrapped(*args, **kwargs) 90 | except StopIteration: 91 | raise 92 | except Exception as error: # pylint: disable=broad-except 93 | trace_factory.set_error(error, traceback.format_exc()) 94 | raise error 95 | finally: 96 | try: 97 | trace_factory.send_traces() 98 | except Exception: # pylint: disable=broad-except 99 | trace_factory.pop_trace() 100 | 101 | 102 | def patch(): 103 | """ 104 | Patch module. 105 | :return: None 106 | """ 107 | wrapt.wrap_function_wrapper( 108 | 'django.core.handlers.base', 109 | 'BaseHandler.load_middleware', 110 | _wrapper 111 | ) 112 | try: 113 | wrapt.wrap_function_wrapper( 114 | 'gunicorn.workers.ggevent', 115 | 'GeventWorker.handle_request', 116 | gunicorn_sync_wrapper 117 | ) 118 | wrapt.wrap_function_wrapper( 119 | 'gunicorn.workers.sync', 120 | 'SyncWorker.handle_request', 121 | gunicorn_sync_wrapper 122 | ) 123 | except Exception: # pylint: disable=broad-except 124 | pass 125 | -------------------------------------------------------------------------------- /epsagon/modules/fastapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | fastapi patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from ..wrappers.fastapi import ( 8 | exception_handler_wrapper, 9 | server_call_wrapper, 10 | route_class_wrapper, 11 | ) 12 | from ..utils import is_lambda_env 13 | 14 | 15 | def _exception_handler_wrapper(wrapped, _instance, args, kwargs): 16 | """ 17 | Wraps the handler given to add_exception_handler. 18 | :param wrapped: wrapt's wrapped 19 | :param instance: wrapt's instance 20 | :param args: wrapt's args 21 | :param kwargs: wrapt's kwargs 22 | """ 23 | # Skip on Lambda environment since it's not relevant and might be duplicate 24 | if is_lambda_env(): 25 | return wrapped(*args, **kwargs) 26 | 27 | if args and len(args) == 2: 28 | args = list(args) 29 | args[1] = exception_handler_wrapper(args[1]) 30 | return wrapped(*args, **kwargs) 31 | 32 | 33 | def patch(): 34 | """ 35 | Patch module. 36 | :return: None 37 | """ 38 | wrapt.wrap_function_wrapper( 39 | 'fastapi.routing', 40 | 'APIRoute.__init__', 41 | route_class_wrapper 42 | ) 43 | wrapt.wrap_function_wrapper( 44 | 'starlette.applications', 45 | 'Starlette.add_exception_handler', 46 | _exception_handler_wrapper 47 | ) 48 | wrapt.wrap_function_wrapper( 49 | 'starlette.middleware.errors', 50 | 'ServerErrorMiddleware.__call__', 51 | server_call_wrapper 52 | ) 53 | -------------------------------------------------------------------------------- /epsagon/modules/flask.py: -------------------------------------------------------------------------------- 1 | """ 2 | flask patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from ..wrappers.flask import FlaskWrapper 8 | from ..utils import print_debug, is_lambda_env 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | Adds `FlaskWrapper` into Flask app. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | """ 19 | # Skip on Lambda environment since it's not relevant and might be duplicate 20 | if is_lambda_env(): 21 | return wrapped(*args, **kwargs) 22 | 23 | response = wrapped(*args, **kwargs) 24 | try: 25 | FlaskWrapper(instance) 26 | except Exception: # pylint: disable=broad-except 27 | print_debug('Could not add Flask wrapper') 28 | return response 29 | 30 | 31 | def patch(): 32 | """ 33 | Patch module. 34 | :return: None 35 | """ 36 | wrapt.wrap_function_wrapper('flask', 'Flask.__init__', _wrapper) 37 | -------------------------------------------------------------------------------- /epsagon/modules/general_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | General wrapper for instrumentation. 3 | """ 4 | 5 | #pylint: disable=W0703 6 | from __future__ import absolute_import 7 | import time 8 | import traceback 9 | from epsagon.trace import trace_factory 10 | 11 | 12 | def wrapper(factory, wrapped, instance, args, kwargs): 13 | """ 14 | General wrapper for instrumentation. 15 | :param factory: Factory class for the event type 16 | :param wrapped: wrapt's wrapped 17 | :param instance: wrapt's instance 18 | :param args: wrapt's args 19 | :param kwargs: wrapt's kwargs 20 | :return: None 21 | """ 22 | 23 | response = None 24 | exception = None 25 | start_time = time.time() 26 | 27 | try: 28 | response = wrapped(*args, **kwargs) 29 | return response 30 | except Exception as operation_exception: 31 | exception = operation_exception 32 | raise 33 | finally: 34 | try: 35 | factory.create_event( 36 | wrapped, 37 | instance, 38 | args, 39 | kwargs, 40 | start_time, 41 | response, 42 | exception 43 | ) 44 | except Exception as instrumentation_exception: 45 | trace_factory.add_exception( 46 | instrumentation_exception, 47 | traceback.format_exc() 48 | ) 49 | -------------------------------------------------------------------------------- /epsagon/modules/greengrasssdk.py: -------------------------------------------------------------------------------- 1 | """ 2 | greengrasssdk patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.greengrasssdk import GreengrassEventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for greengrasssdk instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | return wrapper(GreengrassEventFactory, wrapped, instance, args, kwargs) 21 | 22 | 23 | def patch(): 24 | """ 25 | Patch module. 26 | :return: None 27 | """ 28 | wrapt.wrap_function_wrapper( 29 | 'greengrasssdk.IoTDataPlane', 30 | 'Client.publish', 31 | _wrapper 32 | ) 33 | -------------------------------------------------------------------------------- /epsagon/modules/httplib2.py: -------------------------------------------------------------------------------- 1 | """ 2 | httplib2 patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.httplib2 import Httplib2EventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for httplib2 instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | 21 | return wrapper(Httplib2EventFactory, wrapped, instance, args, kwargs) 22 | 23 | 24 | def patch(): 25 | """ 26 | Patch module. 27 | :return: None 28 | """ 29 | 30 | wrapt.wrap_function_wrapper( 31 | 'httplib2', 32 | 'Http.request', 33 | _wrapper 34 | ) 35 | -------------------------------------------------------------------------------- /epsagon/modules/kafka.py: -------------------------------------------------------------------------------- 1 | """ 2 | kafka-python patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.kafka import KafkaEventFactory 9 | from ..constants import EPSAGON_HEADER 10 | from ..utils import get_epsagon_http_trace_id 11 | 12 | 13 | def _parse_args( 14 | topic, 15 | value=None, 16 | key=None, 17 | headers=None, 18 | partition=None, 19 | timestamp_ms=None 20 | ): 21 | """Sort and return args and kwargs according to the original signature""" 22 | return (topic, ), { 23 | 'value': value, 24 | 'key': key, 25 | 'headers': headers, 26 | 'partition': partition, 27 | 'timestamp_ms': timestamp_ms 28 | } 29 | 30 | 31 | def _wrapper(wrapped, instance, args, kwargs): 32 | """KafkaProducer.send wrapper""" 33 | new_args, new_kwargs = _parse_args(*args, **kwargs) 34 | 35 | # Adds epsagon header only on Kafka record V2. V0/V1 don't support it 36 | # pylint: disable=protected-access 37 | if instance._max_usable_produce_magic() == 2: 38 | if not new_kwargs.get('headers'): 39 | new_kwargs['headers'] = [] 40 | new_kwargs['headers'].append( 41 | (EPSAGON_HEADER, get_epsagon_http_trace_id().encode()) 42 | ) 43 | 44 | return wrapper(KafkaEventFactory, wrapped, instance, new_args, new_kwargs) 45 | 46 | 47 | def patch(): 48 | """ 49 | patch module. 50 | :return: None 51 | """ 52 | wrapt.wrap_function_wrapper( 53 | 'kafka.producer.kafka', 54 | 'KafkaProducer.send', 55 | _wrapper 56 | ) 57 | -------------------------------------------------------------------------------- /epsagon/modules/logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | logging patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | 7 | import json 8 | import os 9 | from functools import partial 10 | 11 | import wrapt 12 | 13 | from ..trace import trace_factory 14 | from ..utils import print_debug, get_trace_log_config 15 | 16 | LOGGING_FUNCTIONS = ( 17 | 'info', 18 | 'debug', 19 | 'error', 20 | 'warning', 21 | 'exception', 22 | 'critical' 23 | ) 24 | 25 | 26 | def _wrapper(wrapped, _instance, args, kwargs): 27 | """ 28 | Wrapper for logging module. 29 | :param wrapped: wrapt's wrapped 30 | :param _instance: wrapt's instance, unused. 31 | :param args: wrapt's args 32 | :param kwargs: wrapt's kwargs 33 | :return: None 34 | """ 35 | if not os.getenv('EPSAGON_DISABLE_LOGGING_ERRORS', '').upper() == 'TRUE': 36 | try: 37 | message = args[0] % args[1:] 38 | trace_factory.set_error(message, from_logs=True) 39 | except Exception: # pylint: disable=broad-except 40 | print_debug('Could not capture exception from log: {}'.format( 41 | args 42 | )) 43 | 44 | return wrapped(*args, **kwargs) 45 | 46 | 47 | def _add_log_id(trace_log_id, msg): 48 | """ 49 | adds log id to the msg 50 | """ 51 | try: 52 | # Check if message is in json format 53 | json_log = json.loads(msg) 54 | json_log['epsagon'] = {'trace_id': trace_log_id} 55 | return json.dumps(json_log) 56 | except Exception: # pylint: disable=broad-except 57 | # message is a regular string, add the ID to the beginning 58 | if not isinstance(msg, str): 59 | msg = str(msg) 60 | return ' '.join([trace_log_id, msg]) 61 | 62 | 63 | def _epsagon_trace_id_wrapper(msg_index, wrapped, _instance, args, kwargs): 64 | """ 65 | Wrapper for logging module. 66 | :param msg_index: the index of the log message in args 67 | (since Logger.log also gets `level`) 68 | :param wrapped: wrapt's wrapped 69 | :param _instance: wrapt's instance, unused. 70 | :param args: wrapt's args 71 | :param kwargs: wrapt's kwargs 72 | :return: None 73 | """ 74 | trace_log_id = trace_factory.get_log_id() 75 | 76 | if not trace_log_id: 77 | return wrapped(*args, **kwargs) 78 | 79 | try: 80 | message = _add_log_id(trace_log_id, args[msg_index]) 81 | except Exception: # pylint: disable=broad-except 82 | # total failure to add log id 83 | return wrapped(*args, **kwargs) 84 | args = ( 85 | args[0:msg_index] + 86 | (message,) + 87 | args[(msg_index + 1):] 88 | ) 89 | return wrapped(*args, **kwargs) 90 | 91 | 92 | def patch(): 93 | """ 94 | Patch module. 95 | :return: None 96 | """ 97 | # Automatically capture exceptions from logging 98 | wrapt.wrap_function_wrapper('logging', 'exception', _wrapper) 99 | wrapt.wrap_function_wrapper('logging', 'Logger.exception', _wrapper) 100 | 101 | # Instrument logging with Epsagon trace ID 102 | if get_trace_log_config(): 103 | wrapt.wrap_function_wrapper( 104 | 'logging', 105 | 'Logger.log', 106 | partial(_epsagon_trace_id_wrapper, 1) 107 | ) 108 | for log_function in LOGGING_FUNCTIONS: 109 | wrapt.wrap_function_wrapper( 110 | 'logging', 111 | 'Logger.{}'.format(log_function), 112 | partial(_epsagon_trace_id_wrapper, 0) 113 | ) 114 | 115 | # Instrument print function is disabled 116 | # wrapt.wrap_function_wrapper( 117 | # 'builtins', 118 | # 'print', 119 | # partial(_epsagon_trace_id_wrapper, 0) 120 | # ) 121 | -------------------------------------------------------------------------------- /epsagon/modules/pg8000.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=C0103 2 | """ 3 | PG8000 patcher module 4 | """ 5 | from __future__ import absolute_import 6 | 7 | import wrapt 8 | from .db_wrapper import connect_wrapper 9 | 10 | 11 | def patch(): 12 | """ 13 | patch module. 14 | :return: None 15 | """ 16 | wrapt.wrap_function_wrapper( 17 | 'pg8000', 18 | 'connect', 19 | connect_wrapper 20 | ) 21 | -------------------------------------------------------------------------------- /epsagon/modules/psycopg2.py: -------------------------------------------------------------------------------- 1 | """ 2 | psycopg2 patcher module 3 | """ 4 | from __future__ import absolute_import 5 | 6 | import wrapt 7 | from .db_wrapper import connect_wrapper 8 | 9 | 10 | #pylint: disable=W0613 11 | def _register_type_wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | register_type wrapper for psycopg2 instrumentation 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | 21 | def _extract_arguments(obj, scope=None): 22 | return obj, scope 23 | 24 | obj, scope = _extract_arguments(*args, **kwargs) 25 | 26 | if scope is not None: 27 | if isinstance(scope, wrapt.ObjectProxy): 28 | scope = scope.__wrapped__ 29 | return wrapped(obj, scope) 30 | 31 | return wrapped(obj) 32 | 33 | 34 | # pylint: disable=abstract-method 35 | class AdapterWrapper(wrapt.ObjectProxy): 36 | """ 37 | a wrapper for an adapter, to strip the connection out of the objectProxy 38 | before calling prepare 39 | """ 40 | 41 | def prepare(self, *args, **kwargs): 42 | """ 43 | Prepare wrapper. 44 | :param args: 45 | :param kwargs: 46 | :return: 47 | """ 48 | if not args: 49 | return self.__wrapped__.prepare(*args, **kwargs) 50 | 51 | connection = args[0] 52 | if isinstance(connection, wrapt.ObjectProxy): 53 | connection = connection.__wrapped__ 54 | 55 | return self.__wrapped__.prepare(connection, *args[1:], **kwargs) 56 | 57 | 58 | def _adapt_wrapper(wrapped, instance, args, kwargs): 59 | """ 60 | adapt wrapper for psycopg2 instrumentation 61 | :param wrapped: wrapt's wrapped 62 | :param instance: wrapt's instance 63 | :param args: wrapt's args 64 | :param kwargs: wrapt's kwargs 65 | :return: None 66 | """ 67 | 68 | adapter = wrapped(*args, **kwargs) 69 | return AdapterWrapper(adapter) if hasattr(adapter, 'prepare') else adapter 70 | 71 | 72 | def _patch_unwrappers(): 73 | """ 74 | patches the functions that do not accept our ObjectProxy to strip the proxy 75 | before calling the function 76 | :return: 77 | """ 78 | 79 | wrapt.wrap_function_wrapper( 80 | 'psycopg2.extensions', 81 | 'register_type', 82 | _register_type_wrapper 83 | ) 84 | 85 | wrapt.wrap_function_wrapper( 86 | 'psycopg2._psycopg', 87 | 'register_type', 88 | _register_type_wrapper 89 | ) 90 | 91 | wrapt.wrap_function_wrapper( 92 | 'psycopg2._json', 93 | 'register_type', 94 | _register_type_wrapper 95 | ) 96 | 97 | wrapt.wrap_function_wrapper( 98 | 'psycopg2.extensions', 99 | 'adapt', 100 | _adapt_wrapper 101 | ) 102 | 103 | 104 | def patch(): 105 | """ 106 | patch module. 107 | :return: None 108 | """ 109 | 110 | wrapt.wrap_function_wrapper( 111 | 'psycopg2', 112 | 'connect', 113 | connect_wrapper 114 | ) 115 | _patch_unwrappers() 116 | -------------------------------------------------------------------------------- /epsagon/modules/pymongo.py: -------------------------------------------------------------------------------- 1 | """ 2 | pymongo patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.pymongo import PyMongoEventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for pymongo instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | 21 | return wrapper(PyMongoEventFactory, wrapped, instance, args, kwargs) 22 | 23 | 24 | def patch(): 25 | """ 26 | Patch module. 27 | :return: None 28 | """ 29 | 30 | wrapt.wrap_function_wrapper( 31 | 'pymongo.collection', 32 | 'Collection.insert_one', 33 | _wrapper 34 | ) 35 | wrapt.wrap_function_wrapper( 36 | 'pymongo.collection', 37 | 'Collection.insert_many', 38 | _wrapper 39 | ) 40 | wrapt.wrap_function_wrapper( 41 | 'pymongo.collection', 42 | 'Collection.find', 43 | _wrapper 44 | ) 45 | wrapt.wrap_function_wrapper( 46 | 'pymongo.collection', 47 | 'Collection.update_one', 48 | _wrapper 49 | ) 50 | wrapt.wrap_function_wrapper( 51 | 'pymongo.collection', 52 | 'Collection.delete_many', 53 | _wrapper 54 | ) 55 | -------------------------------------------------------------------------------- /epsagon/modules/pymysql.py: -------------------------------------------------------------------------------- 1 | """ 2 | pymysql patcher module 3 | """ 4 | from __future__ import absolute_import 5 | 6 | import wrapt 7 | from .db_wrapper import connect_wrapper 8 | 9 | 10 | def patch(): 11 | """ 12 | patch module. 13 | :return: None 14 | """ 15 | wrapt.wrap_function_wrapper( 16 | 'pymysql', 17 | 'connect', 18 | connect_wrapper 19 | ) 20 | -------------------------------------------------------------------------------- /epsagon/modules/pynamodb.py: -------------------------------------------------------------------------------- 1 | """ 2 | PynamoDB patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.pynamodb import PynamoDBEventAdapter, PynamoDBVendoredEventAdapter 9 | 10 | 11 | def _vendored_wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for PynamoDB instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | 21 | # Skip non DynamoDB requests 22 | if 'https://dynamodb.' not in args[0].url: 23 | return wrapped(*args, **kwargs) 24 | 25 | return wrapper( 26 | PynamoDBVendoredEventAdapter, 27 | wrapped, 28 | instance, 29 | args, 30 | kwargs 31 | ) 32 | 33 | 34 | def _wrapper(wrapped, instance, args, kwargs): 35 | """ 36 | General wrapper for PynamoDB instrumentation. 37 | :param wrapped: wrapt's wrapped 38 | :param instance: wrapt's instance 39 | :param args: wrapt's args 40 | :param kwargs: wrapt's kwargs 41 | :return: None 42 | """ 43 | return wrapper(PynamoDBEventAdapter, wrapped, instance, args, kwargs) 44 | 45 | 46 | def patch(): 47 | """ 48 | Patch module. 49 | :return: None 50 | """ 51 | try: 52 | wrapt.wrap_function_wrapper( 53 | 'pynamodb.connection.base', 54 | 'Connection._make_api_call', 55 | _wrapper 56 | ) 57 | except AttributeError: 58 | pass 59 | 60 | try: 61 | wrapt.wrap_function_wrapper( 62 | 'botocore.vendored.requests.sessions', 63 | 'Session.send', 64 | _vendored_wrapper 65 | ) 66 | except AttributeError: 67 | pass 68 | -------------------------------------------------------------------------------- /epsagon/modules/pyqldb.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyqldb patcher module 3 | """ 4 | from __future__ import absolute_import 5 | import wrapt 6 | from epsagon.modules.general_wrapper import wrapper 7 | from ..events.pyqldb import QldbEventFactory 8 | 9 | 10 | def _wrapper(wrapped, instance, args, kwargs): 11 | """ 12 | General wrapper for Pyqldb instrumentation. 13 | :param wrapped: wrapt's wrapped 14 | :param instance: wrapt's instance 15 | :param args: wrapt's args 16 | :param kwargs: wrapt's kwargs 17 | :return: None 18 | """ 19 | return wrapper(QldbEventFactory, wrapped, instance, args, kwargs) 20 | 21 | 22 | def patch(): 23 | """ 24 | patch module. 25 | :return: None 26 | """ 27 | wrapt.wrap_function_wrapper( 28 | 'pyqldb.execution.executor', 29 | 'Executor.execute_statement', 30 | _wrapper 31 | ) 32 | -------------------------------------------------------------------------------- /epsagon/modules/qcloud_cos.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cloud Object Storage patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.qcloud_cos import COSEventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | Cloud Object Storage wrapper for Tencent instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | return wrapper(COSEventFactory, wrapped, instance, args, kwargs) 21 | 22 | 23 | def patch(): 24 | """ 25 | patch module. 26 | :return: None 27 | """ 28 | wrapt.wrap_function_wrapper( 29 | 'qcloud_cos', 30 | 'CosS3Client.send_request', 31 | _wrapper 32 | ) 33 | -------------------------------------------------------------------------------- /epsagon/modules/redis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Redis patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import copy 7 | import wrapt 8 | from epsagon.modules.general_wrapper import wrapper 9 | from ..events.redis import RedisSingleEventFactory, RedisMultiEventFactory 10 | 11 | 12 | def _single_wrapper(wrapped, instance, args, kwargs): 13 | """ 14 | Single execution wrapper for Redis instrumentation. 15 | :param wrapped: wrapt's wrapped 16 | :param instance: wrapt's instance 17 | :param args: wrapt's args 18 | :param kwargs: wrapt's kwargs 19 | :return: None 20 | """ 21 | return wrapper(RedisSingleEventFactory, wrapped, instance, args, kwargs) 22 | 23 | 24 | def _multi_wrapper(wrapped, instance, args, kwargs): 25 | """ 26 | Multi-execution wrapper for Redis instrumentation. 27 | :param wrapped: wrapt's wrapped 28 | :param instance: wrapt's instance 29 | :param args: wrapt's args 30 | :param kwargs: wrapt's kwargs 31 | :return: None 32 | """ 33 | RedisMultiEventFactory.LAST_STACK = copy.deepcopy(instance.command_stack) 34 | return wrapper(RedisMultiEventFactory, wrapped, instance, args, kwargs) 35 | 36 | 37 | def patch(): 38 | """ 39 | patch module. 40 | :return: None 41 | """ 42 | wrapt.wrap_function_wrapper( 43 | 'redis', 44 | 'Redis.execute_command', 45 | _single_wrapper 46 | ) 47 | wrapt.wrap_function_wrapper( 48 | 'redis.client', 49 | 'Pipeline.immediate_execute_command', 50 | _single_wrapper 51 | ) 52 | wrapt.wrap_function_wrapper( 53 | 'redis.client', 54 | 'Pipeline.execute', 55 | _multi_wrapper 56 | ) 57 | -------------------------------------------------------------------------------- /epsagon/modules/requests.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.requests import RequestsEventFactory 9 | from ..constants import EPSAGON_MARKER 10 | from ..utils import print_debug 11 | 12 | 13 | def _wrapper(wrapped, instance, args, kwargs): 14 | """ 15 | General wrapper for requests instrumentation. 16 | :param wrapped: wrapt's wrapped 17 | :param instance: wrapt's instance 18 | :param args: wrapt's args 19 | :param kwargs: wrapt's kwargs 20 | :return: None 21 | """ 22 | # Marking connection pool so requests won't be captured in urllib3 as well 23 | try: 24 | for adapter in instance.adapters.values(): 25 | connection_pool = adapter.poolmanager.connection_from_url( 26 | args[0].url 27 | ) 28 | setattr(connection_pool, EPSAGON_MARKER, True) 29 | except Exception as exception: # pylint: disable=broad-except 30 | print_debug('Could not add marker to requests adapter: {}'.format( 31 | exception 32 | )) 33 | return wrapper(RequestsEventFactory, wrapped, instance, args, kwargs) 34 | 35 | 36 | def patch(): 37 | """ 38 | Patch module. 39 | :return: None 40 | """ 41 | 42 | wrapt.wrap_function_wrapper( 43 | 'requests', 44 | 'Session.send', 45 | _wrapper 46 | ) 47 | -------------------------------------------------------------------------------- /epsagon/modules/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | """ 2 | sqlalchemy patcher module 3 | """ 4 | from __future__ import absolute_import 5 | 6 | from epsagon.modules.general_wrapper import wrapper 7 | from ..events.sqlalchemy import SqlAlchemyEventFactory 8 | from ..utils import patch_once 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for sqlalchemy instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | return wrapper(SqlAlchemyEventFactory, wrapped, instance, args, kwargs) 21 | 22 | def patch(): 23 | """ 24 | patch module. 25 | :return: None 26 | """ 27 | 28 | patch_once( 29 | 'sqlalchemy.orm.session', 30 | 'Session.__init__', 31 | _wrapper 32 | ) 33 | 34 | patch_once( 35 | 'sqlalchemy.orm.session', 36 | 'Session.close', 37 | _wrapper 38 | ) 39 | -------------------------------------------------------------------------------- /epsagon/modules/urllib.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import wrapt 7 | from epsagon.modules.general_wrapper import wrapper 8 | from ..events.urllib import UrllibEventFactory 9 | 10 | 11 | def _wrapper(wrapped, instance, args, kwargs): 12 | """ 13 | General wrapper for requests instrumentation. 14 | :param wrapped: wrapt's wrapped 15 | :param instance: wrapt's instance 16 | :param args: wrapt's args 17 | :param kwargs: wrapt's kwargs 18 | :return: None 19 | """ 20 | return wrapper(UrllibEventFactory, wrapped, instance, args, kwargs) 21 | 22 | 23 | def patch(): 24 | """ 25 | Patch module. 26 | :return: None 27 | """ 28 | 29 | try: 30 | wrapt.wrap_function_wrapper( 31 | 'urllib.request', 32 | 'OpenerDirector._open', 33 | _wrapper 34 | ) 35 | except Exception: # pylint: disable=broad-except 36 | # Can happen in different Python versions. 37 | pass 38 | -------------------------------------------------------------------------------- /epsagon/modules/urllib3.py: -------------------------------------------------------------------------------- 1 | """ 2 | urllib3 patcher module. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import uuid 7 | import wrapt 8 | from epsagon.modules.general_wrapper import wrapper 9 | from ..events.urllib3 import Urllib3EventFactory 10 | from ..http_filters import is_blacklisted_url 11 | from ..constants import EPSAGON_HEADER 12 | 13 | 14 | def _get_headers_from_args( 15 | method=None, 16 | url=None, 17 | body=None, 18 | headers=None, 19 | retries=None, 20 | redirect=None, 21 | assert_same_host=True, 22 | timeout=None, 23 | pool_timeout=None, 24 | release_conn=None, 25 | chunked=False, 26 | body_pos=None, 27 | **response_kw 28 | ): 29 | """ 30 | extract headers from arguments 31 | """ 32 | # pylint: disable=unused-argument 33 | # not using '_' in arg names so unrolling will be smoother 34 | return headers 35 | 36 | 37 | def _wrapper(wrapped, instance, args, kwargs): 38 | """ 39 | General wrapper for requests instrumentation. 40 | :param wrapped: wrapt's wrapped 41 | :param instance: wrapt's instance 42 | :param args: wrapt's args 43 | :param kwargs: wrapt's kwargs 44 | :return: None 45 | """ 46 | # Inject header to support tracing over HTTP requests to 47 | # opentracing monitored code 48 | trace_id = uuid.uuid4().hex 49 | span_id = uuid.uuid4().hex[16:] 50 | parent_span_id = uuid.uuid4().hex[16:] 51 | 52 | host_url = '{}://{}'.format(instance.scheme, instance.host) 53 | 54 | # Detect if URL is blacklisted, and ignore. 55 | if not is_blacklisted_url(host_url): 56 | headers = _get_headers_from_args(*args, **kwargs) 57 | if headers is None: # explicitly checking None to not catch {} 58 | if len(args) >= 4: 59 | # we got None headers as in args[3] 60 | args = list(args) 61 | headers = args[3] = {} 62 | args = tuple(args) 63 | else: 64 | # either kwargs['headers'] == None or it doesn't exist 65 | headers = kwargs['headers'] = {} 66 | 67 | headers[EPSAGON_HEADER] = ( 68 | '{trace_id}:{span_id}:{parent_span_id}:1'.format( 69 | trace_id=trace_id, 70 | span_id=span_id, 71 | parent_span_id=parent_span_id 72 | ) 73 | ) 74 | 75 | return wrapper(Urllib3EventFactory, wrapped, instance, args, kwargs) 76 | 77 | 78 | def patch(): 79 | """ 80 | Patch module. 81 | :return: None 82 | """ 83 | 84 | try: 85 | wrapt.wrap_function_wrapper( 86 | 'urllib3', 87 | 'HTTPConnectionPool.urlopen', 88 | _wrapper 89 | ) 90 | except Exception: # pylint: disable=broad-except 91 | # Can happen in different Python versions. 92 | pass 93 | -------------------------------------------------------------------------------- /epsagon/patcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main patcher module 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from importlib import import_module 7 | import epsagon.modules 8 | 9 | 10 | def _import_exists(module_name): 11 | """ 12 | Validates if import module exists 13 | :param module_name: module name to import 14 | :return: Bool 15 | """ 16 | try: 17 | import_module(module_name) 18 | return True 19 | except ImportError: 20 | return False 21 | 22 | 23 | def patch_all(): 24 | """ 25 | Instrumenting all modules 26 | :return: None 27 | """ 28 | for patch_module in epsagon.modules.MODULES: 29 | if _import_exists(patch_module): 30 | try: 31 | epsagon.modules.MODULES[patch_module].patch() 32 | except Exception: # pylint: disable=broad-except 33 | pass 34 | -------------------------------------------------------------------------------- /epsagon/runners/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/epsagon/runners/__init__.py -------------------------------------------------------------------------------- /epsagon/runners/aws_lambda.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for AWS Lambda. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import os 7 | from uuid import uuid4 8 | from ..event import BaseEvent 9 | from .. import constants 10 | from ..common import ErrorCode 11 | 12 | 13 | class AbstractLambdaRunner(BaseEvent): 14 | """ 15 | Represents Lambda event runner. 16 | """ 17 | ORIGIN = 'runner' 18 | RESOURCE_TYPE = NotImplemented 19 | OPERATION = 'invoke' 20 | ARN_WITH_ALIAS_LENGTH = 8 21 | AWS_ACCOUNT_IND = 4 22 | 23 | def __init__(self, start_time, context): 24 | """ 25 | Initialize. 26 | :param start_time: event's start time (epoch) 27 | :param context: Lambda's context (passed from entry point) 28 | """ 29 | 30 | super(AbstractLambdaRunner, self).__init__(start_time) 31 | 32 | # Creating a unique ID for local runs. 33 | self.event_id = ( 34 | context.aws_request_id 35 | if context.aws_request_id != '1234567890' 36 | else 'local-{}'.format(str(uuid4())) 37 | ) 38 | self.resource['name'] = context.function_name 39 | self.resource['operation'] = self.OPERATION 40 | 41 | arn_split = context.invoked_function_arn.split(':') 42 | self.resource['metadata'].update({ 43 | 'log_stream_name': context.log_stream_name, 44 | 'log_group_name': context.log_group_name, 45 | 'function_version': context.function_version, 46 | 'memory': context.memory_limit_in_mb, 47 | 'aws_account': arn_split[self.AWS_ACCOUNT_IND], 48 | 'cold_start': constants.COLD_START, 49 | 'region': os.getenv('AWS_REGION', ''), 50 | }) 51 | 52 | # Extract Function alias if exists 53 | if len(arn_split) == self.ARN_WITH_ALIAS_LENGTH: 54 | self.resource['metadata']['function_alias'] = arn_split[-1] 55 | 56 | def set_timeout(self): 57 | """ 58 | Sets timeout error code. 59 | :return: None 60 | """ 61 | # Don't override exceptions 62 | if self.error_code != ErrorCode.EXCEPTION: 63 | self.error_code = ErrorCode.TIMEOUT 64 | 65 | 66 | class LambdaRunner(AbstractLambdaRunner): 67 | """ 68 | Represents Lambda event runner. 69 | """ 70 | RESOURCE_TYPE = 'lambda' 71 | 72 | 73 | class StepLambdaRunner(AbstractLambdaRunner): 74 | """ 75 | Represents Lambda event runner. 76 | """ 77 | RESOURCE_TYPE = 'step_function_lambda' 78 | 79 | def add_step_data(self, steps_dict): 80 | """ 81 | Add steps function data. 82 | :param steps_dict: The steps dictionary to add. 83 | """ 84 | self.resource['metadata']['steps_dict'] = steps_dict 85 | -------------------------------------------------------------------------------- /epsagon/runners/azure_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for Azure Functions. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import os 7 | from ..event import BaseEvent 8 | 9 | 10 | class AzureFunctionRunner(BaseEvent): 11 | """ 12 | Represents Azure function event runner. 13 | """ 14 | 15 | ORIGIN = 'runner' 16 | RESOURCE_TYPE = 'azure_function' 17 | OPERATION = 'Invoke' 18 | 19 | def __init__(self, start_time, context): 20 | """ 21 | Initialize. 22 | :param start_time: event's start time (epoch) 23 | :param context: function's context, azure.functions.Context 24 | """ 25 | 26 | super(AzureFunctionRunner, self).__init__(start_time) 27 | 28 | self.event_id = context.invocation_id 29 | self.resource['name'] = context.function_name 30 | self.resource['operation'] = self.OPERATION 31 | 32 | self.resource['metadata'].update({ 33 | 'azure.resource_group': os.getenv('ResourceGroupName', ''), 34 | 'azure.location': os.getenv('Location', ''), 35 | 'azure.function.log': os.getenv('LOGNAME', ''), 36 | 'azure.function.app': os.getenv('WEBSITE_SITE_NAME', ''), 37 | }) 38 | -------------------------------------------------------------------------------- /epsagon/runners/celery.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for a Celery Python function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import time 7 | from uuid import uuid4 8 | from importlib import import_module 9 | from ..event import BaseEvent 10 | from ..utils import add_data_if_needed 11 | 12 | 13 | class CeleryRunner(BaseEvent): 14 | """ 15 | Represents Python Celery event runner. 16 | """ 17 | 18 | ORIGIN = 'runner' 19 | RESOURCE_TYPE = 'celery' 20 | OPERATION = 'execute' 21 | 22 | def __init__(self, *args, **kwargs): # pylint: disable=unused-argument 23 | """ 24 | Initialize. 25 | :param start_time: event's start time (epoch). 26 | """ 27 | 28 | super(CeleryRunner, self).__init__(time.time()) 29 | 30 | self.event_id = str(uuid4()) 31 | 32 | self.resource['name'] = ( 33 | kwargs.get('sender').name 34 | if kwargs.get('sender') 35 | else '' 36 | ) 37 | self.resource['operation'] = self.OPERATION 38 | 39 | app_conn = import_module('celery').current_app.connection() 40 | task_id = kwargs.get('task_id', '') 41 | body = kwargs.get('args') 42 | retval = kwargs.get('retval') 43 | state = kwargs.get('state', '') 44 | 45 | self.resource['metadata'].update({ 46 | 'id': task_id, 47 | 'state': state, 48 | 'hostname': app_conn.hostname or 'localhost', 49 | 'virtual_host': app_conn.virtual_host, 50 | 'driver': app_conn.transport.driver_type, 51 | }) 52 | 53 | if body: 54 | add_data_if_needed( 55 | self.resource['metadata'], 56 | 'args', 57 | body 58 | ) 59 | 60 | if retval: 61 | add_data_if_needed( 62 | self.resource['metadata'], 63 | 'retval', 64 | retval 65 | ) 66 | 67 | def set_retry(self, attempt_number): 68 | """ 69 | Setting retry attempt number 70 | """ 71 | self.resource['metadata']['attempt_number'] = attempt_number 72 | -------------------------------------------------------------------------------- /epsagon/runners/django.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for a Django Python function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import uuid 7 | from ..event import BaseEvent 8 | from ..utils import add_data_if_needed 9 | from ..constants import EPSAGON_HEADER_TITLE 10 | 11 | 12 | class DjangoRunner(BaseEvent): 13 | """ 14 | Represents Python Django event runner. 15 | """ 16 | 17 | ORIGIN = 'runner' 18 | RESOURCE_TYPE = 'python_django' 19 | OPERATION = 'request' 20 | 21 | def __init__(self, start_time, request): 22 | """ 23 | Initialize. 24 | :param start_time: event's start time (epoch). 25 | :param request: the incoming request. 26 | """ 27 | super(DjangoRunner, self).__init__(start_time) 28 | 29 | self.event_id = str(uuid.uuid4()) 30 | self.resource['name'] = request.get_host() 31 | self.resource['operation'] = request.method 32 | 33 | self.resource['metadata'].update({'Path': request.path}) 34 | 35 | if request.body: 36 | add_data_if_needed( 37 | self.resource['metadata'], 38 | 'Request Data', 39 | request.body 40 | ) 41 | 42 | # request.headers introduced since django==2.2 43 | if hasattr(request, 'headers'): 44 | if request.headers.get(EPSAGON_HEADER_TITLE): 45 | self.resource['metadata']['http_trace_id'] = ( 46 | request.headers.get(EPSAGON_HEADER_TITLE) 47 | ) 48 | 49 | if request.headers: 50 | add_data_if_needed( 51 | self.resource['metadata'], 52 | 'Request Headers', 53 | request.headers 54 | ) 55 | 56 | def update_response(self, response): 57 | """ 58 | Adds response data to event. 59 | :param response: WSGI Response 60 | :return: None 61 | """ 62 | if not response: 63 | return 64 | 65 | if hasattr(response, 'content'): 66 | add_data_if_needed( 67 | self.resource['metadata'], 68 | 'Response Data', 69 | response.content 70 | ) 71 | 72 | if hasattr(response, 'items'): 73 | add_data_if_needed( 74 | self.resource['metadata'], 75 | 'Response Headers', 76 | dict(response.items()) 77 | ) 78 | 79 | if hasattr(response, 'status_code'): 80 | self.resource['metadata']['status_code'] = response.status_code 81 | 82 | if response.status_code >= 500: 83 | self.set_error() 84 | -------------------------------------------------------------------------------- /epsagon/runners/flask.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for a Flask Python function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import os 7 | import uuid 8 | from ..event import BaseEvent 9 | from ..utils import add_data_if_needed, normalize_http_url 10 | from ..constants import EPSAGON_HEADER_TITLE 11 | 12 | 13 | class FlaskRunner(BaseEvent): 14 | """ 15 | Represents Python Flask event runner. 16 | """ 17 | 18 | ORIGIN = 'runner' 19 | RESOURCE_TYPE = 'python_flask' 20 | OPERATION = 'request' 21 | 22 | def __init__(self, start_time, app, request): 23 | """ 24 | Initialize. 25 | :param start_time: event's start time (epoch). 26 | :param app: the flask application. 27 | :param request: the incoming request. 28 | """ 29 | 30 | super(FlaskRunner, self).__init__(start_time) 31 | 32 | self.event_id = str(uuid.uuid4()) 33 | 34 | self.resource['name'] = ( 35 | normalize_http_url(request.headers.get('Host')) 36 | if request.headers and request.headers.get('Host') 37 | else app.name 38 | ) 39 | self.resource['operation'] = request.method 40 | 41 | self.resource['metadata'].update({ 42 | 'Base URL': request.base_url, 43 | 'Path': request.path, 44 | 'User Agent': request.headers.get('User-Agent', 'N/A'), 45 | 'Flask Application': app.name, 46 | 'Endpoint': request.endpoint, 47 | }) 48 | 49 | if request.query_string: 50 | self.resource['metadata']['Query String'] = request.query_string 51 | 52 | if request.data: 53 | add_data_if_needed( 54 | self.resource['metadata'], 55 | 'Request Data', 56 | request.data 57 | ) 58 | 59 | request_headers = dict(request.headers) 60 | if request_headers.get(EPSAGON_HEADER_TITLE): 61 | self.resource['metadata']['http_trace_id'] = request_headers.get( 62 | EPSAGON_HEADER_TITLE 63 | ) 64 | 65 | if request_headers: 66 | add_data_if_needed( 67 | self.resource['metadata'], 68 | 'Request Headers', 69 | request_headers 70 | ) 71 | 72 | if request.values: 73 | add_data_if_needed( 74 | self.resource['metadata'], 75 | 'Request Values', 76 | dict(request.values) 77 | ) 78 | 79 | def update_response(self, response): 80 | """ 81 | Adds response data to event. 82 | :param response: WSGI Response 83 | :return: None 84 | """ 85 | 86 | # In some cases capturing the data messes with the original sequence 87 | # (`direct_passthrough`). So we let the user configure this 88 | 89 | if os.getenv('EPSAGON_IGNORE_FLASK_RESPONSE', '').upper() != 'TRUE': 90 | add_data_if_needed( 91 | self.resource['metadata'], 92 | 'Response Data', 93 | response.data 94 | ) 95 | 96 | add_data_if_needed( 97 | self.resource['metadata'], 98 | 'Response Headers', 99 | dict(response.headers) 100 | ) 101 | 102 | self.resource['metadata']['status_code'] = response.status 103 | 104 | if response.status_code >= 500: 105 | self.set_error() 106 | -------------------------------------------------------------------------------- /epsagon/runners/gcp_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for Google Functions. 3 | 4 | """ 5 | 6 | from __future__ import absolute_import 7 | import os 8 | from uuid import uuid4 9 | from ..event import BaseEvent 10 | from .. import constants 11 | 12 | 13 | class GoogleFunctionRunner(BaseEvent): 14 | """ 15 | Represents Google function event runner. 16 | """ 17 | 18 | ORIGIN = 'runner' 19 | RESOURCE_TYPE = 'google_function' 20 | OPERATION = 'Invoke' 21 | 22 | def __init__(self, start_time): 23 | """ 24 | Initialize. 25 | :param start_time: event's start time (epoch) 26 | """ 27 | 28 | super(GoogleFunctionRunner, self).__init__(start_time) 29 | 30 | self.event_id = 'gcp_{}'.format(str(uuid4())) 31 | self.resource['name'] = os.getenv( 32 | 'FUNCTION_NAME', 33 | '' 34 | ) 35 | self.resource['operation'] = self.OPERATION 36 | 37 | self.resource['metadata'].update({ 38 | 'gcp_project': os.getenv('GCP_PROJECT', ''), 39 | 'function_version': os.getenv('X_GOOGLE_FUNCTION_VERSION', ''), 40 | 'memory': os.getenv('FUNCTION_MEMORY_MB', ''), 41 | 'cold_start': constants.COLD_START, 42 | 'region': os.getenv('FUNCTION_REGION', ''), 43 | 'supervisor_hostname': os.getenv('SUPERVISOR_HOSTNAME', ''), 44 | 'supervisor_internal_port': os.getenv( 45 | 'SUPERVISOR_INTERNAL_PORT', '' 46 | ), 47 | 'virtual_env': os.getenv('VIRTUAL_ENV', ''), 48 | 'worker_port': os.getenv('WORKER_PORT', ''), 49 | }) 50 | -------------------------------------------------------------------------------- /epsagon/runners/python_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for a general python function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import uuid 7 | import json 8 | import epsagon.trace 9 | from ..event import BaseEvent 10 | from ..trace_encoder import TraceEncoder 11 | 12 | 13 | class PythonRunner(BaseEvent): 14 | """ 15 | Represents general python event runner. 16 | """ 17 | 18 | ORIGIN = 'runner' 19 | RESOURCE_TYPE = 'python_function' 20 | OPERATION = 'invoke' 21 | 22 | def __init__( 23 | self, 24 | start_time, 25 | wrapped_function, 26 | wrapped_args, 27 | wrapped_kwargs, 28 | name=None 29 | ): 30 | """ 31 | Initialize. 32 | :param start_time: event's start time (epoch). 33 | :param wrapped_function: the function this runner is wrapping. 34 | :param wrapped_args: the arguments the function was called with. 35 | :param wrapped_kwargs: the keyword arguments the function was 36 | called with. 37 | """ 38 | 39 | super(PythonRunner, self).__init__(start_time) 40 | 41 | self.event_id = str(uuid.uuid4()) 42 | self.resource['name'] = name if name else wrapped_function.__name__ 43 | self.resource['operation'] = self.OPERATION 44 | 45 | self.resource['metadata'].update({ 46 | 'python.module': wrapped_function.__module__, 47 | 'python.function.name': wrapped_function.__name__, 48 | }) 49 | 50 | if wrapped_args: 51 | self.add_json_field('python.function.args', wrapped_args) 52 | self.resource['metadata']['python.function.args_length'] = len( 53 | wrapped_args 54 | ) 55 | 56 | if wrapped_kwargs: 57 | self.add_json_field('python.function.kwargs', wrapped_kwargs) 58 | self.resource['metadata']['python.function.kwargs_length'] = len( 59 | wrapped_kwargs 60 | ) 61 | 62 | def add_json_field(self, name, data): 63 | """ 64 | Add a field to metadata with value `data` and name `name`, 65 | only if it is JSON serializable 66 | """ 67 | trace = epsagon.trace.trace_factory.get_trace() 68 | if not trace or trace.metadata_only: 69 | return 70 | 71 | try: 72 | json.dumps(data, cls=TraceEncoder, ensure_ascii=True) 73 | self.resource['metadata'][name] = data 74 | except TypeError: 75 | pass 76 | -------------------------------------------------------------------------------- /epsagon/runners/tencent_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Runner for Tencent SCF. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from ..event import BaseEvent 7 | from .. import constants 8 | from ..common import ErrorCode 9 | 10 | 11 | class TencentFunctionRunner(BaseEvent): 12 | """ 13 | Represents Tencent SCF event runner. 14 | """ 15 | ORIGIN = 'runner' 16 | RESOURCE_TYPE = 'tencent_function' 17 | OPERATION = 'invoke' 18 | 19 | def __init__(self, start_time, context): 20 | """ 21 | Initialize. 22 | :param start_time: event's start time (epoch) 23 | :param context: SCF's context (passed from entry point) 24 | """ 25 | super(TencentFunctionRunner, self).__init__(start_time) 26 | 27 | # Creating a unique ID for local runs. 28 | self.event_id = context['request_id'] 29 | self.resource['name'] = context['function_name'] 30 | self.resource['operation'] = self.OPERATION 31 | 32 | self.resource['metadata'].update({ 33 | 'tencent.scf.version': context['function_version'], 34 | 'tencent.scf.memory': context['memory_limit_in_mb'], 35 | 'tencent.scf.cold_start': constants.COLD_START, 36 | 'tencent.namespace': context['namespace'], 37 | 'tencent.uin': context['tencentcloud_uin'], 38 | 'tencent.app_id': context['tencentcloud_appid'], 39 | 'tencent.region': context['tencentcloud_region'], 40 | }) 41 | 42 | def set_timeout(self): 43 | """ 44 | Sets timeout error code. 45 | :return: None 46 | """ 47 | # Don't override exceptions 48 | if self.error_code != ErrorCode.EXCEPTION: 49 | self.error_code = ErrorCode.TIMEOUT 50 | -------------------------------------------------------------------------------- /epsagon/runners/tornado.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=protected-access 2 | """ 3 | Runner for a Tornado Python framework 4 | """ 5 | 6 | from __future__ import absolute_import 7 | import uuid 8 | from ..event import BaseEvent 9 | from ..utils import add_data_if_needed, print_debug 10 | from ..constants import EPSAGON_HEADER_TITLE 11 | 12 | MAX_PAYLOAD_BYTES = 2000 13 | 14 | 15 | class TornadoRunner(BaseEvent): 16 | """ 17 | Represents Python Tornado event runner. 18 | """ 19 | 20 | ORIGIN = 'runner' 21 | RESOURCE_TYPE = 'python_tornado' 22 | 23 | def __init__(self, start_time, request): 24 | """ 25 | Initialize. 26 | :param start_time: event's start time (epoch). 27 | :param request: the incoming request. 28 | """ 29 | 30 | super(TornadoRunner, self).__init__(start_time) 31 | 32 | self.event_id = str(uuid.uuid4()) 33 | 34 | # Since Tornado doesn't has app name, we use the tracer app name. 35 | self.resource['name'] = request.host 36 | self.resource['operation'] = request.method 37 | 38 | self.resource['metadata'].update({ 39 | 'Host': request.host, 40 | 'url': '{}://{}{}'.format( 41 | request.protocol, 42 | request.host, 43 | request.path 44 | ), 45 | 'Path': request.path, 46 | 'Version': request.version, 47 | 'Remote IP': request.remote_ip, 48 | 'User Agent': request.headers.get('User-Agent', 'N/A'), 49 | }) 50 | 51 | request_headers = dict(request.headers) 52 | 53 | if request_headers.get(EPSAGON_HEADER_TITLE): 54 | self.resource['metadata']['http_trace_id'] = ( 55 | request_headers.get(EPSAGON_HEADER_TITLE) 56 | ) 57 | 58 | if request.query: 59 | add_data_if_needed( 60 | self.resource['metadata'], 61 | 'Query', 62 | request.query 63 | ) 64 | 65 | if request_headers: 66 | add_data_if_needed( 67 | self.resource['metadata'], 68 | 'Request Headers', 69 | request_headers 70 | ) 71 | 72 | try: 73 | if request.body: 74 | body = request.body 75 | if isinstance(body, bytes): 76 | body = body.decode('utf-8') 77 | add_data_if_needed( 78 | self.resource['metadata'], 79 | 'Request Data', 80 | body 81 | ) 82 | except Exception as exception: # pylint: disable=broad-except 83 | print_debug('Could not extract request body: {}'.format(exception)) 84 | 85 | def update_response(self, response, response_body=None): 86 | """ 87 | Adds response data to event. 88 | :param response_body: Response body 89 | :param response: WSGI Response 90 | """ 91 | headers = dict(response._headers.get_all()) 92 | add_data_if_needed( 93 | self.resource['metadata'], 94 | 'Response Headers', 95 | headers 96 | ) 97 | 98 | if response_body: 99 | body = response_body 100 | if isinstance(body, list): 101 | body = body[0] 102 | if isinstance(body, bytes): 103 | body = body.decode('utf-8') 104 | 105 | add_data_if_needed( 106 | self.resource['metadata'], 107 | 'Response Body', 108 | str(body)[:MAX_PAYLOAD_BYTES] 109 | ) 110 | 111 | self.resource['metadata']['status_code'] = response._status_code 112 | self.resource['metadata']['etag'] = headers.get('Etag') 113 | 114 | if not self.error_code and response._status_code >= 500: 115 | self.set_error() 116 | -------------------------------------------------------------------------------- /epsagon/trace_encoder.py: -------------------------------------------------------------------------------- 1 | """ JSONEncoder for trace objects """ 2 | 3 | from datetime import datetime, date 4 | import json 5 | 6 | 7 | class TraceEncoder(json.JSONEncoder): 8 | """ 9 | An encoder for the trace json 10 | """ 11 | 12 | def default(self, o): # pylint: disable=method-hidden 13 | if isinstance(o, set): 14 | return list(o) 15 | if isinstance(o, (datetime, date)): 16 | return o.isoformat() 17 | if isinstance(o, bytes): 18 | return o.decode('utf-8', errors='ignore') 19 | 20 | output = repr(o) 21 | try: 22 | output = json.JSONEncoder.default(self, o) 23 | except TypeError: 24 | pass 25 | return output 26 | -------------------------------------------------------------------------------- /epsagon/trace_transports.py: -------------------------------------------------------------------------------- 1 | """trace transport layers""" 2 | 3 | import os 4 | import base64 5 | import logging 6 | import json 7 | import urllib3 8 | from epsagon.constants import SEND_TIMEOUT 9 | from epsagon.trace_encoder import TraceEncoder 10 | 11 | 12 | def to_json(obj): 13 | return json.dumps(obj, cls=TraceEncoder, ensure_ascii=True) 14 | 15 | 16 | class NoneTransport(object): 17 | @classmethod 18 | def send(cls, _): 19 | logging.error('trace sent using NoneTransport, configure a transport') 20 | 21 | 22 | class LogTransport(object): 23 | """ send traces by logging them """ 24 | 25 | @staticmethod 26 | def send(trace): 27 | trace_json = to_json(trace.to_dict()) 28 | trace_message = base64.b64encode( 29 | trace_json.encode('utf-8') 30 | ).decode('utf-8') 31 | 32 | # pylint: disable=superfluous-parens 33 | print('EPSAGON_TRACE: {}'.format(trace_message)) 34 | 35 | 36 | class HTTPTransport(object): 37 | """ send traces using http request """ 38 | 39 | def __init__(self, dest, token): 40 | self.dest = dest 41 | self.token = token 42 | self.timeout = SEND_TIMEOUT 43 | self.session = urllib3.PoolManager( 44 | cert_reqs='CERT_REQUIRED', 45 | ca_certs=os.path.join(os.path.dirname(__file__), 'cacert.pem'), 46 | headers={ 47 | 'Authorization': 'Bearer {}'.format(self.token), 48 | 'Content-Type': 'application/json' 49 | }, 50 | # max size of reusable connections 51 | maxsize=5 52 | ) 53 | 54 | def send(self, trace): 55 | self.session.request( 56 | 'POST', 57 | self.dest, 58 | body=to_json(trace.to_dict()), 59 | timeout=self.timeout, 60 | retries=False 61 | ) 62 | -------------------------------------------------------------------------------- /epsagon/triggers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/epsagon/triggers/__init__.py -------------------------------------------------------------------------------- /epsagon/triggers/azure_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Triggers for Azure Function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from uuid import uuid4 7 | from epsagon.utils import add_data_if_needed 8 | from ..event import BaseEvent 9 | try: 10 | from urllib.parse import urlparse 11 | except ImportError: 12 | from urlparse import urlparse 13 | 14 | 15 | class BaseAzureTrigger(BaseEvent): 16 | """ 17 | Represents base Azure Function trigger 18 | """ 19 | ORIGIN = 'trigger' 20 | 21 | 22 | class HTTPAzureTrigger(BaseAzureTrigger): 23 | """ 24 | Represents an HTTP Azure Function trigger. 25 | """ 26 | RESOURCE_TYPE = 'http' 27 | 28 | def __init__(self, start_time, event, response): 29 | """ 30 | Initialize. 31 | :param start_time: event's start time (epoch) 32 | :param event: event dict from the entry point, 33 | azure.functions.HttpRequest 34 | :param response: http response, azure.functions.HttpResponse 35 | """ 36 | 37 | super(HTTPAzureTrigger, self).__init__(start_time) 38 | self.resource['operation'] = event.method 39 | url_data = urlparse(event.url) 40 | self.resource['name'] = url_data.netloc 41 | 42 | self.event_id = event.headers.get('x-arr-log-id', str(uuid4())) 43 | 44 | self.resource['metadata'] = { 45 | 'http.request.path': url_data.path, 46 | 'http.status_code': response.status_code, 47 | } 48 | 49 | if event.params: 50 | add_data_if_needed( 51 | self.resource['metadata'], 52 | 'http.request.path_params', 53 | dict(event.params.items()) 54 | ) 55 | 56 | add_data_if_needed( 57 | self.resource['metadata'], 58 | 'http.request.headers', 59 | event.headers.__http_headers__ 60 | ) 61 | 62 | add_data_if_needed( 63 | self.resource['metadata'], 64 | 'http.response.headers', 65 | response.headers.__http_headers__ 66 | ) 67 | 68 | try: 69 | add_data_if_needed( 70 | self.resource['metadata'], 71 | 'http.request.body', 72 | event.get_json() 73 | ) 74 | except Exception: # pylint: disable=broad-except 75 | pass 76 | 77 | 78 | class AzureTriggerFactory(object): 79 | """ 80 | Represents a Azure Function trigger Factory. 81 | """ 82 | 83 | @staticmethod 84 | def factory(start_time, event, result): 85 | """ 86 | Creates trigger event object. 87 | :param start_time: event's start time (epoch) 88 | :param event: event dict from the entry point 89 | :param result: function result, can be relevant in HTTP 90 | :return: Event or None 91 | """ 92 | if 'req' in event: 93 | return HTTPAzureTrigger(start_time, event['req'], result) 94 | return None 95 | -------------------------------------------------------------------------------- /epsagon/triggers/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP triggers for frameworks. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from ..event import BaseEvent 7 | 8 | 9 | class BaseHTTPTrigger(BaseEvent): 10 | """ 11 | Represents base HTTP trigger 12 | """ 13 | ORIGIN = 'trigger' 14 | 15 | 16 | class SQSHTTPTrigger(BaseHTTPTrigger): 17 | """ 18 | Represents SQS HTTP trigger 19 | """ 20 | RESOURCE_TYPE = 'sqs' 21 | 22 | # pylint: disable=W0613 23 | def __init__(self, start_time, request): 24 | """ 25 | Initialize. 26 | :param start_time: event's start time (epoch). 27 | :param request: HTTP request. 28 | """ 29 | 30 | super(SQSHTTPTrigger, self).__init__(start_time) 31 | 32 | self.event_id = request.headers.get('X-Aws-Sqsd-Msgid') 33 | self.resource['name'] = request.headers.get('X-Aws-Sqsd-Queue', 'N/A') 34 | self.resource['operation'] = 'ReceiveMessage' 35 | 36 | 37 | class HTTPTriggerFactory(object): 38 | """ 39 | Represents a HTTP Trigger Factory. 40 | """ 41 | FACTORY = { 42 | class_obj.RESOURCE_TYPE: class_obj 43 | for class_obj in BaseHTTPTrigger.__subclasses__() 44 | } 45 | 46 | @staticmethod 47 | def factory(start_time, request): 48 | """ 49 | Creates trigger event object. 50 | :param start_time: event's start time (epoch) 51 | :param request: HTTP request. 52 | :return: Event or None. 53 | """ 54 | 55 | if request.headers.get('X-Aws-Sqsd-Msgid'): 56 | return SQSHTTPTrigger(start_time, request) 57 | return None 58 | -------------------------------------------------------------------------------- /epsagon/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrappers module. 3 | """ 4 | from __future__ import absolute_import 5 | 6 | from .aws_lambda import lambda_wrapper, step_lambda_wrapper 7 | from .chalice import chalice_wrapper 8 | from .azure_function import azure_wrapper 9 | from .python_function import python_wrapper 10 | from .gcp_function import gcp_wrapper 11 | from .tencent_function import tencent_function_wrapper 12 | 13 | __all__ = [ 14 | 'lambda_wrapper', 15 | 'azure_wrapper', 16 | 'python_wrapper', 17 | 'step_lambda_wrapper', 18 | 'gcp_wrapper', 19 | 'tencent_function_wrapper', 20 | 'chalice_wrapper', 21 | ] 22 | -------------------------------------------------------------------------------- /epsagon/wrappers/azure_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for Azure Function. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import time 7 | import traceback 8 | import warnings 9 | import functools 10 | from ..trace import trace_factory 11 | from ..runners.azure_function import AzureFunctionRunner 12 | from ..triggers.azure_function import AzureTriggerFactory 13 | from ..common import EpsagonWarning 14 | 15 | 16 | def azure_wrapper(func): 17 | """Epsagon's Azure Function wrapper.""" 18 | 19 | @functools.wraps(func) 20 | def _azure_wrapper(*args, **kwargs): 21 | """ 22 | general Azure function wrapper 23 | """ 24 | trace = trace_factory.get_or_create_trace() 25 | trace.prepare() 26 | 27 | context = kwargs.get('context') 28 | if not context: 29 | return func(*args, **kwargs) 30 | 31 | # Create Runner 32 | try: 33 | runner = AzureFunctionRunner(time.time(), context) 34 | trace.set_runner(runner) 35 | except Exception as exception: # pylint: disable=broad-except 36 | warnings.warn( 37 | 'Could not create Azure Function runner: {}'.format(exception), 38 | EpsagonWarning 39 | ) 40 | return func(*args, **kwargs) 41 | 42 | result = None 43 | try: 44 | result = func(*args, **kwargs) 45 | except Exception as exception: 46 | runner.set_exception(exception, traceback.format_exc()) 47 | raise 48 | finally: 49 | runner.terminate() 50 | 51 | # Create Trigger 52 | try: 53 | azure_trigger = AzureTriggerFactory.factory( 54 | time.time(), 55 | kwargs, 56 | result 57 | ) 58 | if azure_trigger: 59 | trace.add_event(azure_trigger) 60 | except Exception as exception: # pylint: disable=broad-except 61 | warnings.warn( 62 | 'Could not create Azure Function trigger: {}'.format(exception), 63 | EpsagonWarning 64 | ) 65 | return func(*args, **kwargs) 66 | 67 | trace_factory.send_traces() 68 | return result 69 | 70 | return _azure_wrapper 71 | -------------------------------------------------------------------------------- /epsagon/wrappers/chalice.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for AWS Lambda in Chalice environment. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from .aws_lambda import lambda_wrapper 7 | 8 | 9 | class ChaliceWrapper(object): 10 | """ 11 | Class handles wrapping Chalice app. 12 | In call we expect an invocation to come, and in getattr we allow `app.attr` 13 | calls. 14 | """ 15 | def __init__(self, app): 16 | self._app = app 17 | 18 | def __getattr__(self, item): 19 | return getattr(self._app, item) 20 | 21 | def __setattr__(self, name, value): 22 | if name == '__class__' and value.__name__ == 'LocalChalice': 23 | # In local runs, the class is being changed to `LocalChalice`, 24 | # So we do that to preserve the same behaviour 25 | value.__getattr__ = self.__getattr__ 26 | super(ChaliceWrapper, self).__setattr__(name, value) 27 | 28 | def __call__(self, *args, **kwargs): 29 | return lambda_wrapper(self._app)(*args, **kwargs) 30 | 31 | 32 | def chalice_wrapper(app): 33 | """ 34 | Chalice wrapper 35 | :param app: Chalice app 36 | :return: ChaliceWrapper 37 | """ 38 | return ChaliceWrapper(app) 39 | -------------------------------------------------------------------------------- /epsagon/wrappers/custom.py: -------------------------------------------------------------------------------- 1 | """ 2 | A helper wrapper to measure internal functions duration 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import time 7 | import functools 8 | from ..trace import trace_factory 9 | 10 | 11 | def measure(func): 12 | """A decorator to measure internal functions duration using labels.""" 13 | 14 | @functools.wraps(func) 15 | def _measure(*args, **kwargs): 16 | """ 17 | Creating a label based on the function name 18 | with the duration in seconds. 19 | """ 20 | start_time = time.time() 21 | result = func(*args, **kwargs) 22 | trace_factory.add_label( 23 | '{}_duration'.format(func.__name__), 24 | float('{:.3f}'.format(time.time() - start_time)) 25 | ) 26 | return result 27 | return _measure 28 | -------------------------------------------------------------------------------- /epsagon/wrappers/django.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for Django. 3 | """ 4 | from __future__ import absolute_import 5 | 6 | import time 7 | import traceback 8 | import warnings 9 | import epsagon 10 | import epsagon.trace 11 | import epsagon.triggers.http 12 | import epsagon.runners.django 13 | 14 | from epsagon.common import EpsagonWarning 15 | from epsagon.utils import ( 16 | collect_container_metadata, 17 | get_traceback_data_from_exception, 18 | ) 19 | from ..http_filters import ignore_request 20 | 21 | class DjangoMiddleware(object): 22 | """ 23 | Represents a Django Middleware for Epsagon instrumentation. 24 | """ 25 | 26 | def __init__(self, get_response): 27 | self.get_response = get_response 28 | 29 | epsagon.trace.trace_factory.switch_to_multiple_traces() 30 | 31 | # pylint: disable=no-self-use 32 | def process_exception(self, request, process_exception): 33 | """ 34 | Processes and appends a given exception to the current trace 35 | """ 36 | if not process_exception: 37 | return 38 | 39 | if ( 40 | not hasattr(request, 'epsagon_trace') or 41 | not request.epsagon_trace.runner 42 | ): 43 | return 44 | 45 | traceback_data = get_traceback_data_from_exception(process_exception) 46 | 47 | request.epsagon_trace.runner.set_exception( 48 | process_exception, 49 | traceback_data, 50 | False 51 | ) 52 | 53 | def __call__(self, request): 54 | # Link epsagon to the request object for easy-access to epsagon lib 55 | request.epsagon = epsagon 56 | if epsagon.http_filters.is_ignored_endpoint(request.path): 57 | return self.get_response(request) 58 | 59 | request_middleware = DjangoRequestMiddleware(request) 60 | request_middleware.before_request() 61 | 62 | response = self.get_response(request) 63 | 64 | request_middleware.after_request(response) 65 | return response 66 | 67 | 68 | class DjangoRequestMiddleware(object): 69 | """ 70 | Django middleware for a single request 71 | """ 72 | 73 | def __init__(self, request): 74 | self.request = request 75 | self.runner = None 76 | self.ignored_request = False 77 | self.should_send_trace = True 78 | 79 | def before_request(self): 80 | """ 81 | Runs before process of response. 82 | """ 83 | trace = epsagon.trace.trace_factory.get_trace() 84 | if not trace: 85 | trace = epsagon.trace.trace_factory.get_or_create_trace() 86 | else: 87 | self.should_send_trace = False 88 | trace.prepare() 89 | 90 | # Ignoring non relevant content types. 91 | self.ignored_request = ignore_request('', self.request.path.lower()) 92 | 93 | if self.ignored_request: 94 | return 95 | 96 | # Create a Django runner with current request. 97 | try: 98 | self.runner = epsagon.runners.django.DjangoRunner( 99 | time.time(), 100 | self.request 101 | ) 102 | trace.set_runner(self.runner) 103 | self.request.epsagon_trace = trace 104 | 105 | # Collect metadata in case this is a container. 106 | collect_container_metadata(self.runner.resource['metadata']) 107 | 108 | # pylint: disable=W0703 109 | except Exception as exception: 110 | # Regress to python runner. 111 | warnings.warn('Could not extract request', EpsagonWarning) 112 | epsagon.trace.trace_factory.add_exception( 113 | exception, 114 | traceback.format_exc() 115 | ) 116 | 117 | # Extract HTTP trigger data. 118 | try: 119 | trigger = epsagon.triggers.http.HTTPTriggerFactory.factory( 120 | time.time(), 121 | self.request 122 | ) 123 | if trigger: 124 | epsagon.trace.trace_factory.add_event(trigger) 125 | # pylint: disable=W0703 126 | except Exception as exception: 127 | epsagon.trace.trace_factory.add_exception( 128 | exception, 129 | traceback.format_exc(), 130 | ) 131 | 132 | def after_request(self, response): 133 | """ 134 | Runs after process of response. 135 | """ 136 | if self.ignored_request: 137 | epsagon.trace.trace_factory.pop_trace() 138 | return 139 | 140 | # Ignoring non relevant content types. 141 | if ignore_request(response.get('Content-Type', '').lower(), ''): 142 | self.ignored_request = True 143 | epsagon.trace.trace_factory.pop_trace() 144 | return 145 | 146 | # Safety in case we run on an old Django version 147 | if not self.runner: 148 | return 149 | 150 | self.runner.update_response(response) 151 | if self.should_send_trace: 152 | epsagon.trace.trace_factory.send_traces() 153 | -------------------------------------------------------------------------------- /epsagon/wrappers/flask.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for Python Flask. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | import time 8 | import warnings 9 | 10 | from flask import request 11 | import epsagon.trace 12 | import epsagon.triggers.http 13 | import epsagon.runners.flask 14 | from epsagon.common import EpsagonWarning 15 | from epsagon.utils import collect_container_metadata,\ 16 | get_traceback_data_from_exception 17 | from ..http_filters import ignore_request, \ 18 | is_ignored_endpoint, add_ignored_endpoints 19 | 20 | 21 | class FlaskWrapper(object): 22 | """ 23 | Wraps Flask wsgi application. 24 | """ 25 | EPSAGON_MARKER = '_epsagon_wrapper' 26 | 27 | def __init__(self, app, ignored_endpoints=None): 28 | """ 29 | WSGI app wrapper for flask application. 30 | :param app: the :class:`flask.Flask` application object. 31 | :param ignored_endpoints: endpoint paths to ignore. 32 | """ 33 | # Wrapping app only once 34 | if getattr(app, self.EPSAGON_MARKER, False): 35 | return 36 | setattr(app, self.EPSAGON_MARKER, True) 37 | self.app = app 38 | if ignored_endpoints: 39 | if isinstance(ignored_endpoints, list): 40 | add_ignored_endpoints(ignored_endpoints) 41 | elif isinstance(ignored_endpoints, str): 42 | add_ignored_endpoints([ignored_endpoints]) 43 | 44 | # Override request handling. 45 | self.app.before_request(self._before_request) 46 | self.app.after_request(self._after_request) 47 | self.app.teardown_request(self._teardown_request) 48 | 49 | # Whether we ignore this request or not. 50 | self.ignored_request = False 51 | epsagon.trace.trace_factory.switch_to_multiple_traces() 52 | 53 | def _before_request(self): 54 | """ 55 | Runs when new request comes in. 56 | :return: None. 57 | """ 58 | # Ignoring non relevant content types. 59 | self.ignored_request = ignore_request('', request.path.lower()) 60 | 61 | if self.ignored_request: 62 | return 63 | 64 | trace = epsagon.trace.trace_factory.get_or_create_trace() 65 | trace.prepare() 66 | 67 | # Create flask runner with current request. 68 | try: 69 | runner = epsagon.runners.flask.FlaskRunner( 70 | time.time(), 71 | self.app, 72 | request 73 | ) 74 | trace.set_runner(runner) 75 | 76 | # Collect metadata in case this is a container. 77 | collect_container_metadata(runner.resource['metadata']) 78 | 79 | # pylint: disable=W0703 80 | except Exception as exception: 81 | # Regress to python runner. 82 | warnings.warn('Could not extract request', EpsagonWarning) 83 | trace.add_exception( 84 | exception, 85 | traceback.format_exc() 86 | ) 87 | 88 | # Extract HTTP trigger data. 89 | try: 90 | trigger = epsagon.triggers.http.HTTPTriggerFactory.factory( 91 | time.time(), 92 | request 93 | ) 94 | if trigger: 95 | trace.add_event(trigger) 96 | # pylint: disable=W0703 97 | except Exception as exception: 98 | trace.add_exception( 99 | exception, 100 | traceback.format_exc(), 101 | ) 102 | 103 | def _after_request(self, response): 104 | """ 105 | Runs after first process of response. 106 | :param response: The current Response object. 107 | :return: Response. 108 | """ 109 | if self.ignored_request: 110 | return response 111 | 112 | # Ignoring non relevant content types. 113 | if ignore_request(response.content_type.lower(), ''): 114 | self.ignored_request = True 115 | return response 116 | 117 | trace = epsagon.trace.trace_factory.get_or_create_trace() 118 | 119 | if trace.runner: 120 | trace.runner.update_response(response) 121 | return response 122 | 123 | def _teardown_request(self, exception): 124 | """ 125 | Runs at the end of the request. Exception will be passed if happens. 126 | If no flask url rule exists for a request, then the request trace 127 | will be passed. 128 | :param exception: Exception (or None). 129 | :return: None. 130 | """ 131 | if self.ignored_request: 132 | return 133 | trace = epsagon.trace.trace_factory.get_or_create_trace() 134 | if exception and trace.runner: 135 | traceback_data = get_traceback_data_from_exception(exception) 136 | trace.runner.set_exception(exception, traceback_data) 137 | # Ignoring endpoint, only if no error happened. 138 | if (not exception and 139 | request.url_rule and 140 | is_ignored_endpoint(request.url_rule.rule) 141 | ): 142 | return 143 | 144 | epsagon.trace.trace_factory.send_traces() 145 | -------------------------------------------------------------------------------- /epsagon/wrappers/gcp_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for Google Function. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | import time 8 | import functools 9 | import warnings 10 | import epsagon.trace 11 | import epsagon.wrappers.python_function 12 | from epsagon.common import EpsagonWarning 13 | from ..runners.gcp_function import GoogleFunctionRunner 14 | from .. import constants 15 | 16 | 17 | def gcp_wrapper(func): 18 | """Epsagon's GCP wrapper.""" 19 | 20 | @functools.wraps(func) 21 | def _gcp_wrapper(*args, **kwargs): 22 | """ 23 | Generic google function wrapper 24 | """ 25 | trace = epsagon.trace.trace_factory.get_or_create_trace() 26 | trace.prepare() 27 | 28 | try: 29 | runner = GoogleFunctionRunner( 30 | time.time(), 31 | ) 32 | # pylint: disable=W0703 33 | except Exception as exception: 34 | # Regress to python runner. 35 | warnings.warn( 36 | 'GCP environment is invalid, using simple python wrapper', 37 | EpsagonWarning 38 | ) 39 | trace.add_exception(exception, traceback.format_exc()) 40 | return epsagon.wrappers.python_function.wrap_python_function( 41 | func, 42 | args, 43 | kwargs 44 | ) 45 | 46 | constants.COLD_START = False 47 | 48 | result = None 49 | try: 50 | result = func(*args, **kwargs) 51 | return result 52 | # pylint: disable=W0703 53 | except Exception as exception: 54 | runner.set_exception(exception, traceback.format_exc()) 55 | raise 56 | finally: 57 | try: 58 | if not trace.metadata_only: 59 | runner.resource['metadata']['return_value'] = result 60 | # pylint: disable=W0703 61 | except Exception as exception: 62 | trace.add_exception(exception, traceback.format_exc()) 63 | try: 64 | trace.add_event(runner) 65 | epsagon.trace.trace_factory.send_traces() 66 | # pylint: disable=W0703 67 | except Exception: 68 | pass 69 | 70 | return _gcp_wrapper 71 | -------------------------------------------------------------------------------- /epsagon/wrappers/python_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for a general python function 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import time 7 | import traceback 8 | import functools 9 | import epsagon.trace 10 | import epsagon.runners.python_function 11 | from epsagon.utils import collect_container_metadata 12 | from epsagon import constants 13 | 14 | 15 | def wrap_python_function(func, args, kwargs, name=None): 16 | """ 17 | Wrap a python function call with a simple python wrapper. Used as default 18 | when wrapping with other wrappers is impossible. 19 | NOTE: this function does not prepare the tracer (clears the previous run) 20 | :param func: The function to wrap. 21 | :param args: The arguments to the function. 22 | :param kwargs: The keyword arguments to the function. 23 | :param name: Resource name for the runner 24 | :return: The function's result. 25 | """ 26 | try: 27 | runner = epsagon.runners.python_function.PythonRunner( 28 | time.time(), 29 | func, 30 | args, 31 | kwargs, 32 | name=name 33 | ) 34 | epsagon.trace.trace_factory.set_runner(runner) 35 | 36 | # Collect metadata in case this is a container. 37 | collect_container_metadata(runner.resource['metadata']) 38 | 39 | # pylint: disable=W0703 40 | except Exception: 41 | # If we failed, just call the user's function. Nothing more to do. 42 | return func(*args, **kwargs) 43 | 44 | # settings in case we are in a lambda and context is None 45 | constants.COLD_START = False 46 | 47 | result = None 48 | try: 49 | result = func(*args, **kwargs) 50 | return result 51 | # pylint: disable=W0703 52 | except Exception as exception: 53 | runner.set_exception(exception, traceback.format_exc()) 54 | raise 55 | finally: 56 | try: 57 | runner.add_json_field('python.function.return_value', result) 58 | # pylint: disable=W0703 59 | except Exception as exception: 60 | epsagon.trace.trace_factory.add_exception( 61 | exception, 62 | traceback.format_exc(), 63 | ) 64 | try: 65 | epsagon.trace.trace_factory.send_traces() 66 | # pylint: disable=W0703 67 | except Exception: 68 | pass 69 | 70 | 71 | def python_wrapper(*args, **kwargs): 72 | """ 73 | Epsagon's general Python wrapper. 74 | 75 | Receives optional keyword arg 'name': used to identify this resource in 76 | the application (defaults to function name). 77 | 78 | Options for using: 79 | - @python_wrapper(name='my-resource') 80 | def my_function(params): 81 | ... 82 | 83 | - @python_wrapper() 84 | def my_function(params): 85 | ... 86 | 87 | - @python_wrapper 88 | def my_function(params): 89 | ... 90 | 91 | NOTE: Should not be used for two functions in the same stack trace, or when 92 | running under a traced context (e.g. AWS Lambda, HTTP frameworks, etc) 93 | """ 94 | name = kwargs.get('name') 95 | 96 | def _inner_wrapper(func): 97 | 98 | @functools.wraps(func) 99 | def _python_wrapper(*args, **kwargs): 100 | epsagon.trace.trace_factory.get_or_create_trace().prepare() 101 | return wrap_python_function(func, args, kwargs, name=name) 102 | 103 | return _python_wrapper 104 | 105 | if len(args) == 1 and callable(args[0]): 106 | return _inner_wrapper(args[0]) 107 | 108 | return _inner_wrapper 109 | -------------------------------------------------------------------------------- /epsagon/wrappers/tencent_function.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wrapper for Tencent Serverless Cloud Functions. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import traceback 7 | import time 8 | import functools 9 | import warnings 10 | try: 11 | from collections import Mapping 12 | except: # pylint: disable=W0702 13 | from collections.abc import Mapping 14 | 15 | import epsagon.trace 16 | import epsagon.runners.tencent_function 17 | import epsagon.wrappers.python_function 18 | import epsagon.utils 19 | import epsagon.runners.python_function 20 | from epsagon.common import EpsagonWarning 21 | from epsagon.triggers.tencent_function import TencentFunctionTriggerFactory 22 | from .. import constants 23 | 24 | 25 | def _add_status_code(runner, return_value): 26 | """ 27 | Tries to extract the status code from the return value and add it 28 | as a metadata field 29 | :param runner: Runner event to update 30 | :param return_value: The return value to extract from 31 | """ 32 | if isinstance(return_value, Mapping): 33 | status_code = return_value.get('statusCode') 34 | if status_code: 35 | runner.resource['metadata']['status_code'] = status_code 36 | 37 | 38 | def tencent_function_wrapper(func): 39 | """Epsagon's Tencent SCF wrapper.""" 40 | 41 | # avoid double instrumentation 42 | if getattr(func, '__instrumented__', False): 43 | return func 44 | 45 | @functools.wraps(func) 46 | def _tencent_function_wrapper(*args, **kwargs): 47 | """ 48 | Generic SCF function wrapper 49 | """ 50 | start_time = time.time() 51 | cold_start_duration = start_time - constants.COLD_START_TIME 52 | trace = epsagon.trace.trace_factory.get_or_create_trace() 53 | trace.prepare() 54 | 55 | try: 56 | event, context = args 57 | except ValueError: 58 | # This can happen when someone manually calls handler without 59 | # parameters / sends kwargs. In such case we ignore this trace. 60 | return func(*args, **kwargs) 61 | 62 | try: 63 | runner = epsagon.runners.tencent_function.TencentFunctionRunner( 64 | start_time, 65 | context 66 | ) 67 | trace.set_runner(runner) 68 | # pylint: disable=W0703 69 | except Exception as exception: 70 | # Regress to python runner. 71 | warnings.warn( 72 | 'SCF context is invalid, using simple python wrapper', 73 | EpsagonWarning 74 | ) 75 | trace.add_exception( 76 | exception, 77 | traceback.format_exc() 78 | ) 79 | return epsagon.wrappers.python_function.wrap_python_function( 80 | func, 81 | args, 82 | kwargs 83 | ) 84 | 85 | if constants.COLD_START: 86 | runner.resource['metadata'][ 87 | 'tencent.scf.cold_start_duration' 88 | ] = cold_start_duration 89 | constants.COLD_START = False 90 | 91 | try: 92 | trace.add_event( 93 | TencentFunctionTriggerFactory.factory( 94 | start_time, 95 | event, 96 | context, 97 | runner 98 | ) 99 | ) 100 | # pylint: disable=W0703 101 | except Exception as exception: 102 | trace.add_exception( 103 | exception, 104 | traceback.format_exc(), 105 | additional_data={'event': event} 106 | ) 107 | 108 | result = None 109 | try: 110 | result = func(*args, **kwargs) 111 | return result 112 | # pylint: disable=W0703 113 | except Exception as exception: 114 | runner.set_exception( 115 | exception, 116 | traceback.format_exc(), 117 | handled=False 118 | ) 119 | raise 120 | finally: 121 | try: 122 | _add_status_code(runner, result) 123 | if not trace.metadata_only: 124 | runner.resource['metadata']['tencent.scf.return_data'] = ( 125 | result 126 | ) 127 | # pylint: disable=W0703 128 | except Exception as exception: 129 | trace.add_exception( 130 | exception, 131 | traceback.format_exc(), 132 | ) 133 | 134 | try: 135 | epsagon.trace.trace_factory.send_traces() 136 | # pylint: disable=W0703 137 | except Exception: 138 | epsagon.utils.print_debug('Failed to send SCF trace') 139 | 140 | _tencent_function_wrapper.__instrumented__ = True 141 | return _tencent_function_wrapper 142 | -------------------------------------------------------------------------------- /examples/aws_lambda.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example for Epsagon usage in AWS Lambda function. 3 | """ 4 | 5 | import epsagon 6 | epsagon.init( 7 | token='my-secret-token', 8 | app_name='my-app-name', 9 | metadata_only=False # Optional 10 | ) 11 | 12 | 13 | @epsagon.lambda_wrapper 14 | def handle(event, context): 15 | return 'It worked!' 16 | -------------------------------------------------------------------------------- /examples/custom_labels.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example for custom labels usage in AWS Lambda function. 3 | """ 4 | 5 | import epsagon 6 | epsagon.init( 7 | token='my-secret-token', 8 | app_name='my-app-name', 9 | metadata_only=False # Optional 10 | ) 11 | 12 | 13 | @epsagon.lambda_wrapper 14 | def handle(event, context): 15 | epsagon.label('label', 'something_to_filter_afterwards') 16 | epsagon.label('number_of_records_parsed_successfully', 42) 17 | 18 | return 'It worked!' 19 | -------------------------------------------------------------------------------- /examples/custom_python.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple example for Epsagon usage when running a custom Python code 3 | """ 4 | import sys 5 | 6 | import epsagon 7 | epsagon.init( 8 | token='my-secret-token', 9 | app_name='my-app-name', 10 | metadata_only=False # Optional 11 | ) 12 | 13 | 14 | @epsagon.python_wrapper(name='my-python-resource') 15 | def main(args): 16 | print('Hello world: ' + str(args)) 17 | return 'It worked!' 18 | 19 | 20 | if __name__ == '__main__': 21 | main(sys.argv[1:]) 22 | -------------------------------------------------------------------------------- /examples/fastapi_example.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import epsagon 3 | 4 | epsagon.init( 5 | token='', 6 | metadata_only=False, 7 | ignored_endpoints=['/ignored'], 8 | ) 9 | 10 | app = FastAPI() 11 | 12 | 13 | @app.get("/") 14 | def root(): 15 | return {"message": "Fast API in Python"} 16 | 17 | 18 | @app.get("/ignored") 19 | def ignored(): 20 | return {"message": "This Is Ignored"} 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epsagon-python", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@semantic-release/exec": "^3.3.2", 13 | "semantic-release": "^17.4.7", 14 | "serverless": "^2.60.0", 15 | "serverless-python-requirements": "^4.3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | boto3 3 | tox 4 | mock 5 | Flask 6 | # Choose 1.9.5 in py2, and 2.5.3 in py3 7 | pylint>=1.9.5,<=2.5.3 8 | pylint-quotes 9 | django 10 | pytest==6.2.5; python_version > '3.7' 11 | pytest==6.1.0; python_version == '3.7' 12 | pytest==6.0.0; python_version >= '3.5' and python_version < '3.7' 13 | pytest==4.6.0; python_version < '3.5' 14 | requests 15 | sqlalchemy==1.3.23 16 | psycopg2-binary 17 | aiohttp; python_version >= '3.5' 18 | fastapi==0.65.2; python_version >= '3.5' 19 | pytest-asyncio; python_version >= '3.5' 20 | pytest-aiohttp; python_version >= '3.5' 21 | httpx; python_version >= '3.5' 22 | asynctest; python_version >= '3.5' 23 | pytest-lazy-fixture; python_version >= '3.5' 24 | moto; python_version >= '3.5' 25 | moto==2.1.0; python_version < '3.5' 26 | tornado 27 | kafka-python 28 | pytest-httpserver; python_version >= '3.5' 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | wrapt>=1.11.0 2 | autowrapt==1.0 3 | urllib3 4 | six 5 | future 6 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "releasing new version..." && 3 | ./scripts/semantic_release.sh && 4 | sleep 60 && 5 | echo "publishing layer..." && 6 | ./scripts/publish_layer.sh && 7 | echo "deployment successful" 8 | -------------------------------------------------------------------------------- /scripts/publish_layer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | declare -a regions=("ap-northeast-1" "ap-northeast-2" "ap-south-1" "ap-southeast-1" "ap-southeast-2" "ca-central-1" "eu-central-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") 3 | mkdir python 4 | pip install awscli jq 5 | pip install epsagon -t python/ 6 | zip -r epsagon-python-layer.zip python -x ".*" -x "__MACOSX" -x "*.pyc" -x "*__pycache__*" 7 | rm -Rf python/ 8 | 9 | for region in "${regions[@]}" 10 | do 11 | echo ${region} 12 | aws s3 cp epsagon-python-layer.zip s3://epsagon-layers-${region}/ 13 | LAYER_VERSION=$(aws lambda publish-layer-version --layer-name epsagon-python-layer --description "Epsagon Python layer that includes pre-installed packages to get up and running with monitoring and distributed tracing" --content S3Bucket=epsagon-layers-${region},S3Key=epsagon-python-layer.zip --compatible-runtimes python3.9 python3.8 python3.7 python3.6 python2.7 --compatible-architectures "arm64" "x86_64" --license-info MIT --region ${region} | jq '.Version') 14 | aws lambda add-layer-version-permission --layer-name epsagon-python-layer --version-number ${LAYER_VERSION} --statement-id sid1 --action lambda:GetLayerVersion --principal \* --region ${region} 15 | done 16 | 17 | rm epsagon-python-layer.zip 18 | -------------------------------------------------------------------------------- /scripts/publish_package.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from semantic_release.pypi import upload_to_pypi 4 | 5 | if __name__ == '__main__': 6 | try: 7 | upload_to_pypi( 8 | username=os.environ['PYPI_USERNAME'], 9 | password=os.environ['PYPI_PASSWORD'], 10 | ) 11 | except Exception: 12 | logging.exception('failed to publish') 13 | exit(1) 14 | -------------------------------------------------------------------------------- /scripts/run_acceptance_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [ -z $AWS_ACCESS_KEY_ID ] || [ -z $AWS_SECRET_ACCESS_KEY ]; then 3 | echo "AWS credentials must be set in order to run acceptance tests" 4 | exit 1 5 | elif [ $TRAVIS_PYTHON_VERSION != "2.7" ] && [ $TRAVIS_PYTHON_VERSION != "3.6" ]; then 6 | npm install && export PATH=$(pwd)/node_modules/.bin:$PATH 7 | ./acceptance/run.sh $TRAVIS_BUILD_NUMBER $TRAVIS_PYTHON_VERSION 8 | fi 9 | -------------------------------------------------------------------------------- /scripts/run_lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Skip aio based files from older Python versions 3 | ret=`python -c 'import sys; print(0 if sys.version_info < (3, 5, 3) else 1)'` 4 | excludes='' 5 | if [ $ret -eq 0 ]; then 6 | excludes='aiohttp.py,fastapi.py' 7 | fi 8 | pylint --msg-template='{path}:{line}: [{msg_id}({symbol}) {obj}] {msg}' --ignore-patterns=$excludes epsagon/ 9 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Skip aio based files from older Python versions 3 | ret=`python -c 'import sys; print(0 if sys.version_info < (3, 5, 3) else 1)'` 4 | excludes='' 5 | if [ $ret -eq 0 ]; then 6 | pytest -vv --ignore-glob=*fastapi* --ignore-glob=*requests_event* --ignore-glob=*transports* 7 | else 8 | pytest -vv 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/semantic_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.name "semantic-release (via TravisCI)" 3 | git config --global user.email "semantic-release@travis" 4 | pip install --upgrade wheel setuptools twine pkginfo 5 | pip install python-semantic-release==4.6.0 6 | npm install @semantic-release/exec semantic-release 7 | ./node_modules/.bin/semantic-release 8 | -------------------------------------------------------------------------------- /scripts/set_version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from semantic_release.history import set_new_version 3 | 4 | if __name__ == '__main__': 5 | set_new_version(sys.argv[1]) 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [flake8] 5 | max-line-length = 80 6 | 7 | [semantic_release] 8 | version_variable = epsagon/constants.py:__version__ 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | with open('./requirements.txt', 'r') as reqs_file: 7 | reqs = reqs_file.readlines() 8 | 9 | # Get version 10 | with open(os.path.join('epsagon', 'constants.py'), 'rt') as consts_file: 11 | version = re.search(r'__version__ = \'(.*?)\'', consts_file.read()).group(1) 12 | 13 | setup( 14 | name='epsagon', 15 | version=version, 16 | description='Epsagon Instrumentation for Python', 17 | long_description=open('README.md').read(), 18 | long_description_content_type='text/markdown', 19 | author='Epsagon', 20 | author_email='support@epsagon.com', 21 | url='https://github.com/epsagon/epsagon-python', 22 | packages=find_packages(exclude=('tests', 'examples')), 23 | package_data={'epsagon': ['*.pem']}, 24 | install_requires=reqs, 25 | license='MIT', 26 | setup_requires=['pytest-runner'], 27 | tests_require=['pytest'], 28 | entry_points={ 29 | 'epsagon': ['string = epsagon:auto_load'] 30 | }, 31 | keywords=[ 32 | 'serverless', 33 | 'microservices', 34 | 'epsagon', 35 | 'tracing', 36 | 'distributed-tracing', 37 | 'lambda', 38 | 'aws-lambda', 39 | 'debugging', 40 | 'monitoring' 41 | ], 42 | classifiers=( 43 | 'Intended Audience :: Developers', 44 | 'Operating System :: OS Independent', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 2', 47 | 'Programming Language :: Python :: 2.7', 48 | 'Programming Language :: Python :: 3', 49 | 'Programming Language :: Python :: 3.6', 50 | 'Programming Language :: Python :: 3.7', 51 | 'Programming Language :: Python :: 3.8', 52 | 'Topic :: Software Development :: Libraries', 53 | 'Topic :: Software Development :: Libraries :: Python Modules', 54 | ) 55 | ) 56 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common and builtin fixtures 3 | """ 4 | import pytest 5 | from mock import MagicMock 6 | 7 | import epsagon 8 | 9 | TEST_TOKEN = 'test' 10 | TEST_APP = 'test_app' 11 | TEST_COLLECTOR = 'collector' 12 | 13 | 14 | class TestTransport(MagicMock): 15 | """ 16 | Mock trace transport for tests 17 | """ 18 | @property 19 | def last_trace(self): 20 | """ 21 | :return: The last Trace object that was sent (None if no trace was sent) 22 | """ 23 | return self.send.call_args[0][0] if self.send.call_args else None 24 | 25 | @property 26 | def sent_traces(self): 27 | """ 28 | :return: List of all the Trace objects that were sent 29 | """ 30 | return [ 31 | call_args[0][0] for call_args in self.send.call_args_list 32 | ] 33 | 34 | 35 | @pytest.fixture(scope='function', autouse=False) 36 | def trace_transport(clean_traces): 37 | """ 38 | Fixture for overriding the trace transport class with a `TestTransport` instance 39 | :return: New `TestTransport` object 40 | """ 41 | epsagon.trace_factory.transport = TestTransport() 42 | return epsagon.trace_factory.transport 43 | 44 | 45 | def init_epsagon(**kwargs): 46 | """ 47 | Call `epsagon.init` with default test args 48 | :param kwargs: Optional args to pass 49 | """ 50 | default_kwargs = { 51 | 'token': TEST_TOKEN, 52 | 'app_name': TEST_APP, 53 | 'metadata_only': False, 54 | 'collector_url': TEST_COLLECTOR, 55 | } 56 | default_kwargs.update(kwargs) 57 | 58 | epsagon.init(**default_kwargs) 59 | 60 | 61 | @pytest.fixture(scope='module', autouse=True) 62 | def call_init_epsagon(): 63 | """ 64 | Init epsagon with default test values 65 | """ 66 | init_epsagon() 67 | return epsagon 68 | 69 | 70 | @pytest.fixture(scope='function', autouse=True) 71 | def clean_traces(): 72 | """ 73 | Remove current traces from previous test (so that they will not effect the current test) 74 | """ 75 | epsagon.trace_factory.singleton_trace = None 76 | epsagon.trace_factory.traces = {} 77 | 78 | @pytest.fixture(scope='function', autouse=True) 79 | def reset_tracer_mode(): 80 | """ 81 | Resets trace factory tracer mode to a single trace. 82 | """ 83 | epsagon.trace_factory.use_single_trace = True 84 | epsagon.use_async_tracer = False 85 | -------------------------------------------------------------------------------- /tests/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/events/__init__.py -------------------------------------------------------------------------------- /tests/events/test_eventbridge.py: -------------------------------------------------------------------------------- 1 | from epsagon.trace import trace_factory 2 | from moto import mock_events 3 | import boto3 4 | import json 5 | 6 | fake_event = { 7 | "DetailType": 'test-detail-type', 8 | "Source": 'test-source', 9 | "Detail": json.dumps({"test": 1234}), 10 | "EventBusName": 'test-event-bus-name', 11 | } 12 | 13 | fake_event_bus_name_missing = { 14 | "DetailType": 'test-detail-type', 15 | "Source": 'test-source', 16 | "Detail": json.dumps({"test": 1234}) 17 | } 18 | 19 | 20 | def setup_function(func): 21 | trace_factory.use_single_trace = True 22 | trace_factory.get_or_create_trace() 23 | 24 | 25 | def teardown_function(func): 26 | trace_factory.singleton_trace = None 27 | 28 | def _get_active_trace(): 29 | return trace_factory.active_trace 30 | 31 | 32 | def _put_event(client, event): 33 | response = client.put_events(Entries=[event]) 34 | return response 35 | 36 | 37 | @mock_events 38 | def test_event_resources(): 39 | client = boto3.client('events', region_name='us-west-1') 40 | _put_event(client, fake_event) 41 | trace = _get_active_trace() 42 | assert trace.events[0].resource["operation"] == 'PutEvents' 43 | assert trace.events[0].RESOURCE_TYPE == 'eventbridge' 44 | assert trace.events[0].resource["name"] == fake_event["EventBusName"] 45 | 46 | 47 | @mock_events 48 | def test_event_resources_without_bus_name(): 49 | client = boto3.client('events', region_name='us-west-1') 50 | _put_event(client, fake_event_bus_name_missing) 51 | trace = _get_active_trace() 52 | assert trace.events[0].RESOURCE_TYPE == 'eventbridge' 53 | assert trace.events[0].resource["name"] == 'CloudWatch Events' 54 | 55 | 56 | @mock_events 57 | def test_event_metadata(): 58 | client = boto3.client('events', region_name='us-west-1') 59 | _put_event(client, fake_event) 60 | trace = _get_active_trace() 61 | assert trace.events[0].resource["metadata"]['aws.cloudwatch.detail_type'] == fake_event["DetailType"] 62 | assert trace.events[0].resource["metadata"]['aws.cloudwatch.source'] == fake_event["Source"] 63 | assert trace.events[0].resource["metadata"]['aws.cloudwatch.detail'] == fake_event["Detail"] 64 | -------------------------------------------------------------------------------- /tests/events/test_kafka.py: -------------------------------------------------------------------------------- 1 | import json 2 | import epsagon.wrappers.python_function 3 | import epsagon.runners.python_function 4 | import epsagon.constants 5 | import mock 6 | from kafka import KafkaProducer 7 | 8 | TEST_URL = 'https://example.test/' 9 | 10 | def record_mock(*args, **kwargs): 11 | return [{}, False, False] 12 | 13 | 14 | @mock.patch('epsagon.trace.TraceFactory.add_event') 15 | @mock.patch('kafka.producer.kafka.KafkaProducer._wait_on_metadata') 16 | @mock.patch('kafka.producer.kafka.KafkaProducer._partition') 17 | @mock.patch('kafka.producer.record_accumulator.RecordAccumulator.append', side_effect=record_mock) 18 | def test_sanity(append_mock, partition_mock, wait_on_metadata_mock, add_event_mock): 19 | retval = 'success' 20 | body = {'test': 1} 21 | 22 | @epsagon.wrappers.python_function.python_wrapper 23 | def wrapped_function(): 24 | producer = KafkaProducer( 25 | bootstrap_servers=['host:10'], 26 | client_id='test_client_id', 27 | api_version=(0, 11, 5), 28 | value_serializer=lambda x: json.dumps(x).encode('ascii'), 29 | ) 30 | response = producer.send('topic', body, headers=[('content-encoding', b'base64')]) 31 | return retval 32 | assert wrapped_function() == retval 33 | wait_on_metadata_mock.assert_called() 34 | partition_mock.assert_called() 35 | append_mock.assert_called() 36 | add_event_mock.assert_called() 37 | event = add_event_mock.call_args_list[0].args[0] 38 | assert event.resource['name'] == 'topic' 39 | assert event.resource['operation'] == 'send' 40 | assert event.resource['type'] == 'kafka' 41 | assert event.resource['metadata']['messaging.kafka.client_id'] == ( 42 | 'test_client_id' 43 | ) 44 | assert event.resource['metadata'][ 45 | 'messaging.message_payload_size_bytes' 46 | ] == len(str(body)) 47 | assert event.resource['metadata']['messaging.message'] == body 48 | assert ( 49 | epsagon.constants.EPSAGON_HEADER in 50 | event.resource['metadata']['messaging.headers'] 51 | ) 52 | 53 | 54 | @mock.patch('epsagon.trace.TraceFactory.add_event') 55 | @mock.patch('kafka.producer.kafka.KafkaProducer._wait_on_metadata') 56 | @mock.patch('kafka.producer.kafka.KafkaProducer._partition') 57 | @mock.patch('kafka.producer.record_accumulator.RecordAccumulator.append', side_effect=record_mock) 58 | def test_no_header_injection(append_mock, partition_mock, wait_on_metadata_mock, add_event_mock): 59 | # Verify header is not injected in older kafka api versions (V1) 60 | retval = 'success' 61 | body = {'test': 1} 62 | 63 | @epsagon.wrappers.python_function.python_wrapper 64 | def wrapped_function(): 65 | producer = KafkaProducer( 66 | bootstrap_servers=['host:10'], 67 | client_id='test_client_id', 68 | api_version=(0, 10, 0), 69 | value_serializer=lambda x: json.dumps(x).encode('ascii'), 70 | ) 71 | response = producer.send('topic', body) 72 | return retval 73 | assert wrapped_function() == retval 74 | wait_on_metadata_mock.assert_called() 75 | partition_mock.assert_called() 76 | append_mock.assert_called() 77 | add_event_mock.assert_called() 78 | event = add_event_mock.call_args_list[0].args[0] 79 | assert ( 80 | epsagon.constants.EPSAGON_HEADER not in 81 | event.resource['metadata'] 82 | ) 83 | -------------------------------------------------------------------------------- /tests/events/test_requests_event.py: -------------------------------------------------------------------------------- 1 | """ 2 | Requests tests 3 | """ 4 | import requests 5 | from epsagon.trace import trace_factory 6 | from epsagon.events.requests import RequestsEvent 7 | 8 | RESPONSE_DATA = b'test data' 9 | TEST_PATH = '/' 10 | 11 | def setup_function(): 12 | trace_factory.metadata_only = False 13 | trace_factory.use_single_trace = True 14 | trace_factory.get_or_create_trace() 15 | 16 | 17 | def teardown_function(): 18 | trace_factory.singleton_trace = None 19 | 20 | 21 | def _get_active_trace(): 22 | return trace_factory.active_trace 23 | 24 | def _validate_request_event_metadata(event): 25 | assert event.resource["operation"] == 'GET' 26 | assert event.RESOURCE_TYPE == 'http' 27 | 28 | 29 | def test_get_request_sanity(httpserver): 30 | """ 31 | Tests get request sanity 32 | """ 33 | httpserver.expect_request(TEST_PATH).respond_with_data(RESPONSE_DATA) 34 | response = requests.get(httpserver.url_for(TEST_PATH)) 35 | trace = _get_active_trace() 36 | event = trace.events[0] 37 | _validate_request_event_metadata(event) 38 | assert(response.content) == RESPONSE_DATA 39 | 40 | def test_get_request_stream(httpserver): 41 | """ 42 | Tests get request sanity 43 | """ 44 | httpserver.expect_request(TEST_PATH).respond_with_data(RESPONSE_DATA) 45 | response = requests.get(httpserver.url_for(TEST_PATH), stream=True) 46 | trace = _get_active_trace() 47 | event = trace.events[0] 48 | _validate_request_event_metadata(event) 49 | assert(response.raw.read()) == RESPONSE_DATA 50 | -------------------------------------------------------------------------------- /tests/events/test_tornado_client.py: -------------------------------------------------------------------------------- 1 | import epsagon.wrappers.python_function 2 | import epsagon.runners.python_function 3 | import epsagon.constants 4 | import mock 5 | from tornado.httpclient import AsyncHTTPClient 6 | 7 | TEST_URL = 'https://example.test/' 8 | 9 | 10 | @mock.patch('epsagon.trace.TraceFactory.add_event') 11 | def test_sanity(add_event_mock): 12 | retval = 'success' 13 | 14 | @epsagon.wrappers.python_function.python_wrapper 15 | def wrapped_function(): 16 | http_client = AsyncHTTPClient() 17 | http_client.fetch(TEST_URL) 18 | return retval 19 | assert wrapped_function() == retval 20 | add_event_mock.assert_called() 21 | event = add_event_mock.call_args_list[0].args[0] 22 | assert event.resource['name'] == 'example.test' 23 | assert event.resource['operation'] == 'GET' 24 | assert event.resource['type'] == 'http' 25 | assert 'http_trace_id' in event.resource['metadata'] 26 | -------------------------------------------------------------------------------- /tests/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/modules/__init__.py -------------------------------------------------------------------------------- /tests/modules/test_greengrasssdk.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from epsagon.events.greengrasssdk import GreengrassEventFactory 3 | 4 | 5 | @mock.patch('epsagon.trace.TraceFactory.add_event') 6 | def test_sanity(add_event_mock): 7 | params = { 8 | 'topic': 'name', 9 | 'queueFullPolicy': True, 10 | 'payload': 'test', 11 | } 12 | GreengrassEventFactory.create_event(None, None, None, params, None, None, None) 13 | add_event_mock.assert_called_once() 14 | event = add_event_mock.call_args_list[0].args[0] 15 | assert event.event_id.startswith('greengrass-') 16 | assert event.resource['name'] == 'name' 17 | assert event.resource['operation'] == 'publish' 18 | assert event.resource['metadata']['aws.greengrass.queueFullPolicy'] == True 19 | assert event.resource['metadata']['aws.greengrass.payload'] == 'test' 20 | -------------------------------------------------------------------------------- /tests/modules/test_logging.py: -------------------------------------------------------------------------------- 1 | import epsagon.wrappers.python_function 2 | from epsagon.trace import trace_factory 3 | import epsagon.runners.python_function 4 | import epsagon.constants 5 | import logging 6 | 7 | 8 | def setup_function(func): 9 | trace_factory.use_single_trace = True 10 | epsagon.constants.COLD_START = True 11 | 12 | 13 | def test_logging_exception_capture(trace_transport): 14 | retval = 'success' 15 | 16 | @epsagon.wrappers.python_function.python_wrapper 17 | def wrapped_function(event, context): 18 | logging.exception('test') 19 | return retval 20 | 21 | assert wrapped_function('a', 'b') == retval 22 | 23 | exception = trace_transport.last_trace.events[0].exception 24 | assert exception['type'] == 'Exception' 25 | assert exception['additional_data']['from_logs'] is True 26 | assert exception['message'] == 'test' 27 | 28 | 29 | def test_logging_exception_capture_with_args(trace_transport): 30 | retval = 'success' 31 | 32 | @epsagon.wrappers.python_function.python_wrapper 33 | def wrapped_function(event, context): 34 | logging.exception('test %s %s', 'test', 'test') 35 | return retval 36 | 37 | assert wrapped_function('a', 'b') == retval 38 | 39 | exception = trace_transport.last_trace.events[0].exception 40 | assert exception['type'] == 'Exception' 41 | assert exception['additional_data']['from_logs'] is True 42 | assert exception['message'] == 'test test test' 43 | -------------------------------------------------------------------------------- /tests/modules/test_requests.py: -------------------------------------------------------------------------------- 1 | import epsagon.wrappers.python_function 2 | from epsagon.trace import trace_factory 3 | import epsagon.runners.python_function 4 | import epsagon.constants 5 | import mock 6 | import requests 7 | 8 | TEST_URL = 'https://test.com/' 9 | 10 | def setup_function(func): 11 | trace_factory.use_single_trace = True 12 | epsagon.constants.COLD_START = True 13 | 14 | @mock.patch('urllib3.poolmanager.PoolManager.connection_from_url') 15 | def test_pool_manager_patching(pool_manager_mock): 16 | retval = 'success' 17 | 18 | @epsagon.wrappers.python_function.python_wrapper 19 | def wrapped_function(): 20 | requests.get(TEST_URL) 21 | return retval 22 | 23 | assert wrapped_function() == retval 24 | pool_manager_mock.assert_called_with(TEST_URL) 25 | -------------------------------------------------------------------------------- /tests/modules/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import epsagon.wrappers.python_function 2 | import epsagon.runners.python_function 3 | import epsagon.constants 4 | import mock 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | 9 | DB_NAME = 'db' 10 | HOST_NAME = 'host' 11 | ENGINE = create_engine('postgresql://user:password@{}/{}'.format(HOST_NAME, DB_NAME)) 12 | 13 | 14 | @mock.patch('epsagon.events.sqlalchemy.SqlAlchemyEvent') 15 | def test_sanity(sqlalchemy_event_mock): 16 | retval = 'success' 17 | 18 | @epsagon.wrappers.python_function.python_wrapper 19 | def wrapped_function(): 20 | session = sessionmaker(bind=ENGINE)() 21 | session.close() 22 | return retval 23 | 24 | assert wrapped_function() == retval 25 | init_instrumentation, close_instrumentation = [ 26 | call.args for call in sqlalchemy_event_mock.call_args_list 27 | ] 28 | 29 | assert init_instrumentation[-1] =='initialize' 30 | assert close_instrumentation[-1] == 'close' 31 | 32 | init_instrumentation[1].bind.url.database = DB_NAME 33 | init_instrumentation[1].bind.url.host = HOST_NAME 34 | close_instrumentation[1].bind.url.database = DB_NAME 35 | close_instrumentation[1].bind.url.host = HOST_NAME 36 | -------------------------------------------------------------------------------- /tests/test_epsagon_init.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test epsagon init 3 | """ 4 | import mock 5 | import epsagon 6 | import os 7 | from imp import reload 8 | from epsagon.trace_transports import HTTPTransport 9 | 10 | 11 | @mock.patch('epsagon.patcher.patch_all') 12 | @mock.patch('os.getenv', side_effect=(lambda x: { 13 | 'EPSAGON_HANDLER': None, 14 | 'DISABLE_EPSAGON': 'FALSE', 15 | 'DISABLE_EPSAGON_PATCH': 'FALSE', 16 | }[x])) 17 | def test_epsagon(wrapped_get, wrapped_patch): 18 | reload(epsagon) 19 | wrapped_get.assert_has_calls([ 20 | mock.call('DISABLE_EPSAGON'), 21 | mock.call('DISABLE_EPSAGON_PATCH'), 22 | ]) 23 | wrapped_patch.assert_called() 24 | 25 | 26 | @mock.patch('epsagon.patcher.patch_all') 27 | @mock.patch('os.getenv', side_effect=(lambda x: { 28 | 'EPSAGON_HANDLER': None, 29 | 'DISABLE_EPSAGON': 'FALSE', 30 | 'DISABLE_EPSAGON_PATCH': 'TRUE', 31 | }[x])) 32 | def test_epsagon_no_patch_env(wrapped_get, wrapped_patch): 33 | reload(epsagon) 34 | wrapped_get.assert_has_calls([ 35 | mock.call('DISABLE_EPSAGON'), 36 | mock.call('DISABLE_EPSAGON_PATCH'), 37 | ]) 38 | wrapped_patch.assert_not_called() 39 | 40 | 41 | @mock.patch('epsagon.patcher.patch_all') 42 | @mock.patch('os.getenv', side_effect=(lambda x: { 43 | 'EPSAGON_HANDLER': None, 44 | 'DISABLE_EPSAGON': 'TRUE', 45 | 'DISABLE_EPSAGON_PATCH': 'TRUE', 46 | }[x])) 47 | def test_epsagon_disable_epsagon_and_disable_patch(wrapped_get, wrapped_patch): 48 | reload(epsagon) 49 | wrapped_get.assert_has_calls([ 50 | mock.call('DISABLE_EPSAGON'), 51 | mock.call('DISABLE_EPSAGON_PATCH'), 52 | ]) 53 | wrapped_patch.assert_not_called() 54 | assert os.environ['DISABLE_EPSAGON_PATCH'] == 'TRUE' 55 | 56 | def dummy(): 57 | return True 58 | assert epsagon.lambda_wrapper(dummy) is dummy 59 | assert epsagon.step_lambda_wrapper(dummy) is dummy 60 | assert epsagon.azure_wrapper(dummy) is dummy 61 | assert epsagon.python_wrapper(dummy) is dummy 62 | assert epsagon.gcp_wrapper(dummy) is dummy 63 | 64 | 65 | default_http = HTTPTransport("epsagon", "1234") 66 | 67 | 68 | @mock.patch('epsagon.utils.create_transport', side_effect=lambda x, y: default_http) 69 | @mock.patch('epsagon.trace.TraceFactory.initialize') 70 | @mock.patch('os.getenv', side_effect=(lambda x: { 71 | 'EPSAGON_HANDLER': 'epsagon.lambda_wrapper', 72 | 'DISABLE_EPSAGON': 'FALSE', 73 | 'DISABLE_EPSAGON_PATCH': 'FALSE', 74 | 'EPSAGON_SSL': 'FALSE', 75 | 'EPSAGON_TOKEN': '1234', 76 | 'EPSAGON_APP_NAME': 'test', 77 | 'EPSAGON_COLLECTOR_URL': 'epsagon', 78 | 'EPSAGON_METADATA': 'TRUE', 79 | 'EPSAGON_DISABLE_ON_TIMEOUT': 'FALSE', 80 | 'EPSAGON_DEBUG': 'FALSE', 81 | 'EPSAGON_SEND_TRACE_ON_ERROR': 'FALSE', 82 | 'EPSAGON_URLS_TO_IGNORE': '', 83 | 'EPSAGON_IGNORED_KEYS': '', 84 | 'EPSAGON_ALLOWED_KEYS': '', 85 | 'EPSAGON_LOG_TRANSPORT': 'FALSE', 86 | 'EPSAGON_ENDPOINTS_TO_IGNORE': '', 87 | 'EPSAGON_SPLIT_ON_SEND': 'FALSE', 88 | 'EPSAGON_PROPAGATE_LAMBDA_ID': 'FALSE', 89 | 'EPSAGON_LOGGING_TRACING_ENABLED': 'TRUE', 90 | 'AWS_LAMBDA_FUNCTION_NAME': None, 91 | 'EPSAGON_STEPS_OUTPUT_PATH': '', 92 | 'EPSAGON_SAMPLE_RATE': 0.5 93 | }[x])) 94 | def test_epsagon_wrapper_env_init(_wrapped_get, wrapped_init, wrapped_create): 95 | reload(epsagon) 96 | epsagon.init() 97 | wrapped_init.assert_called_with( 98 | app_name='test', 99 | token='1234', 100 | collector_url='epsagon', 101 | metadata_only=True, 102 | disable_timeout_send=False, 103 | debug=False, 104 | send_trace_only_on_error=False, 105 | url_patterns_to_ignore=None, 106 | keys_to_ignore=None, 107 | keys_to_allow=None, 108 | transport=default_http, 109 | split_on_send=False, 110 | propagate_lambda_id=False, 111 | logging_tracing_enabled=True, 112 | step_dict_output_path=None, 113 | sample_rate=0.5, 114 | ) 115 | wrapped_create.assert_called_with("epsagon", "1234") 116 | 117 | 118 | @mock.patch('epsagon.http_filters.add_ignored_endpoints') 119 | @mock.patch('os.getenv', side_effect=(lambda x: { 120 | 'EPSAGON_HANDLER': 'epsagon.lambda_wrapper', 121 | 'DISABLE_EPSAGON': 'FALSE', 122 | 'DISABLE_EPSAGON_PATCH': 'FALSE', 123 | 'EPSAGON_SSL': 'FALSE', 124 | 'EPSAGON_TOKEN': '1234', 125 | 'EPSAGON_APP_NAME': 'test', 126 | 'EPSAGON_COLLECTOR_URL': 'epsagon', 127 | 'EPSAGON_METADATA': 'TRUE', 128 | 'EPSAGON_DISABLE_ON_TIMEOUT': 'FALSE', 129 | 'EPSAGON_DEBUG': 'FALSE', 130 | 'EPSAGON_SEND_TRACE_ON_ERROR': 'FALSE', 131 | 'EPSAGON_URLS_TO_IGNORE': '', 132 | 'EPSAGON_IGNORED_KEYS': '', 133 | 'EPSAGON_ALLOWED_KEYS': '', 134 | 'EPSAGON_STEPS_OUTPUT_PATH': '', 135 | 'EPSAGON_ENDPOINTS_TO_IGNORE': '/health,/test', 136 | 'EPSAGON_LOG_TRANSPORT': 'FALSE', 137 | 'EPSAGON_SPLIT_ON_SEND': 'FALSE', 138 | 'EPSAGON_PROPAGATE_LAMBDA_ID': 'FALSE', 139 | 'EPSAGON_LOGGING_TRACING_ENABLED': 'TRUE', 140 | 'AWS_LAMBDA_FUNCTION_NAME': None, 141 | 'EPSAGON_SAMPLE_RATE': 0.5 142 | }[x])) 143 | def test_epsagon_wrapper_env_endpoints(_wrapped_get, wrapped_http): 144 | reload(epsagon) 145 | epsagon.init() 146 | wrapped_http.assert_called_with(['/health', '/test']) 147 | -------------------------------------------------------------------------------- /tests/test_modules.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from epsagon.modules.botocore import _wrapper as _botocore_wrapper 4 | from epsagon.modules.requests import _wrapper as _request_wrapper 5 | from epsagon.modules.pymongo import _wrapper as _pymongo_wrapper 6 | from epsagon.trace import trace_factory 7 | 8 | EXCEPTION_MESSAGE = 'Test exception' 9 | EXCEPTION_TYPE = RuntimeError 10 | 11 | 12 | def raise_exception(*args): 13 | raise EXCEPTION_TYPE(EXCEPTION_MESSAGE) 14 | 15 | 16 | def _test(func): 17 | func(lambda: None, [], [], {}) 18 | trace = trace_factory.get_or_create_trace() 19 | assert len(trace.exceptions) == 1 20 | assert trace.exceptions[0]['message'] == EXCEPTION_MESSAGE 21 | assert trace.exceptions[0]['type'] == str(EXCEPTION_TYPE) 22 | assert 'time' in trace.exceptions[0].keys() 23 | assert len(trace.exceptions[0]['traceback']) > 0 24 | 25 | 26 | def setup_function(function): 27 | """Setup function that resets the tracer's exceptions list. 28 | """ 29 | trace_factory.get_or_create_trace().exceptions = [] 30 | 31 | 32 | @mock.patch( 33 | 'epsagon.events.requests.RequestsEventFactory.create_event', 34 | side_effect=raise_exception 35 | ) 36 | def test_request_wrapper_failsafe(_): 37 | """Validates that the request wrapper is not raising any exception to 38 | the user.""" 39 | _test(_request_wrapper) 40 | 41 | 42 | @mock.patch('epsagon.events.pymongo.PyMongoEventFactory.create_event', 43 | side_effect=raise_exception) 44 | def test_pymongo_wrapper_failsafe(_): 45 | """Validates that the pymongo wrapper is not raising any exception to 46 | the user.""" 47 | _test(_pymongo_wrapper) 48 | 49 | 50 | @mock.patch('epsagon.events.botocore.BotocoreEventFactory.create_event', 51 | side_effect=raise_exception) 52 | def test_botocore_wrapper_failsafe(_): 53 | """Validates that the botocore wrapper is not raising any exception to 54 | the user.""" 55 | _test(_botocore_wrapper) 56 | -------------------------------------------------------------------------------- /tests/test_patcher.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mock 3 | import collections 4 | import importlib 5 | from imp import reload 6 | 7 | 8 | os.environ['DISABLE_EPSAGON_PATCH'] = 'TRUE' 9 | import epsagon.patcher 10 | 11 | @mock.patch('epsagon.patcher.import_module', side_effect=[True]) 12 | @mock.patch('epsagon.modules') 13 | def test_patch_all(patched_modules, _): 14 | module_mock = mock.NonCallableMagicMock(patch=mock.MagicMock()) 15 | patched_modules.MODULES = {'test': module_mock} 16 | epsagon.patcher.patch_all() 17 | module_mock.patch.assert_called() 18 | 19 | @mock.patch('epsagon.patcher.import_module', side_effect=ImportError()) 20 | @mock.patch('epsagon.modules') 21 | def test_patch_all_import_error(patched_modules, _,): 22 | reload(importlib) 23 | module_mock = mock.NonCallableMagicMock(patch=mock.MagicMock()) 24 | patched_modules.MODULES = {'test': module_mock} 25 | epsagon.patcher.patch_all() 26 | module_mock.patch.assert_not_called() 27 | 28 | 29 | @mock.patch('epsagon.patcher.import_module') 30 | @mock.patch('epsagon.modules') 31 | def test_patch_all_import_ok_then_error(patched_modules, patched_import): 32 | def import_side_effect(): 33 | yield 'True' 34 | raise ImportError() 35 | 36 | patched_import.side_effect = import_side_effect() 37 | module1_mock = mock.NonCallableMagicMock(patch=mock.MagicMock()) 38 | module2_mock = mock.NonCallableMagicMock(patch=mock.MagicMock()) 39 | patched_modules.MODULES = collections.OrderedDict( 40 | [('module1', module1_mock, ), ('module2', module2_mock,)] 41 | ) 42 | 43 | epsagon.patcher.patch_all() 44 | module1_mock.patch.assert_called() 45 | module2_mock.patch.assert_not_called() 46 | 47 | -------------------------------------------------------------------------------- /tests/test_transports.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | import urllib3 4 | from epsagon.trace_transports import HTTPTransport 5 | from epsagon.trace import (trace_factory) 6 | 7 | 8 | def test_httptransport_sanity(httpserver): 9 | collector_url = '/collector' 10 | httpserver.expect_request(collector_url).respond_with_data("success") 11 | http_transport = HTTPTransport(httpserver.url_for(collector_url), 'token') 12 | trace = trace_factory.get_or_create_trace() 13 | http_transport.send(trace) 14 | 15 | 16 | def test_httptransport_timeout(): 17 | start_time = time.time() 18 | # non-routable IP address, will result in a timeout 19 | http_transport = HTTPTransport('http://10.255.255.1', 'token') 20 | trace = trace_factory.get_or_create_trace() 21 | 22 | # This will make sure we get TimeoutError and not MaxRetryError 23 | with pytest.raises(urllib3.exceptions.TimeoutError): 24 | http_transport.send(trace) 25 | 26 | duration = time.time() - start_time 27 | 28 | # Making sure that an unreachable url will result in duration almost equal to the 29 | # timeout duration set 30 | assert http_transport.timeout < duration < http_transport.timeout + 0.3 31 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import epsagon.trace 2 | import epsagon.utils 3 | import epsagon.http_filters 4 | from epsagon.trace import trace_factory 5 | 6 | 7 | def setup_function(func): 8 | trace_factory.get_or_create_trace() 9 | 10 | 11 | def test_blacklist_url(): 12 | """ 13 | Test is_blacklisted_url functionality. 14 | :return: None 15 | """ 16 | 17 | epsagon.http_filters.BLACKLIST_URLS = { 18 | str.endswith: [ 19 | '.com', 20 | ], 21 | str.__contains__: [ 22 | 'restricted', 23 | ], 24 | } 25 | 26 | assert epsagon.http_filters.is_blacklisted_url('http://www.google.com') 27 | assert epsagon.http_filters.is_blacklisted_url('https://www.restricted-site.org') 28 | assert epsagon.http_filters.is_blacklisted_url('http://www.restricted-site.com') 29 | assert epsagon.http_filters.is_blacklisted_url('file://test.file') 30 | assert not epsagon.http_filters.is_blacklisted_url('https://www.com.org') 31 | assert not epsagon.http_filters.is_blacklisted_url('http://www.google.org') 32 | 33 | 34 | def test_original_blacklist_url(): 35 | """ 36 | Validate original needed URLs are in. 37 | :return: None 38 | """ 39 | 40 | assert epsagon.http_filters.is_blacklisted_url('http://tc.us-east-1.epsagon.com') 41 | assert epsagon.http_filters.is_blacklisted_url('https://client.tc.epsagon.com') 42 | 43 | 44 | def test_trace_blacklist(): 45 | """ 46 | Validate trace URL Blacklist mechanism. 47 | :return: None 48 | """ 49 | trace_factory.get_trace().url_patterns_to_ignore = set(('test.net', 'test2.net')) 50 | assert epsagon.http_filters.is_payload_collection_blacklisted('http://www.test.net') 51 | assert epsagon.http_filters.is_payload_collection_blacklisted('http://www.bla.test.net') 52 | assert not epsagon.http_filters.is_payload_collection_blacklisted('http://www.test.new.net') 53 | trace_factory.get_trace().url_patterns_to_ignore = set() 54 | assert not epsagon.http_filters.is_payload_collection_blacklisted('http://www.test.net') 55 | assert not epsagon.http_filters.is_payload_collection_blacklisted('http://www.bla.test.net') 56 | -------------------------------------------------------------------------------- /tests/wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/wrappers/__init__.py -------------------------------------------------------------------------------- /tests/wrappers/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common tests helpers 3 | """ 4 | import mock 5 | import requests 6 | from threading import Thread 7 | 8 | 9 | def get_tracer_patch_kwargs(): 10 | return { 11 | 'metadata_only': False, 12 | 'disable_timeout_send': False, 13 | 'prepare': mock.MagicMock(), 14 | 'send_traces': mock.MagicMock(), 15 | 'events': [], 16 | 'add_event': mock.MagicMock(), 17 | 'add_exception': mock.MagicMock(), 18 | 'set_runner': mock.MagicMock() 19 | } 20 | 21 | 22 | def _send_get_request(target_url, results): 23 | """ 24 | Sends a get requests to a given target URL string 25 | :return: the given target url 26 | """ 27 | requests.get(target_url) 28 | results.append(target_url) 29 | 30 | 31 | def multiple_threads_handler(threads_count=3): 32 | """ 33 | Invokes `threads_count` new threads, each performs an HTTP get request. 34 | Waits for all threads and validates a result has been returned from each 35 | thread. 36 | """ 37 | threads = [] 38 | results = [] 39 | for i in range(threads_count): 40 | thread = Thread(target = _send_get_request, args = ("http://google.com", results)) 41 | thread.start() 42 | threads.append(thread) 43 | for thread in threads: 44 | thread.join() 45 | assert len(results) == threads_count 46 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/wrappers/django_test/db.sqlite3 -------------------------------------------------------------------------------- /tests/wrappers/django_test/django_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/wrappers/django_test/django_test/__init__.py -------------------------------------------------------------------------------- /tests/wrappers/django_test/django_test/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_test project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/django_test/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_test project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import epsagon 15 | 16 | epsagon.init( 17 | token='test-token', 18 | app_name='django-test', 19 | metadata_only=False, 20 | ) 21 | 22 | 23 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 24 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 25 | 26 | 27 | # Quick-start development settings - unsuitable for production 28 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 29 | 30 | # SECURITY WARNING: keep the secret key used in production secret! 31 | SECRET_KEY = '*21q4t60y)@x)mh8b*rs-33d@ot#v*vz=d1yh_naq-yxb)5dco' 32 | 33 | # SECURITY WARNING: don't run with debug turned on in production! 34 | DEBUG = True 35 | 36 | ALLOWED_HOSTS = [] 37 | 38 | 39 | # Application definition 40 | 41 | INSTALLED_APPS = [ 42 | 'django.contrib.admin', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.sessions', 46 | 'django.contrib.messages', 47 | 'django.contrib.staticfiles', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware' 58 | ] 59 | 60 | ROOT_URLCONF = 'django_test.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'django_test.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/django_test/urls.py: -------------------------------------------------------------------------------- 1 | """django_test URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path('polls/', include('polls.urls')), 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/django_test/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_test project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_test.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/wrappers/django_test/polls/__init__.py -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PollsConfig(AppConfig): 5 | name = 'polls' 6 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/tests/wrappers/django_test/polls/migrations/__init__.py -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django wrapper tests 3 | """ 4 | 5 | # Implement future tests -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('a', views.indexA, name='a'), 7 | path('b', views.indexB, name='b'), 8 | ] -------------------------------------------------------------------------------- /tests/wrappers/django_test/polls/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse 3 | 4 | 5 | def indexA(request): 6 | return HttpResponse("This is A") 7 | 8 | def indexB(request): 9 | return HttpResponse("This is B") 10 | -------------------------------------------------------------------------------- /tests/wrappers/django_test/stress.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import threading 4 | 5 | ANSWER_A = b'This is A' 6 | ANSWER_B = b'This is B' 7 | 8 | def threadA(): 9 | time.sleep(2) 10 | response = requests.get("http://127.0.0.1:8000/polls/a") 11 | if response.content != ANSWER_A: 12 | print("Bad answer in A!") 13 | print(response.content) 14 | 15 | def threadB(): 16 | time.sleep(2) 17 | response = requests.get("http://127.0.0.1:8000/polls/b") 18 | if response.content != ANSWER_B: 19 | print("Bad answer in B!") 20 | print(response.content) 21 | 22 | def main(): 23 | a_threads = [threading.Thread(target=threadA) for _ in range(20)] 24 | b_threads = [threading.Thread(target=threadB) for _ in range(20)] 25 | for a, b in zip(a_threads, b_threads): 26 | a.start() 27 | b.start() 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /tests/wrappers/test_custom.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import json 3 | import itertools 4 | import epsagon.constants 5 | 6 | 7 | @mock.patch( 8 | 'time.time', 9 | side_effect=itertools.count(start=1) 10 | ) 11 | def test_function_wrapper_sanity(_, trace_transport): 12 | retval = 'success' 13 | 14 | @epsagon.measure 15 | def measured_function(): 16 | return retval 17 | 18 | @epsagon.python_wrapper(name='test-func') 19 | def wrapped_function(): 20 | measured_function() 21 | return retval 22 | 23 | assert wrapped_function() == retval 24 | labels = json.loads(trace_transport.last_trace.events[0].resource['metadata']['labels']) 25 | assert labels['measured_function_duration'] == 1 26 | -------------------------------------------------------------------------------- /tests/wrappers/test_django_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test Django wrapper functionality 3 | """ 4 | 5 | import pytest 6 | import mock 7 | from epsagon.wrappers.django import DjangoRequestMiddleware 8 | 9 | 10 | TEST_BODY = 'test_body' 11 | TEST_METHOD = 'test_method' 12 | TEST_PATH = '/test_path' 13 | 14 | 15 | @pytest.fixture 16 | def test_request(): 17 | """ A test request """ 18 | class TestRequest: 19 | def __init__(self, path, method, body): 20 | self.path = path 21 | self.method = method 22 | self.body = body 23 | 24 | def get_host(self): 25 | return "test" 26 | 27 | return TestRequest( 28 | TEST_PATH, 29 | TEST_METHOD, 30 | TEST_BODY 31 | ) 32 | 33 | 34 | @mock.patch('epsagon.triggers.http.HTTPTriggerFactory') 35 | @mock.patch('epsagon.runners.django.DjangoRunner') 36 | @mock.patch('time.time', return_value=1) 37 | def test_before_request(_, runner_mock, trigger_mock, test_request): 38 | """ 39 | Test before_request generates a runner and a trigger. 40 | """ 41 | request_mw = DjangoRequestMiddleware(test_request) 42 | 43 | request_mw.before_request() 44 | trigger_mock.factory.assert_called_once() 45 | 46 | runner_mock.assert_called_with(1, test_request) 47 | 48 | 49 | @mock.patch('epsagon.triggers.http.HTTPTriggerFactory') 50 | @mock.patch('epsagon.runners.django.DjangoRunner.update_response') 51 | @mock.patch('time.time', return_value=1) 52 | def test_after_request( 53 | _, runner_mock, trigger_mock, test_request, trace_transport 54 | ): 55 | """ 56 | Test the whole flow - a trace is generated with the 57 | right content. 58 | """ 59 | request_mw = DjangoRequestMiddleware(test_request) 60 | 61 | request_mw.before_request() 62 | request_mw.after_request({"content": "bla"}) 63 | 64 | runner_mock.assert_called_once() 65 | trigger_mock.factory.assert_called_once() 66 | 67 | assert trace_transport.last_trace.events[ 68 | 0].resource['metadata']['Request Data'] == TEST_BODY 69 | assert trace_transport.last_trace.events[ 70 | 0].resource['metadata']['Path'] == TEST_PATH 71 | 72 | 73 | @mock.patch('epsagon.triggers.http.HTTPTriggerFactory') 74 | @mock.patch('epsagon.runners.django.DjangoRunner') 75 | @mock.patch('time.time', return_value=1) 76 | def test_ignored_path(_, runner_mock, trigger_mock, test_request): 77 | """ 78 | Make sure we don't capture ignored pathes 79 | """ 80 | ignored_request = test_request 81 | # JS files should be ignored 82 | ignored_request.path = "*.js" 83 | request_mw = DjangoRequestMiddleware(ignored_request) 84 | 85 | request_mw.before_request() 86 | trigger_mock.factory.assert_not_called() 87 | -------------------------------------------------------------------------------- /tests/wrappers/test_python_function.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import mock 3 | import pytest 4 | from collections import namedtuple 5 | from epsagon import trace_factory 6 | import epsagon.constants 7 | 8 | trace_mock = mock.MagicMock() 9 | 10 | Frame = namedtuple('Frame', 'f_locals') 11 | FrameInfo = namedtuple( 12 | 'FrameInfo', 13 | 'frame, filename, lineno, function, code_context, index' 14 | ) 15 | 16 | def setup_function(func): 17 | trace_factory.use_single_trace = True 18 | 19 | 20 | def test_function_wrapper_sanity(trace_transport, ): 21 | retval = 'success' 22 | 23 | @epsagon.python_wrapper(name='test-func') 24 | def wrapped_function(event, context): 25 | return retval 26 | 27 | assert wrapped_function('a', 'b') == 'success' 28 | 29 | assert len(trace_transport.last_trace.events) == 1 30 | 31 | event = trace_transport.last_trace.events[0] 32 | assert event.resource['type'] == 'python_function' 33 | assert event.resource['name'] == 'test-func' 34 | assert event.resource['metadata']['python.function.return_value'] == retval 35 | assert event.error_code == 0 36 | 37 | 38 | def test_function_wrapper_function_exception(trace_transport): 39 | @epsagon.python_wrapper() 40 | def wrapped_function(event, context): 41 | raise TypeError('test') 42 | 43 | with pytest.raises(TypeError): 44 | wrapped_function('a', 'b') 45 | 46 | assert len(trace_transport.last_trace.events) == 1 47 | 48 | event = trace_transport.last_trace.events[0] 49 | assert event.exception['type'] == 'TypeError' 50 | assert event.resource['metadata']['python.function.return_value'] is None 51 | assert not trace_transport.last_trace.exceptions 52 | 53 | 54 | @mock.patch( 55 | 'epsagon.trace.trace_factory.get_or_create_trace', 56 | side_effect=lambda: trace_mock 57 | ) 58 | def test_python_wrapper_python_runner_factory_failed(_): 59 | @epsagon.python_wrapper 60 | def wrapped_function(event, context): 61 | return 'success' 62 | 63 | with mock.patch( 64 | 'epsagon.runners.python_function.PythonRunner', 65 | side_effect=TypeError() 66 | ): 67 | assert wrapped_function('a', 'b') == 'success' 68 | 69 | trace_mock.prepare.assert_called_once() 70 | trace_mock.send_traces.assert_not_called() 71 | trace_mock.set_runner.assert_not_called() 72 | trace_mock.reset_mock() 73 | 74 | 75 | @mock.patch( 76 | 'inspect.trace', 77 | return_value=[FrameInfo( 78 | frame=Frame(f_locals={'param': 'value'}), 79 | filename='filename', 80 | lineno=1, 81 | function='function', 82 | code_context=['code_context'], 83 | index=0, 84 | )] 85 | ) 86 | def test_function_wrapper_function_exception_frames(_, trace_transport): 87 | @epsagon.python_wrapper() 88 | def wrapped_function(): 89 | raise TypeError('test') 90 | 91 | with pytest.raises(TypeError): 92 | wrapped_function() 93 | 94 | assert len(trace_transport.last_trace.events) == 1 95 | event = trace_transport.last_trace.events[0] 96 | if sys.version_info.major == 3: 97 | assert event.exception['frames'] == { 98 | 'filename/function/1': {'param': 'value'} 99 | } 100 | 101 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py36 3 | 4 | [testenv] 5 | deps = 6 | -r requirements-dev.txt 7 | commands = 8 | pytest 9 | -------------------------------------------------------------------------------- /trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-python/91e28fe43bc4f42152fb156145088cb8c9f69b85/trace.png --------------------------------------------------------------------------------