├── .gitignore ├── LICENSE ├── README.md ├── autodynatrace ├── __about__.py ├── __init__.py ├── log.py ├── sdk.py └── wrappers │ ├── __init__.py │ ├── aiohttp │ ├── __init__.py │ └── wrapper.py │ ├── asyncio │ ├── __init__.py │ └── wrapper.py │ ├── bottle │ ├── __init__.py │ └── wrapper.py │ ├── celery │ ├── __init__.py │ └── wrapper.py │ ├── concurrent │ ├── __init__.py │ └── wrapper.py │ ├── confluent_kafka │ ├── __init__.py │ └── wrapper.py │ ├── custom │ ├── __init__.py │ └── wrapper.py │ ├── cx_Oracle │ ├── __init__.py │ └── wrapper.py │ ├── dbapi │ └── __init__.py │ ├── django │ ├── __init__.py │ ├── apps.py │ ├── db.py │ ├── middlewares.py │ ├── utils.py │ └── wrapper.py │ ├── fastapi │ ├── __init__.py │ ├── middleware.py │ └── wrapper.py │ ├── flask │ ├── __init__.py │ └── wrapper.py │ ├── grpc │ ├── __init__.py │ └── wrapper.py │ ├── paramiko │ ├── __init__.py │ └── wrapper.py │ ├── pika │ ├── __init__.py │ └── wrapper.py │ ├── psycopg2 │ ├── __init__.py │ └── wrapper.py │ ├── pymongo │ ├── __init__.py │ └── wrapper.py │ ├── pysnmp │ ├── __init__.py │ └── wrapper.py │ ├── redis │ ├── __init__.py │ ├── utils.py │ └── wrapper.py │ ├── ruxit │ ├── __init__.py │ └── wrapper.py │ ├── sqlalchemy │ ├── __init__.py │ └── wrapper.py │ ├── starlette │ ├── __init__.py │ └── wrapper.py │ ├── subprocess │ ├── __init__.py │ └── wrapper.py │ ├── suds │ ├── __init__.py │ └── wrapper.py │ ├── tornado │ ├── __init__.py │ └── wrapper.py │ ├── urllib │ ├── __init__.py │ └── wrapper.py │ └── utils.py ├── pyproject.toml └── tests ├── __init__.py ├── test_custom.py ├── test_django.py ├── test_motor.py └── test_psycopg2.py /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .vscode 3 | .pytest_cache 4 | __pycache__ 5 | *.pyc 6 | .tox/ 7 | autodynatrace.egg-info/ 8 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Files: All contained files that aren't explicitly mentioned below 2 | Library: autodynatrace 3 | License: 4 | 5 | Apache License 6 | Version 2.0, January 2004 7 | http://www.apache.org/licenses/ 8 | 9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, 14 | and distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by 17 | the copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all 20 | other entities that control, are controlled by, or are under common 21 | control with that entity. For the purposes of this definition, 22 | "control" means (i) the power, direct or indirect, to cause the 23 | direction or management of such entity, whether by contract or 24 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 25 | outstanding shares, or (iii) beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity 28 | exercising permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation 32 | source, and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but 36 | not limited to compiled object code, generated documentation, 37 | and conversions to other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or 40 | Object form, made available under the License, as indicated by a 41 | copyright notice that is included in or attached to the work 42 | (an example is provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object 45 | form, that is based on (or derived from) the Work and for which the 46 | editorial revisions, annotations, elaborations, or other modifications 47 | represent, as a whole, an original work of authorship. For the purposes 48 | of this License, Derivative Works shall not include works that remain 49 | separable from, or merely link (or bind by name) to the interfaces of, 50 | the Work and Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including 53 | the original version of the Work and any modifications or additions 54 | to that Work or Derivative Works thereof, that is intentionally 55 | submitted to Licensor for inclusion in the Work by the copyright owner 56 | or by an individual or Legal Entity authorized to submit on behalf of 57 | the copyright owner. For the purposes of this definition, "submitted" 58 | means any form of electronic, verbal, or written communication sent 59 | to the Licensor or its representatives, including but not limited to 60 | communication on electronic mailing lists, source code control systems, 61 | and issue tracking systems that are managed by, or on behalf of, the 62 | Licensor for the purpose of discussing and improving the Work, but 63 | excluding communication that is conspicuously marked or otherwise 64 | designated in writing by the copyright owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity 67 | on behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of 71 | this License, each Contributor hereby grants to You a perpetual, 72 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 73 | copyright license to reproduce, prepare Derivative Works of, 74 | publicly display, publicly perform, sublicense, and distribute the 75 | Work and such Derivative Works in Source or Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | (except as stated in this section) patent license to make, have made, 81 | use, offer to sell, sell, import, and otherwise transfer the Work, 82 | where such license applies only to those patent claims licensable 83 | by such Contributor that are necessarily infringed by their 84 | Contribution(s) alone or by combination of their Contribution(s) 85 | with the Work to which such Contribution(s) was submitted. If You 86 | institute patent litigation against any entity (including a 87 | cross-claim or counterclaim in a lawsuit) alleging that the Work 88 | or a Contribution incorporated within the Work constitutes direct 89 | or contributory patent infringement, then any patent licenses 90 | granted to You under this License for that Work shall terminate 91 | as of the date such litigation is filed. 92 | 93 | 4. Redistribution. You may reproduce and distribute copies of the 94 | Work or Derivative Works thereof in any medium, with or without 95 | modifications, and in Source or Object form, provided that You 96 | meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or 99 | Derivative Works a copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices 102 | stating that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works 105 | that You distribute, all copyright, patent, trademark, and 106 | attribution notices from the Source form of the Work, 107 | excluding those notices that do not pertain to any part of 108 | the Derivative Works; and 109 | 110 | (d) If the Work includes a "NOTICE" text file as part of its 111 | distribution, then any Derivative Works that You distribute must 112 | include a readable copy of the attribution notices contained 113 | within such NOTICE file, excluding those notices that do not 114 | pertain to any part of the Derivative Works, in at least one 115 | of the following places: within a NOTICE text file distributed 116 | as part of the Derivative Works; within the Source form or 117 | documentation, if provided along with the Derivative Works; or, 118 | within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents 120 | of the NOTICE file are for informational purposes only and 121 | do not modify the License. You may add Your own attribution 122 | notices within Derivative Works that You distribute, alongside 123 | or as an addendum to the NOTICE text from the Work, provided 124 | that such additional attribution notices cannot be construed 125 | as modifying the License. 126 | 127 | You may add Your own copyright statement to Your modifications and 128 | may provide additional or different license terms and conditions 129 | for use, reproduction, or distribution of Your modifications, or 130 | for any such Derivative Works as a whole, provided Your use, 131 | reproduction, and distribution of the Work otherwise complies with 132 | the conditions stated in this License. 133 | 134 | 5. Submission of Contributions. Unless You explicitly state otherwise, 135 | any Contribution intentionally submitted for inclusion in the Work 136 | by You to the Licensor shall be under the terms and conditions of 137 | this License, without any additional terms or conditions. 138 | Notwithstanding the above, nothing herein shall supersede or modify 139 | the terms of any separate license agreement you may have executed 140 | with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade 143 | names, trademarks, service marks, or product names of the Licensor, 144 | except as required for reasonable and customary use in describing the 145 | origin of the Work and reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or 148 | agreed to in writing, Licensor provides the Work (and each 149 | Contributor provides its Contributions) on an "AS IS" BASIS, 150 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 151 | implied, including, without limitation, any warranties or conditions 152 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 153 | PARTICULAR PURPOSE. You are solely responsible for determining the 154 | appropriateness of using or redistributing the Work and assume any 155 | risks associated with Your exercise of permissions under this License. 156 | 157 | 8. Limitation of Liability. In no event and under no legal theory, 158 | whether in tort (including negligence), contract, or otherwise, 159 | unless required by applicable law (such as deliberate and grossly 160 | negligent acts) or agreed to in writing, shall any Contributor be 161 | liable to You for damages, including any direct, indirect, special, 162 | incidental, or consequential damages of any character arising as a 163 | result of this License or out of the use or inability to use the 164 | Work (including but not limited to damages for loss of goodwill, 165 | work stoppage, computer failure or malfunction, or any and all 166 | other commercial damages or losses), even if such Contributor 167 | has been advised of the possibility of such damages. 168 | 169 | 9. Accepting Warranty or Additional Liability. While redistributing 170 | the Work or Derivative Works thereof, You may choose to offer, 171 | and charge a fee for, acceptance of support, warranty, indemnity, 172 | or other liability obligations and/or rights consistent with this 173 | License. However, in accepting such obligations, You may act only 174 | on Your own behalf and on Your sole responsibility, not on behalf 175 | of any other Contributor, and only if You agree to indemnify, 176 | defend, and hold each Contributor harmless for any liability 177 | incurred by, or claims asserted against, such Contributor by reason 178 | of your accepting any such warranty or additional liability. 179 | 180 | END OF TERMS AND CONDITIONS 181 | 182 | APPENDIX: How to apply the Apache License to your work. 183 | 184 | To apply the Apache License to your work, attach the following 185 | boilerplate notice, with the fields enclosed by brackets "[]" 186 | replaced with your own identifying information. (Don't include 187 | the brackets!) The text should be enclosed in the appropriate 188 | comment syntax for the file format. We also recommend that a 189 | file or class name and description of purpose be included on the 190 | same "printed page" as the copyright notice for easier 191 | identification within third-party archives. 192 | 193 | Copyright [yyyy] [name of copyright owner] 194 | 195 | Licensed under the Apache License, Version 2.0 (the "License"); 196 | you may not use this file except in compliance with the License. 197 | You may obtain a copy of the License at 198 | 199 | http://www.apache.org/licenses/LICENSE-2.0 200 | 201 | Unless required by applicable law or agreed to in writing, software 202 | distributed under the License is distributed on an "AS IS" BASIS, 203 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 204 | See the License for the specific language governing permissions and 205 | limitations under the License. 206 | 207 | =============================================================================== 208 | 209 | This software uses and contains code from the following 3rd party library: 210 | 211 | File: src/oneagent/_impl/six.py 212 | Library: six (http://six.readthedocs.io/) 213 | Copyright notice: 214 | 215 | """Utilities for writing code that runs on Python 2 and 3""" 216 | 217 | # Copyright (c) 2010-2015 Benjamin Peterson 218 | # 219 | # Permission is hereby granted, free of charge, to any person obtaining a copy 220 | # of this software and associated documentation files (the "Software"), to deal 221 | # in the Software without restriction, including without limitation the rights 222 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 223 | # copies of the Software, and to permit persons to whom the Software is 224 | # furnished to do so, subject to the following conditions: 225 | # 226 | # The above copyright notice and this permission notice shall be included in all 227 | # copies or substantial portions of the Software. 228 | # 229 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 230 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 231 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 232 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 233 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 234 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 235 | # SOFTWARE. 236 | 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## autodynatrace - OneAgent-SDK-Python-AutoInstrumentation 2 | 3 | 4 | [![Actions Status](https://github.com/dynatrace-oss/OneAgent-SDK-Python-AutoInstrumentation/workflows/Tests/badge.svg)](https://github.com/dynatrace-oss/OneAgent-SDK-Python-AutoInstrumentation/actions) 5 | [![Downloads](https://pepy.tech/badge/autodynatrace)](https://pepy.tech/project/autodynatrace) 6 | 7 | Dynatrace provides a [powerful SDK](https://github.com/Dynatrace/OneAgent-SDK-for-Python) that can be used to achieve code level visibility and transaction tracing for applications written in multiple languages, including python. This project provides a library called *autodynatrace*, which is a wrapper around the OneAgent SDK for Python and allows you to instrument python applications with minimal code changes. 8 | 9 | 10 | > **Warning** 11 | > Autodynatrace is opensource and supported via github issues, this is not supported by Dynatrace via support tickets. 12 | 13 | ### Usage 14 | 15 | `pip install autodynatrace` 16 | 17 | ### Option 1 - Instrumentation without code changes 18 | 19 | Add the environment variable `AUTOWRAPT_BOOTSTRAP=autodynatrace` to your python processes 20 | 21 | ### Option 2 - Semi-Auto Instrumentation 22 | 23 | For most technologies, just import it in your code. 24 | 25 | `import autodynatrace` 26 | 27 | ### Technologies supported: 28 | 29 | - **aiohttp** (client) 30 | - **asyncio** - Run with the environment variable `AUTODYNATRACE_INSTRUMENT_CONCURRENT=True` 31 | - **bottle** 32 | - **celery** 33 | - **concurrent.futures** 34 | - **confluent_kafka** 35 | - **cx_Oracle** 36 | - **django** 37 | - **fastapi** 38 | - **flask** 39 | - **grpc** (client) 40 | - **paramiko** 41 | - **pika** (RabbitMQ) 42 | - **psycopg2** 43 | - **pymongo** 44 | - **pysnmp** 45 | - **redis** 46 | - **ruxit** (Dynatrace plugin framework) 47 | - **sqlalchemy** 48 | - **subprocess** 49 | - **suds** 50 | - **starlette** 51 | - **tornado** 52 | - **urllib** 53 | - **urllib3** 54 | - **custom annotations** 55 | 56 | ### Django 57 | 58 | For Django, add `"autodynatrace.wrappers.django"` to `INSTALLED_APPS` 59 | 60 | ### Environment variables 61 | 62 | * `AUTODYNATRACE_CAPTURE_HEADERS`: Default: `False`, set to `True` to capture request headers 63 | * `AUTODYNATRACE_LOG_LEVEL`: Default `WARNING` 64 | * `AUTODYNATRACE_FORKABLE`: Default `False`, set to `True` to [instrument forked processes](https://github.com/Dynatrace/OneAgent-SDK-for-Python#using-the-oneagent-sdk-for-python-with-forked-child-processes-only-available-on-linux). Use this for gunicorn/uwsgi 65 | * `AUTODYNATRACE_VIRTUAL_HOST`: Overwrite the default Virtual Host for web frameworks 66 | * `AUTODYNATRACE_APPLICATION_ID`: Overwrite the default Application Name for web frameworks 67 | * `AUTODYNATRACE_CONTEXT_ROOT`: Overwrite the default Context Root for web frameworks 68 | * `AUTODYNATRACE_CUSTOM_SERVICE_NAME`: Overwrite the custom service name (used by `@autodynatrace.trace`) 69 | * `AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN`: Default `False`, set to `True` to use fully qualified names for service and method names in custom traced services 70 | * `AUTODYNATRACE_INSTRUMENT_`: If set to `False`, Disables the instrumentation for a specific lib, example: `AUTODYNATRACE_INSTRUMENT_CONCURRENT=False`, default is `True` 71 | 72 | ### Support 73 | 74 | For support using this open source project, please open a github issue explaining your issue and providing code examples, environment details 75 | -------------------------------------------------------------------------------- /autodynatrace/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.1.0" 2 | -------------------------------------------------------------------------------- /autodynatrace/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import logging 3 | import threading 4 | import os 5 | import sys 6 | 7 | from wrapt.importer import when_imported 8 | 9 | from .log import init as log_init, logger 10 | from .sdk import init as sdk_init 11 | from .__about__ import __version__ 12 | 13 | dt_custom_prop = os.environ.get("DT_CUSTOM_PROP") 14 | if dt_custom_prop: 15 | os.environ["DT_CUSTOM_PROP"] = "{} Autodynatrace={}".format(dt_custom_prop, reversed) 16 | else: 17 | os.environ["DT_CUSTOM_PROP"] = "Autodynatrace={}".format(__version__) 18 | 19 | 20 | log_level_name = os.environ.get("AUTODYNATRACE_LOG_LEVEL", "WARNING") 21 | log_init(logging.getLevelName(log_level_name)) 22 | 23 | forkable = os.environ.get("AUTODYNATRACE_FORKABLE", "False").strip().lower() in ("true", "1") 24 | sdk_init(forkable=forkable) 25 | 26 | from .wrappers.custom import dynatrace_custom_tracer as trace 27 | 28 | _LOCK = threading.Lock() 29 | _INSTRUMENTED_LIBS = set() 30 | _INSTRUMENT_LIBS_LAZY = set() 31 | 32 | INSTRUMENT_LIBS = { 33 | "flask": True, 34 | "celery": True, 35 | "pymongo": True, 36 | "sqlalchemy": True, 37 | "django": True, 38 | "redis": True, 39 | "pika": True, 40 | "cx_Oracle": True, 41 | "grpc": True, 42 | "ruxit": True, 43 | "confluent_kafka": True, 44 | "pysnmp": True, 45 | "concurrent": False, 46 | "urllib": True, 47 | "suds": True, 48 | "subprocess": True, 49 | "paramiko": True, 50 | "psycopg2": True, 51 | "tornado": True, 52 | "fastapi": True, 53 | "starlette": True, 54 | "aiohttp": True, 55 | "bottle": True, 56 | "asyncio": True, 57 | } 58 | 59 | 60 | def will_instrument(lib_name, default): 61 | # Check if the user has chose to forcefully instrument (or not instrument) this lib 62 | lib_environment_variable_name = "AUTODYNATRACE_INSTRUMENT_{}".format(lib_name.upper()) 63 | 64 | if lib_environment_variable_name not in os.environ: 65 | # If the environment variable was not set, return the default instrumentation setting 66 | return default 67 | 68 | return os.environ.get(lib_environment_variable_name, "True").strip().lower() in ("true", "1") 69 | 70 | 71 | def load(_): 72 | pass 73 | 74 | 75 | def instrument_all(): 76 | libs = INSTRUMENT_LIBS.copy() 77 | instrument(libs) 78 | 79 | 80 | def _on_import_wrapper(lib): 81 | def on_import(hook): 82 | 83 | path = "autodynatrace.wrappers.%s" % lib 84 | try: 85 | logger.debug("Instrumenting imported lib '{}'".format(lib)) 86 | imported_module = importlib.import_module(path) 87 | imported_module.instrument() 88 | except Exception: 89 | logger.debug("Could not instrument {}".format(lib), exc_info=True) 90 | 91 | return on_import 92 | 93 | 94 | def instrument(instrument_libs): 95 | 96 | for lib_name, default in instrument_libs.items(): 97 | 98 | if will_instrument(lib_name, default): 99 | 100 | if lib_name in sys.modules: 101 | instrument_lib(lib_name) 102 | _INSTRUMENTED_LIBS.add(lib_name) 103 | 104 | else: 105 | when_imported(lib_name)(_on_import_wrapper(lib_name)) 106 | _INSTRUMENT_LIBS_LAZY.add(lib_name) 107 | 108 | patched_libs = get_already_instrumented() 109 | lazy_libs = get_will_instrument() 110 | logger.info( 111 | "Instrumented {}/{} libraries ({}). Will instrument when imported: ({})".format( 112 | len(patched_libs), len(instrument_libs), ", ".join(patched_libs), ", ".join(lazy_libs) 113 | ) 114 | ) 115 | 116 | 117 | def instrument_lib(lib): 118 | try: 119 | logger.debug("Attempting to instrument %s", lib) 120 | return _instrument_lib(lib) 121 | except Exception: 122 | logger.debug("Failed to instrument %s", lib, exc_info=True) 123 | return False 124 | 125 | 126 | def _instrument_lib(lib): 127 | 128 | path = "autodynatrace.wrappers.%s" % lib 129 | with _LOCK: 130 | if lib in _INSTRUMENTED_LIBS and lib: 131 | logger.debug("Skipping (already instrumented): {}".format(path)) 132 | return False 133 | 134 | imported_module = importlib.import_module(path) 135 | imported_module.instrument() 136 | _INSTRUMENTED_LIBS.add(lib) 137 | return True 138 | 139 | 140 | def get_already_instrumented(): 141 | with _LOCK: 142 | return sorted(_INSTRUMENTED_LIBS) 143 | 144 | 145 | def get_will_instrument(): 146 | with _LOCK: 147 | return sorted(_INSTRUMENT_LIBS_LAZY) 148 | 149 | 150 | instrument_all() 151 | -------------------------------------------------------------------------------- /autodynatrace/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("autodynatrace") 4 | 5 | 6 | def init(level=logging.INFO): 7 | ch = logging.StreamHandler() 8 | f = logging.Formatter( 9 | "%(asctime)s: %(process)d %(levelname)s %(name)s - %(funcName)s: %(message)s" 10 | ) 11 | ch.setFormatter(f) 12 | logger.addHandler(ch) 13 | logger.setLevel(level) 14 | -------------------------------------------------------------------------------- /autodynatrace/sdk.py: -------------------------------------------------------------------------------- 1 | import oneagent 2 | import atexit 3 | 4 | from .log import logger 5 | 6 | sdk = None 7 | 8 | 9 | def shutdown(): 10 | oneagent.shutdown() 11 | 12 | 13 | def init(forkable=False): 14 | global sdk 15 | oneagent.initialize(forkable=forkable) 16 | state = oneagent.get_sdk().agent_state 17 | logger.debug("Initialized autodynatrace with AgentState: {}".format(state)) 18 | if state != oneagent.common.AgentState.ACTIVE: 19 | logger.warning("Could not initialize the OneAgent SDK, AgentState: {}".format(state)) 20 | 21 | atexit.register(shutdown) 22 | sdk = oneagent.get_sdk() 23 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynatrace-oss/OneAgent-SDK-Python-AutoInstrumentation/77d817d8f96cfde7341471f9cc5af81f79de4b04/autodynatrace/wrappers/__init__.py -------------------------------------------------------------------------------- /autodynatrace/wrappers/aiohttp/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/aiohttp/wrapper.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import wrapt 3 | 4 | from oneagent.common import DYNATRACE_HTTP_HEADER_NAME 5 | from ...log import logger 6 | from ...sdk import sdk 7 | 8 | 9 | def instrument(): 10 | @wrapt.patch_function_wrapper("aiohttp.client", "ClientSession._request") 11 | async def dynatrace_request(wrapped, instance, args, kwargs): 12 | 13 | method = args[0] 14 | url = str(args[1]) 15 | headers = dict(kwargs.get("headers", {})) 16 | 17 | with sdk.trace_outgoing_web_request(url, method, headers) as tracer: 18 | tag = tracer.outgoing_dynatrace_string_tag.decode() 19 | logger.debug("dynatrace - tracing {} '{}' with tag '{}'".format(method, url, tag)) 20 | headers.update({DYNATRACE_HTTP_HEADER_NAME: tag}) 21 | kwargs["headers"] = headers 22 | 23 | response = await wrapped(*args, **kwargs) 24 | 25 | tracer.set_status_code(response.status) 26 | tracer.add_response_headers(dict(response.headers)) 27 | 28 | return response 29 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument -------------------------------------------------------------------------------- /autodynatrace/wrappers/asyncio/wrapper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Coroutine 3 | 4 | import wrapt 5 | 6 | from ...log import logger 7 | from ...sdk import sdk 8 | 9 | 10 | def instrument(): 11 | 12 | @wrapt.patch_function_wrapper("asyncio.tasks", "ensure_future") 13 | def dynatrace_coroutine(wrapped, instance, args, kwargs): 14 | if args and len(args) > 0: 15 | first_arg = args[0] 16 | if asyncio.iscoroutine(first_arg): 17 | args = (trace_coro(first_arg),) + args[1:] 18 | 19 | return wrapped(*args, **kwargs) 20 | 21 | async def trace_coro(coro: Coroutine): 22 | name = coro.__qualname__ 23 | with sdk.trace_custom_service(name, "asyncio") as tracer: 24 | try: 25 | logger.debug(f"tracing asyncio.tasks.ensure_future: {name}") 26 | return await coro 27 | finally: 28 | logger.debug(f"finished asyncio.tasks.ensure_future: {name}") 29 | 30 | @wrapt.patch_function_wrapper("asyncio.base_events", "BaseEventLoop.run_until_complete") 31 | def dynatrace_run_until_complete(wrapped, instance, args, kwargs): 32 | with sdk.trace_custom_service("run_until_complete", "asyncio"): 33 | return wrapped(*args, **kwargs) 34 | 35 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/bottle/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/bottle/wrapper.py: -------------------------------------------------------------------------------- 1 | from ...log import logger 2 | from ...sdk import sdk 3 | 4 | import os 5 | 6 | 7 | from bottle import hook, request, response 8 | 9 | 10 | def instrument(): 11 | @hook("before_request") 12 | def instrument_before_request(): 13 | try: 14 | # extract host and port from the request 15 | host = request.environ.get("HTTP_HOST", "unknown") 16 | app_name = "{}".format(host) 17 | 18 | virtual_host = os.environ.get("AUTODYNATRACE_VIRTUAL_HOST", "{}".format(host)) 19 | app_name = os.environ.get("AUTODYNATRACE_APPLICATION_ID", "Bottle ({})".format(app_name)) 20 | context_root = os.environ.get("AUTODYNATRACE_CONTEXT_ROOT", "/") 21 | 22 | # Create the oneagent web app 23 | web_app_info = sdk.create_web_application_info(virtual_host, app_name, context_root) 24 | 25 | with web_app_info: 26 | # Attempt to extract the x-dynatrace header from the request 27 | dynatrace_header = request.headers.get("x-dynatrace") 28 | logger.debug("Bottle - tracing incoming request ({}) with header: {}".format(request.url, dynatrace_header)) 29 | tracer = sdk.trace_incoming_web_request(web_app_info, request.url, request.method, headers=request.headers, str_tag=dynatrace_header) 30 | tracer.start() 31 | setattr(request, "__dynatrace_tracer", tracer) 32 | except Exception as e: 33 | logger.warning("Bottle - failed to instrument request: {}".format(e)) 34 | 35 | @hook("after_request") 36 | def instrument_after_request(): 37 | try: 38 | # check if the request was instrumented 39 | tracer = getattr(request, "__dynatrace_tracer", None) 40 | if tracer: 41 | tracer.set_status_code(response.status_code) 42 | tracer.add_response_headers(response.headers) 43 | logger.debug("Bottle - ending incoming request ({}) with status: {}".format(request.url, response.status_code)) 44 | tracer.end() 45 | except Exception as e: 46 | logger.warning("Bottle - failed to instrument response: {}".format(e)) 47 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/celery/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/celery/wrapper.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from celery import signals 4 | from celery import registry 5 | 6 | from ...log import logger 7 | from ...sdk import sdk, shutdown, init 8 | import oneagent 9 | 10 | 11 | DT_KEY = "__dynatrace_tag" 12 | 13 | CELERY_WORKER_PROCESS = sys.argv and sys.argv[0].endswith("celery") and "worker" in sys.argv 14 | 15 | if CELERY_WORKER_PROCESS: 16 | logger.debug("It looks like we are initializing Celery workers, reinitializing with forkable=True") 17 | shutdown() 18 | init(forkable=True) 19 | 20 | 21 | def instrument(): 22 | def get_task_id(context): 23 | headers = context.get("headers") 24 | body = context.get("body") 25 | if headers: 26 | return headers.get("id") 27 | else: 28 | return body.get("id") 29 | 30 | def add_tracer(task, task_id, tracer): 31 | tracer_dict = getattr(task, DT_KEY, None) 32 | if tracer_dict is None: 33 | tracer_dict = dict() 34 | setattr(task, DT_KEY, tracer_dict) 35 | 36 | tracer_dict[task_id] = tracer 37 | 38 | def get_tracer(task, task_id): 39 | tracer_dict = getattr(task, DT_KEY, None) 40 | if tracer_dict is not None: 41 | return tracer_dict.get(task_id, None) 42 | 43 | def remove_tracer(task, task_id): 44 | tracer_dict = getattr(task, DT_KEY, None) 45 | if tracer_dict is not None: 46 | tracer_dict.pop(task_id, None) 47 | 48 | def dt_after_task_publish(**kwargs): 49 | # This is executed on the app side, after a task has been sent to Celery 50 | task_name = kwargs.get("sender") 51 | task = registry.tasks.get(task_name) 52 | task_id = get_task_id(kwargs) 53 | 54 | if task is None or task_id is None: 55 | logger.debug("Could not obtain task or task_id") 56 | return 57 | 58 | tracer = get_tracer(task, task_id) 59 | if tracer is not None: 60 | tracer.end() 61 | remove_tracer(task, task_id) 62 | 63 | def dt_before_task_publish(**kwargs): 64 | # This is executed on the app side, before a task has been sent to Celery 65 | task_name = kwargs.get("sender") 66 | task = registry.tasks.get(task_name) 67 | task_id = get_task_id(kwargs) 68 | 69 | if task is None or task_id is None: 70 | logger.debug("Could not obtain task or task_id") 71 | return 72 | 73 | msi_handle = sdk.create_messaging_system_info( 74 | "Celery", 75 | kwargs.get("routing_key", "celery"), 76 | oneagent.common.MessagingDestinationType.QUEUE, 77 | oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER, "celery"), 78 | ) 79 | 80 | with msi_handle: 81 | tracer = sdk.trace_outgoing_message(msi_handle) 82 | tracer.start() 83 | tag: str = tracer.outgoing_dynatrace_string_tag.decode("utf-8") 84 | logger.debug("Celery - inserting tag {}".format(tag)) 85 | kwargs["headers"][DT_KEY] = tag 86 | add_tracer(task, task_id, tracer) 87 | 88 | def dt_task_prerun(*args, **kwargs): 89 | # This is executed on the worker, before a task is run 90 | task = kwargs.get("sender") 91 | task_id = kwargs.get("task_id") 92 | if task is None or task_id is None: 93 | logger.debug("Could not obtain task or task_id") 94 | return 95 | 96 | msi_handle = sdk.create_messaging_system_info( 97 | "Celery", 98 | task.request.delivery_info.get("routing_key", "celery"), 99 | oneagent.common.MessagingDestinationType.QUEUE, 100 | oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER), 101 | ) 102 | 103 | with msi_handle: 104 | with sdk.trace_incoming_message_receive(msi_handle): 105 | tag = getattr(task.request, DT_KEY, "") 106 | logger.debug("Celery - received tag: {}".format(tag)) 107 | tracer = sdk.trace_incoming_message_process(msi_handle, str_tag=tag) 108 | tracer.start() 109 | add_tracer(task, task_id, tracer) 110 | 111 | def dt_task_postrun(*args, **kwargs): 112 | # This is executed on the worker, after a task is run 113 | task = kwargs.get("sender") 114 | task_id = kwargs.get("task_id") 115 | 116 | if task is None or task_id is None: 117 | logger.debug("Could not obtain task or task_id") 118 | return 119 | 120 | tracer = get_tracer(task, task_id) 121 | if tracer is not None: 122 | tracer.end() 123 | remove_tracer(task, task_id) 124 | 125 | signals.task_prerun.connect(dt_task_prerun, weak=False) 126 | signals.task_postrun.connect(dt_task_postrun, weak=False) 127 | signals.after_task_publish.connect(dt_after_task_publish, weak=False) 128 | signals.before_task_publish.connect(dt_before_task_publish, weak=False) 129 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/concurrent/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/concurrent/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | 7 | def instrument(): 8 | @wrapt.patch_function_wrapper("concurrent.futures.thread", "_WorkItem.__init__") 9 | def dynatrace_submit(wrapped, instance, args, kwargs): 10 | fn = args[1] 11 | module = getattr(fn, "__module__", "thread") 12 | 13 | if hasattr(fn, "__class__") and getattr(fn.__class__, "__name__", "unknown") != "method": 14 | function_name = "__call__" 15 | class_name = getattr(fn.__class__, "__name__", "unknown") 16 | else: 17 | function_name = getattr(fn, "__name__", "unknown") 18 | if hasattr(fn, "__self__") and hasattr(fn.__self__, "__class__"): 19 | class_name = getattr(fn.__self__.__class__, "__name__", "unknown") 20 | else: 21 | class_name = "Unknown" 22 | if class_name == "module": 23 | method = "{}.{}".format(module, function_name) 24 | else: 25 | method = "{}.{}".format(class_name, function_name) 26 | 27 | with sdk.trace_custom_service(method, "Threading"): 28 | logger.debug("Tracing upstream thread execution {}".format(method)) 29 | link = sdk.create_in_process_link() 30 | setattr(instance, "__dynatrace_link", link) 31 | return wrapped(*args, **kwargs) 32 | 33 | @wrapt.patch_function_wrapper("concurrent.futures.thread", "_WorkItem.run") 34 | def dynatrace_run(wrapped, instance, args, kwargs): 35 | link = getattr(instance, "__dynatrace_link", None) 36 | if link is not None: 37 | logger.debug("Tracing downstream thread execution") 38 | with sdk.trace_in_process_link(link): 39 | return wrapped(*args, **kwargs) 40 | return wrapped(*args, **kwargs) 41 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/confluent_kafka/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/confluent_kafka/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | from oneagent.common import MessagingDestinationType 6 | from oneagent.sdk import Channel, ChannelType 7 | from threading import Thread, local 8 | 9 | import confluent_kafka 10 | 11 | threadlocal = local() 12 | threadlocal.tracer = None 13 | 14 | class Producer(confluent_kafka.Producer): 15 | pass 16 | 17 | 18 | class Consumer(confluent_kafka.Consumer): 19 | pass 20 | 21 | 22 | def instrument(): 23 | confluent_kafka.Consumer = Consumer 24 | confluent_kafka.Producer = Producer 25 | 26 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.confluent_kafka.wrapper", "Producer.__init__") 27 | def custom_producer_init(wrapped, instance, args, kwargs): 28 | 29 | servers = args[0].get("bootstrap.servers", "unknown-host") 30 | setattr(instance, "dt_servers", servers) 31 | 32 | return wrapped(*args, **kwargs) 33 | 34 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.confluent_kafka.wrapper", "Consumer.__init__") 35 | def custom_consumer_init(wrapped, instance, args, kwargs): 36 | 37 | servers = args[0].get("bootstrap.servers", "unknown-host") 38 | setattr(instance, "dt_servers", servers) 39 | 40 | return wrapped(*args, **kwargs) 41 | 42 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.confluent_kafka.wrapper", "Producer.produce") 43 | def custom_produce(wrapped, instance, args, kwargs): 44 | 45 | servers = getattr(instance, "dt_servers", "unknown-host") 46 | topic = args[0] if args else kwargs.get("topic", "unknown-topic") 47 | 48 | msi_handle = sdk.create_messaging_system_info( 49 | "Kafka", 50 | topic, 51 | MessagingDestinationType.TOPIC, 52 | Channel(ChannelType.TCP_IP, servers), 53 | ) 54 | 55 | with msi_handle: 56 | with sdk.trace_outgoing_message(msi_handle) as tracer: 57 | tag = tracer.outgoing_dynatrace_string_tag 58 | logger.debug("kafka-producer Injecting message with header '{}'".format(tag)) 59 | headers = kwargs.get("headers", {}) 60 | headers.update({"dtdTraceTagInfo": tag}) 61 | kwargs["headers"] = headers 62 | return wrapped(*args, **kwargs) 63 | 64 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.confluent_kafka.wrapper", "Consumer.poll") 65 | def custom_poll(wrapped, instance, args, kwargs): 66 | if threadlocal.tracer is not None: 67 | threadlocal.tracer.end() 68 | threadlocal.tracer = None 69 | message = wrapped(*args, **kwargs) 70 | if message is not None: 71 | try: 72 | servers = getattr(instance, "dt_servers", "unknown-host") 73 | msi_handle = sdk.create_messaging_system_info( 74 | "Kafka", 75 | message.topic(), 76 | MessagingDestinationType.TOPIC, 77 | Channel(ChannelType.TCP_IP, servers), 78 | ) 79 | with msi_handle: 80 | tag = None 81 | headers = message.headers() 82 | if headers is not None: 83 | for header in headers: 84 | if header[0] == "dtdTraceTagInfo": 85 | tag = header[1] 86 | tracer = sdk.trace_incoming_message_process(msi_handle, str_tag=tag) 87 | tracer.start() 88 | threadlocal.tracer = tracer 89 | logger.debug("kafka-consumer: Received message with tag {}".format(tag)) 90 | return message 91 | except Exception: 92 | logger.debug("Could not trace Consumer.poll", exc_info=True) 93 | return message 94 | return message 95 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/custom/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import dynatrace_custom_tracer 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/custom/wrapper.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | 8 | def use_fully_qualified_name(): 9 | return ( 10 | os.environ.get("AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN") 11 | and os.environ.get("AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN").lower() == "true" 12 | ) 13 | 14 | 15 | def get_custom_defined_service_name(): 16 | return os.environ.get("AUTODYNATRACE_CUSTOM_SERVICE_NAME") 17 | 18 | 19 | def generate_service_name(wrapped, service=None): 20 | if service is not None and isinstance(service, str): 21 | return service 22 | if get_custom_defined_service_name(): 23 | return get_custom_defined_service_name() 24 | else: 25 | return get_module_path(wrapped) 26 | 27 | 28 | def get_module_path(wrapped): 29 | module_path = "unknown" 30 | if hasattr(wrapped, "__module__"): 31 | module_path = wrapped.__module__ 32 | elif hasattr(wrapped, "__func__"): 33 | return get_module_path(wrapped.__func__) 34 | 35 | class_name = None 36 | qual_name = None 37 | result = module_path 38 | 39 | if hasattr(wrapped, "im_class"): 40 | class_name = wrapped.im_class.__name__ 41 | result = class_name 42 | 43 | if hasattr(wrapped, "__qualname__") and "." in wrapped.__qualname__: 44 | qual_name = wrapped.__qualname__.split(".")[0] 45 | result = qual_name 46 | 47 | if use_fully_qualified_name(): 48 | result = ".".join([i for i in [module_path, class_name, qual_name] if i]) 49 | 50 | return result 51 | 52 | 53 | def generate_method_name(wrapped, method=None): 54 | if method is not None: 55 | return method 56 | name = "unknown" 57 | if hasattr(wrapped, "__name__"): 58 | name = wrapped.__name__ 59 | elif hasattr(wrapped, "__func__"): 60 | return generate_method_name(wrapped.__func__) 61 | 62 | if get_custom_defined_service_name() or use_fully_qualified_name(): 63 | path = get_module_path(wrapped) 64 | return "{}.{}".format(path, name) 65 | else: 66 | return name 67 | 68 | 69 | def dynatrace_custom_tracer(service=None, method=None): 70 | def dynatrace_decorator(wrapped): 71 | @functools.wraps(wrapped) 72 | def wrapper(*args, **kwargs): 73 | service_name = generate_service_name(wrapped, service) 74 | method_name = generate_method_name(wrapped, method) 75 | 76 | with sdk.trace_custom_service(method_name, service_name): 77 | logger.debug("Custom tracing - {}: {}".format(service_name, method_name)) 78 | return wrapped(*args, **kwargs) 79 | 80 | return wrapper 81 | 82 | if callable(service): 83 | return dynatrace_decorator(service) 84 | else: 85 | return dynatrace_decorator 86 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/cx_Oracle/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/cx_Oracle/wrapper.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | 4 | import cx_Oracle 5 | import wrapt 6 | 7 | from ...log import logger 8 | from ...sdk import sdk 9 | 10 | 11 | import oneagent 12 | 13 | 14 | class DynatraceConnection(cx_Oracle.Connection): 15 | def __init__(self, *args, **kwargs): 16 | super(DynatraceConnection, self).__init__(*args, **kwargs) 17 | 18 | 19 | class DynatraceCursor(cx_Oracle.Cursor): 20 | def __init__(self, *args, **kwargs): 21 | super(DynatraceCursor, self).__init__(*args, **kwargs) 22 | 23 | 24 | def instrument(): 25 | def trace_query(wrapped, instance, args, kwargs): 26 | if args: 27 | query = args[0] 28 | dsn = instance.connection.dsn or "unknown_oracle" 29 | host = None 30 | port = 1521 31 | 32 | host_match = re.search(r"HOST=(.*?)\)", dsn) 33 | if host_match and host_match.groups(): 34 | host = host_match.group(1) 35 | 36 | port_match = re.search(r"PORT=(.*?)\)", dsn) 37 | if port_match and port_match.groups(): 38 | port = port_match.group(1) 39 | 40 | service_name = dsn 41 | service_match = re.search(r"SERVICE_NAME=(.*?)\)", dsn) 42 | if service_match and service_match.groups(): 43 | service_name = service_match.group(1) 44 | 45 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER, "Oracle") 46 | if host is not None: 47 | try: 48 | socket.inet_pton(socket.AF_INET6, host) 49 | host = "[{}]".format(host) 50 | except Exception: 51 | pass 52 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(host, port)) 53 | 54 | db_info = sdk.create_database_info(service_name, oneagent.sdk.DatabaseVendor.ORACLE, channel) 55 | with sdk.trace_sql_database_request(db_info, "{}".format(query)): 56 | logger.debug( 57 | "Tracing cx_Oracle query: '{}', host: '{}', port: '{}', database: '{}'".format( 58 | query, host, port, service_name 59 | ) 60 | ) 61 | return wrapped(*args, **kwargs) 62 | 63 | return wrapped(*args, **kwargs) 64 | 65 | def fetchone_wrapper(wrapped, instance, args, kwargs): 66 | if not getattr(instance, "_dt_fetchone_reported", False): 67 | with sdk.trace_custom_service("fetchone", "cx_Oracle"): 68 | setattr(instance, "_dt_fetchone_reported", True) 69 | sdk.add_custom_request_attribute("Note", "Only the first fetch (slowest) is recorded") 70 | return wrapped(*args, **kwargs) 71 | return wrapped(*args, **kwargs) 72 | 73 | @wrapt.patch_function_wrapper("cx_Oracle", "connect") 74 | def connect_dynatrace(wrapped, instance, args, kwargs): 75 | return DynatraceConnection(*args, **kwargs) 76 | 77 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceConnection.cursor") 78 | def cursor_dynatrace(wrapped, instance, args, kwargs): 79 | return DynatraceCursor(instance, *args, **kwargs) 80 | 81 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.execute") 82 | def execute_dynatrace(wrapped, instance, args, kwargs): 83 | return trace_query(wrapped, instance, args, kwargs) 84 | 85 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.executemany") 86 | def execute_many_dynatrace(wrapped, instance, args, kwargs): 87 | return trace_query(wrapped, instance, args, kwargs) 88 | 89 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.fetchone") 90 | def fetchone_dynatrace(wrapped, instance, args, kwargs): 91 | return fetchone_wrapper(wrapped, instance, args, kwargs) 92 | 93 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.fetchmany") 94 | def fetchmany_dynatrace(wrapped, instance, args, kwargs): 95 | with sdk.trace_custom_service("fetchmany", "cx_Oracle"): 96 | return wrapped(*args, **kwargs) 97 | 98 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.fetchall") 99 | def fetchall_dynatrace(wrapped, instance, args, kwargs): 100 | with sdk.trace_custom_service("fetchall", "cx_Oracle"): 101 | return wrapped(*args, **kwargs) 102 | 103 | @wrapt.patch_function_wrapper("autodynatrace.wrappers.cx_Oracle.wrapper", "DynatraceCursor.__next__") 104 | def next_dynatrace(wrapped, instance, args, kwargs): 105 | return fetchone_wrapper(wrapped, instance, args, kwargs) 106 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/dbapi/__init__.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | from ..utils import normalize_vendor 6 | 7 | 8 | class TracedCursor(wrapt.ObjectProxy): 9 | def __init__(self, cursor, db_info): 10 | super(TracedCursor, self).__init__(cursor) 11 | self.db_info = db_info 12 | self._self_last_execute_operation = None 13 | self._original_cursor = cursor 14 | 15 | def _trace_method(self, method, query, *args, **kwargs): 16 | 17 | # It could be psycopg2.sql.Composable, but we don't want to import that here 18 | if not isinstance(query, str): 19 | try: 20 | query = query.as_string(self._original_cursor) 21 | except Exception: 22 | pass 23 | logger.debug("Tracing Database Call '{}' to {}".format(str(query), self.db_info)) 24 | 25 | try: 26 | with sdk.trace_sql_database_request(self.db_info, f"{query}"): 27 | return method(*args, **kwargs) 28 | except Exception as e: 29 | logger.warning(f"Error instrumenting database: {e}") 30 | return method(*args, **kwargs) 31 | 32 | def executemany(self, query, *args, **kwargs): 33 | self._self_last_execute_operation = query 34 | return self._trace_method(self.__wrapped__.executemany, query, query, *args, **kwargs) 35 | 36 | def execute(self, query, *args, **kwargs): 37 | self._self_last_execute_operation = query 38 | return self._trace_method(self.__wrapped__.execute, query, query, *args, **kwargs) 39 | 40 | def callproc(self, proc, args): 41 | self._self_last_execute_operation = proc 42 | return self._trace_method(self.__wrapped__.callproc, proc, proc, args) 43 | 44 | def __enter__(self): 45 | self.__wrapped__.__enter__() 46 | return self 47 | 48 | 49 | class TracedConnection(wrapt.ObjectProxy): 50 | def __init__(self, conn, cursor_cls=None): 51 | 52 | if not cursor_cls: 53 | cursor_cls = TracedCursor 54 | super(TracedConnection, self).__init__(conn) 55 | self._self_cursor_cls = cursor_cls 56 | 57 | def _trace_method(self, method, *args, **kwargs): 58 | logger.info("Tracing Connection {}".format(args)) 59 | return method(*args, **kwargs) 60 | 61 | def cursor(self, *args, **kwargs): 62 | return self.__wrapped__.cursor(*args, **kwargs) 63 | 64 | def commit(self, *args, **kwargs): 65 | span_name = "commit" 66 | return self._trace_method(self.__wrapped__.commit, span_name, {}, *args, **kwargs) 67 | 68 | def rollback(self, *args, **kwargs): 69 | span_name = "rollback" 70 | return self._trace_method(self.__wrapped__.rollback, span_name, {}, *args, **kwargs) 71 | 72 | 73 | def _get_vendor(conn): 74 | try: 75 | name = _get_module_name(conn) 76 | except Exception: 77 | logger.debug("couldnt parse module name", exc_info=True) 78 | name = "sql" 79 | return normalize_vendor(name) 80 | 81 | 82 | def _get_module_name(conn): 83 | return conn.__class__.__module__.split(".")[0] 84 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | from.middlewares import DynatraceMiddleware 3 | 4 | 5 | default_app_config = "autodynatrace.wrappers.django.apps.DynatraceConfig" 6 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig, apps 2 | from ...log import logger 3 | from .wrapper import instrument_django 4 | 5 | 6 | class DynatraceConfig(AppConfig): 7 | name = "autodynatrace.wrappers.django" 8 | label = "dynatrace_django" 9 | 10 | def ready(self): 11 | instrument_django() 12 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/db.py: -------------------------------------------------------------------------------- 1 | from django.db import connections 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | from ..utils import normalize_vendor 6 | from ..dbapi import TracedCursor 7 | 8 | import oneagent 9 | 10 | import socket 11 | 12 | CURSOR_ATTR = "_dynatrace_cursor" 13 | ALL_CONNS_ATTR = "_dynatrace_connections_all" 14 | 15 | 16 | def instrument_db(): 17 | if hasattr(connections, ALL_CONNS_ATTR): 18 | return 19 | 20 | setattr(connections, ALL_CONNS_ATTR, connections.all) 21 | 22 | def all_connections(self, *args, **kwargs): 23 | conns = getattr(self, ALL_CONNS_ATTR)() 24 | for conn in conns: 25 | instrument_conn(conn) 26 | return conns 27 | 28 | connections.all = all_connections.__get__(connections, type(connections)) 29 | 30 | 31 | def instrument_conn(conn): 32 | if hasattr(conn, CURSOR_ATTR): 33 | return 34 | 35 | setattr(conn, CURSOR_ATTR, conn.cursor) 36 | 37 | def cursor(): 38 | alias = getattr(conn, "alias", "default") 39 | vendor = normalize_vendor(getattr(conn, "vendor", "db")) 40 | logger.debug("Instrumenting the DB {} {}".format(alias, vendor)) 41 | 42 | db_technology = conn.settings_dict.get("ENGINE", "SQL").split(".")[-1] 43 | db_name = conn.settings_dict.get("NAME", "Unknow") 44 | db_host = conn.settings_dict.get("HOST", None) 45 | db_port = conn.settings_dict.get("PORT", None) 46 | 47 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER, None) 48 | if db_host and db_port: 49 | try: 50 | socket.inet_pton(socket.AF_INET6, db_host) 51 | db_host = "[{}]".format(db_host) 52 | except Exception: 53 | pass 54 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(db_host, db_port)) 55 | 56 | db_info = sdk.create_database_info(str(db_name), str(db_technology), channel) 57 | return TracedCursor(conn._dynatrace_cursor(), db_info) 58 | 59 | conn.cursor = cursor 60 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/middlewares.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | import os 4 | 5 | 6 | from ...log import logger 7 | from ...sdk import sdk 8 | from ..utils import func_name 9 | 10 | from .utils import get_request_uri, get_host, get_app_name 11 | 12 | try: 13 | from django.utils.deprecation import MiddlewareMixin 14 | 15 | MiddlewareClass = MiddlewareMixin 16 | except ImportError: 17 | MiddlewareClass = object 18 | 19 | 20 | class DynatraceMiddleware(MiddlewareClass): 21 | def process_request(self, request): 22 | try: 23 | url = get_request_uri(request) 24 | logger.debug("Tracing request {}".format(url)) 25 | host = get_host(request) 26 | method = request.method 27 | app_name = get_app_name(request) 28 | 29 | headers = {} 30 | dt_header = request.META.get("HTTP_X_DYNATRACE", None) 31 | if dt_header is not None: 32 | headers = {"x-dynatrace": dt_header} 33 | 34 | if os.environ.get("AUTODYNATRACE_CAPTURE_HEADERS", False): 35 | headers.update(getattr(request, "headers", {})) 36 | 37 | virtual_host = os.environ.get("AUTODYNATRACE_VIRTUAL_HOST", host) 38 | context_root = os.environ.get("AUTODYNATRACE_CONTEXT_ROOT", "/") 39 | 40 | with sdk.create_web_application_info(virtual_host, app_name, context_root) as web_app_info: 41 | tracer = sdk.trace_incoming_web_request(web_app_info, url, method, headers=headers) 42 | _set_req_tracer(request, tracer) 43 | tracer.start() 44 | 45 | except Exception: 46 | logger.error("Error tracing request", exc_info=True) 47 | 48 | def process_view(self, request, view_func, *args, **kwargs): 49 | name = func_name(view_func) 50 | logger.debug("Starting view tracer {}".format(name)) 51 | tracer = sdk.trace_custom_service(name, "Django Views") 52 | _add_child_tracer(request, tracer) 53 | tracer.start() 54 | 55 | def process_response(self, request, response): 56 | try: 57 | 58 | # First, end all children 59 | for child in _get_child_tracers(request): 60 | if child: 61 | child.end() 62 | 63 | tracer = _get_req_tracer(request) 64 | if tracer: 65 | tracer.set_status_code(response.status_code) 66 | tracer.end() 67 | 68 | except Exception: 69 | logger.debug("Error processing response", exc_info=True) 70 | finally: 71 | return response 72 | 73 | 74 | def _get_req_tracer(request): 75 | return getattr(request, "_dynatrace_tracer", None) 76 | 77 | 78 | def _set_req_tracer(request, tracer): 79 | return setattr(request, "_dynatrace_tracer", tracer) 80 | 81 | 82 | def _add_child_tracer(request, tracer): 83 | tracers = _get_child_tracers(request) 84 | tracers.append(tracer) 85 | setattr(request, "_dynatrace_child_tracers", tracers) 86 | 87 | 88 | def _get_child_tracers(request): 89 | return getattr(request, "_dynatrace_child_tracers", []) 90 | 91 | 92 | def get_middleware_insertion_point(): 93 | middleware = getattr(settings, "MIDDLEWARE", None) 94 | if middleware is not None and django.VERSION >= (1, 10): 95 | return "MIDDLEWARE", middleware 96 | return "MIDDLEWARE_CLASSES", getattr(settings, "MIDDLEWARE_CLASSES", None) 97 | 98 | 99 | def insert_dynatrace_middleware(): 100 | middleware_attribute, middleware = get_middleware_insertion_point() 101 | if middleware is not None: 102 | setattr( 103 | settings, 104 | middleware_attribute, 105 | type(middleware)(("autodynatrace.wrappers.django.middlewares.DynatraceMiddleware",)) + middleware, 106 | ) 107 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from six.moves.urllib import parse 4 | 5 | from ...log import logger 6 | 7 | try: 8 | from django.core.urlresolvers import resolve 9 | except ImportError: 10 | from django.urls import resolve 11 | 12 | 13 | def get_host(request): 14 | host = None 15 | try: 16 | host = request.get_host() 17 | except Exception: 18 | logger.debug("Failed to get Django request host", exc_info=True) 19 | 20 | if not host: 21 | try: 22 | if "HTTP_HOST" in request.META: 23 | host = request.META["HTTP_HOST"] 24 | else: 25 | host = request.META["SERVER_NAME"] 26 | port = str(request.META["SERVER_PORT"]) 27 | if port != ("443" if request.is_secure() else "80"): 28 | host = "{0}:{1}".format(host, port) 29 | except Exception: 30 | logger.debug("Failed to build Django request host", exc_info=True) 31 | host = "unknown" 32 | 33 | return str(host) 34 | 35 | 36 | def get_request_uri(request): 37 | 38 | host = get_host(request) 39 | scheme = request.scheme or "" 40 | return parse.urlunparse(parse.ParseResult(scheme=scheme, netloc=host, path=request.path, params="", query="", fragment="")) 41 | 42 | 43 | def get_app_name(request): 44 | try: 45 | app_name = resolve(request.path).kwargs.get("name") 46 | logger.debug("Resolved app_name as '{}' from resolving the path".format(app_name)) 47 | if app_name is None: 48 | app_name = "{}:{}".format(request.META.get("SERVER_NAME"), request.META.get("SERVER_PORT")) 49 | except Exception: 50 | app_name = "{}:{}".format(request.META.get("SERVER_NAME"), request.META.get("SERVER_PORT")) 51 | logger.debug("Could not get app name, using default: {}".format(app_name)) 52 | 53 | return os.environ.get("AUTODYNATRACE_APPLICATION_ID", "Django ({})".format(app_name)) 54 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/django/wrapper.py: -------------------------------------------------------------------------------- 1 | import django 2 | import wrapt 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | from .middlewares import insert_dynatrace_middleware 8 | from .db import instrument_db 9 | 10 | 11 | def instrument(): 12 | @wrapt.patch_function_wrapper("django", "setup") 13 | def dynatrace_setup(wrapped, instance, args, kwargs): 14 | from django.conf import settings 15 | 16 | if "autodynatrace.wrappers.django" not in settings.INSTALLED_APPS: 17 | if isinstance(settings.INSTALLED_APPS, tuple): 18 | 19 | settings.INSTALLED_APPS = settings.INSTALLED_APPS + ("autodynatrace.wrappers.django",) 20 | else: 21 | settings.INSTALLED_APPS.append("autodynatrace.wrappers.django") 22 | 23 | logger.debug("Added autodynatrace to settings.INSTALLED_APPS") 24 | wrapped(*args, **kwargs) 25 | 26 | 27 | def instrument_django(): 28 | insert_dynatrace_middleware() 29 | instrument_db() 30 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/fastapi/middleware.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | 7 | class DynatraceASGIMiddleware: 8 | def __init__(self, app): 9 | self.app = app 10 | 11 | async def __call__(self, scope, receive, send): 12 | # If this is not an http request, do nothing and return 13 | if scope["type"] != "http": 14 | return await self.app(scope, receive, send) 15 | 16 | request_headers = scope.get("headers", []) 17 | headers = {} 18 | for key, value in request_headers: 19 | key = key.decode() if isinstance(key, bytes) else key 20 | value = value.decode() if isinstance(value, bytes) else value 21 | headers[key.lower()] = value 22 | 23 | dt_tag = headers.get("x-dynatrace") 24 | 25 | host = "" 26 | # scope["server"] is optional, if missing, it defaults to None. 27 | # https://asgi.readthedocs.io/en/latest/specs/www.html 28 | if scope.get("server") and scope["server"][1]: 29 | # [host, port] 30 | host = f"{scope['server'][0]}:{scope['server'][1]}" 31 | elif scope.get("server"): 32 | # [path, None] 33 | host = f"{scope['server'][0]}" 34 | 35 | url = f"{scope['scheme']}://{host}{scope['path']}?{scope['query_string'].decode()}" 36 | method = scope.get("method", "GET") 37 | app = scope.get("app") 38 | app_name = app.title if app is not None else "FastAPI" 39 | root = scope.get("root_path", "/") 40 | 41 | virtual_host = os.environ.get("AUTODYNATRACE_VIRTUAL_HOST", f"{host}") 42 | app_name = os.environ.get("AUTODYNATRACE_APPLICATION_ID", f"{app_name}") 43 | context_root = os.environ.get("AUTODYNATRACE_CONTEXT_ROOT", root or "/") 44 | 45 | with sdk.create_web_application_info(virtual_host, app_name, context_root) as web_app_info: 46 | with sdk.trace_incoming_web_request( 47 | web_app_info, url, method, headers=headers, str_tag=dt_tag, 48 | ) as tracer: 49 | logger.debug(f"dynatrace - asgi - Tracing url: '{url}' with tag {dt_tag}") 50 | 51 | async def wrapped_send(message): 52 | if message.get("type") == "http.response.start" and "status" in message: 53 | tracer.set_status_code(message["status"]) 54 | 55 | if "headers" in message: 56 | response_headers = {} 57 | for k, v in message["headers"]: 58 | k = k.decode() if isinstance(k, bytes) else k 59 | v = v.decode() if isinstance(v, bytes) else v 60 | response_headers[k.lower()] = v 61 | 62 | tracer.add_response_headers(response_headers) 63 | 64 | return await send(message) 65 | 66 | return await self.app(scope, receive, wrapped_send) 67 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/fastapi/wrapper.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | from fastapi.middleware import Middleware 3 | from .middleware import DynatraceASGIMiddleware 4 | import wrapt 5 | 6 | from ...log import logger 7 | from ...sdk import sdk 8 | 9 | 10 | def instrument(): 11 | @wrapt.patch_function_wrapper("fastapi.applications", "FastAPI.__init__") 12 | def init_dynatrace(wrapped, instance, args, kwargs): 13 | middlewares = kwargs.pop("middleware", []) 14 | middlewares.insert(0, Middleware(DynatraceASGIMiddleware)) 15 | kwargs.update({"middleware": middlewares}) 16 | return wrapped(*args, **kwargs) 17 | 18 | @wrapt.patch_function_wrapper("fastapi", "routing.run_endpoint_function") 19 | async def session_init_dynatrace(wrapped, instance, args, kwargs): 20 | try: 21 | name = kwargs.get("dependant").call.__name__ 22 | except Exception: 23 | name = "run_endpoint_function" 24 | 25 | logger.debug("Tracing fastapi.{}".format(name)) 26 | with sdk.trace_custom_service(name, "FastAPI"): 27 | return await wrapped(*args, **kwargs) 28 | 29 | @wrapt.patch_function_wrapper("fastapi", "routing.serialize_response") 30 | async def serialize_response_dynatrace(wrapped, instance, args, kwargs): 31 | logger.debug("Tracing fastapi.routing.serialize_response") 32 | with sdk.trace_custom_service("serialize_response", "FastAPI"): 33 | return await wrapped(*args, **kwargs) 34 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/flask/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/flask/wrapper.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from wsgiref.util import request_uri 3 | import wrapt 4 | 5 | from ...log import logger 6 | from ...sdk import sdk 7 | import socket 8 | import os 9 | 10 | 11 | def instrument(): 12 | @wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") 13 | def full_dispatch_request_dynatrace(wrapped, instance, args, kwargs): 14 | try: 15 | env = flask.request.environ 16 | method = env.get("REQUEST_METHOD", "GET") 17 | url = env.get("REQUEST_URI") or env.get("RAW_URI") or env.get("werkzeug.request").url or request_uri(env) 18 | host = env.get("SERVER_NAME") or socket.gethostname() or "localhost" 19 | app_name = flask.current_app.name 20 | 21 | dt_headers = None 22 | dt_header = flask.request.headers.get("X-Dynatrace") 23 | if os.environ.get("AUTODYNATRACE_CAPTURE_HEADERS", False): 24 | dt_headers = dict(flask.request.headers) 25 | 26 | virtual_host = os.environ.get("AUTODYNATRACE_VIRTUAL_HOST", "{}".format(host)) 27 | app_name = os.environ.get("AUTODYNATRACE_APPLICATION_ID", "Flask ({})".format(app_name)) 28 | context_root = os.environ.get("AUTODYNATRACE_CONTEXT_ROOT", "/") 29 | 30 | wappinfo = sdk.create_web_application_info(virtual_host, app_name, context_root) 31 | 32 | except Exception as e: 33 | logger.debug("dynatrace - could not instrument: {}".format(e)) 34 | return wrapped(*args, **kwargs) 35 | 36 | with wappinfo: 37 | logger.debug("Tracing with header: {}".format(dt_header)) 38 | tracer = sdk.trace_incoming_web_request(wappinfo, url, method, headers=dt_headers, str_tag=dt_header) 39 | tracer.start() 40 | logger.debug("dynatrace - full_dispatch_request_dynatrace: {}".format(url)) 41 | setattr(flask.request, "__dynatrace_tracer", tracer) 42 | response = wrapped(*args, **kwargs) 43 | tracer.set_status_code(response.status_code) 44 | tracer.end() 45 | return response 46 | 47 | @wrapt.patch_function_wrapper("flask", "Flask.handle_exception") 48 | def handle_exception_dynatrace(wrapped, instance, args, kwargs): 49 | tracer = getattr(flask.request, "__dynatrace_tracer", None) 50 | if tracer is not None: 51 | exception_type = type(args[0]).__name__ 52 | exception_message = str(args[0]) 53 | 54 | logger.debug("Reporting flask exception: {} - {}".format(exception_type, exception_message)) 55 | tracer.set_status_code(500) 56 | tracer.mark_failed(exception_type, exception_message) 57 | tracer.end() 58 | return wrapped(*args, **kwargs) 59 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/grpc/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/grpc/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | import grpc 7 | 8 | 9 | def instrument(): 10 | def instrument_grpc_callable(wrapped, instance, args, kwargs): 11 | try: 12 | target = instance._channel.target() 13 | method = instance._method 14 | 15 | if isinstance(method, bytes): 16 | method = method.decode("utf-8") 17 | 18 | if isinstance(target, bytes): 19 | target = target.decode("utf-8") 20 | 21 | url = f"grpc://{target}{method}" 22 | 23 | except Exception as e: 24 | logger.debug("Could not instrument grpc call, error: '{}'", e) 25 | return wrapped(*args, **kwargs) 26 | 27 | with sdk.trace_outgoing_web_request(url, "POST") as tracer: 28 | logger.debug("Tracing GRPC, url: '{}'".format(url)) 29 | ret = wrapped(*args, **kwargs) 30 | try: 31 | rpc_state = ret[0] 32 | sdk.add_custom_request_attribute("Status code", "{}".format(rpc_state.code)) 33 | if rpc_state.code == grpc.StatusCode.OK: 34 | tracer.set_status_code(200) 35 | else: 36 | tracer.set_status_code(500) 37 | finally: 38 | return ret 39 | 40 | @wrapt.patch_function_wrapper("grpc._channel", "_UnaryUnaryMultiCallable._blocking") 41 | def unary_call_dynatrace(wrapped, instance, args, kwargs): 42 | return instrument_grpc_callable(wrapped, instance, args, kwargs) 43 | 44 | @wrapt.patch_function_wrapper("grpc._channel", "_UnaryStreamMultiCallable.__call__") 45 | def unary_stream_call_dynatrace(wrapped, instance, args, kwargs): 46 | return instrument_grpc_callable(wrapped, instance, args, kwargs) 47 | 48 | @wrapt.patch_function_wrapper("grpc._channel", "_SingleThreadedUnaryStreamMultiCallable.__call__") 49 | def single_unary_call_dynatrace(wrapped, instance, args, kwargs): 50 | return instrument_grpc_callable(wrapped, instance, args, kwargs) 51 | 52 | @wrapt.patch_function_wrapper("grpc._channel", "_StreamUnaryMultiCallable._blocking") 53 | def stream_unary_blocking_dynatrace(wrapped, instance, args, kwargs): 54 | return instrument_grpc_callable(wrapped, instance, args, kwargs) 55 | 56 | @wrapt.patch_function_wrapper("grpc._channel", "_StreamStreamMultiCallable.__call__") 57 | def stream_call_dynatrace(wrapped, instance, args, kwargs): 58 | return instrument_grpc_callable(wrapped, instance, args, kwargs) 59 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/paramiko/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/paramiko/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | 7 | def instrument(): 8 | @wrapt.patch_function_wrapper("paramiko.client", "SSHClient.connect") 9 | def paramiko_connect(wrapped, instance, args, kwargs): 10 | try: 11 | host = args[0] 12 | except IndexError: 13 | host = kwargs.get("hostname","") 14 | port = kwargs.get("port", 22) 15 | url = "ssh://{}:{}".format(host, port) 16 | 17 | with sdk.trace_outgoing_web_request(url, "POST") as tracer: 18 | logger.debug("Tracing paramiko SSHClient.connect, url: '{}'".format(url)) 19 | return wrapped(*args, **kwargs) 20 | 21 | @wrapt.patch_function_wrapper("paramiko.client", "SSHClient.exec_command") 22 | def paramiko_exec_command(wrapped, instance, args, kwargs): 23 | cmd = args[0] 24 | with sdk.trace_custom_service("exec_command", "Paramiko"): 25 | logger.debug("Tracing paramiko SSHClient.exec_command, cmd: '{}'".format(cmd)) 26 | sdk.add_custom_request_attribute("Command", cmd) 27 | return wrapped(*args, **kwargs) 28 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pika/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pika/wrapper.py: -------------------------------------------------------------------------------- 1 | import pika 2 | import wrapt 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | import oneagent 8 | 9 | 10 | def instrument(): 11 | @wrapt.patch_function_wrapper("pika.channel", "Channel.basic_publish") 12 | def basic_publish_dynatrace(wrapped, instance, args, kwargs): 13 | 14 | try: 15 | host = instance.connection.params.host 16 | port = instance.connection.params.port 17 | exchange = kwargs.get("exchange") 18 | routing_key = kwargs.get("routing_key") 19 | 20 | except Exception as e: 21 | logger.warn("autodynatrace - could not instrument Channel.basic_publish: {}".format(e)) 22 | return wrapped(*args, **kwargs) 23 | 24 | properties = kwargs.get("properties") 25 | if properties is not None: 26 | if properties.headers is None: 27 | properties.headers = {} 28 | else: 29 | props = pika.BasicProperties(headers={}) 30 | kwargs["properties"] = props 31 | 32 | messaging_system = sdk.create_messaging_system_info( 33 | "RabbitMQ", 34 | routing_key, 35 | oneagent.common.MessagingDestinationType.QUEUE, 36 | oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(host, port)), 37 | ) 38 | with messaging_system: 39 | with sdk.trace_outgoing_message(messaging_system) as tracer: 40 | tag = tracer.outgoing_dynatrace_string_tag.decode("utf-8") 41 | kwargs["properties"].headers[oneagent.common.DYNATRACE_MESSAGE_PROPERTY_NAME] = tag 42 | logger.debug( 43 | "autodynatrace - Tracing RabbitMQ host={}, port={}, routing_key={}, tag={}".format( 44 | host, port, routing_key, tag 45 | ) 46 | ) 47 | return wrapped(*args, **kwargs) 48 | 49 | @wrapt.patch_function_wrapper("pika.adapters.blocking_connection", "BlockingChannel._on_consumer_message_delivery") 50 | def on_message_callback_dynatrace(wrapped, instance, args, kwargs): 51 | 52 | try: 53 | channel, method_frame, header_frame, body = args 54 | host = channel.connection.params.host 55 | port = channel.connection.params.port 56 | routing_key = method_frame.routing_key 57 | exchange = method_frame.exchange 58 | tag = None 59 | if header_frame is not None and header_frame.headers is not None: 60 | tag = header_frame.headers.get(oneagent.common.DYNATRACE_MESSAGE_PROPERTY_NAME, None) 61 | 62 | except Exception as e: 63 | logger.warn("autodynatrace - Could not trace BlockingChannel._on_consumer_message_delivery: {}".format(e)) 64 | return wrapped(*args, **kwargs) 65 | 66 | messaging_system = sdk.create_messaging_system_info( 67 | "RabbitMQ", 68 | routing_key, 69 | oneagent.common.MessagingDestinationType.QUEUE, 70 | oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(host, port)), 71 | ) 72 | with messaging_system: 73 | with sdk.trace_incoming_message_receive(messaging_system): 74 | logger.debug( 75 | "autodynatrace - Tracing RabbitMQ host={}, port={}, routing_key={}, tag={}".format( 76 | host, port, routing_key, tag 77 | ) 78 | ) 79 | with sdk.trace_incoming_message_process(messaging_system, str_tag=tag): 80 | return wrapped(*args, **kwargs) 81 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/psycopg2/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/psycopg2/wrapper.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | import wrapt 3 | import oneagent 4 | import functools 5 | 6 | from ...log import logger 7 | from ...sdk import sdk 8 | 9 | 10 | def instrument(): 11 | def parse_dsn(dsn): 12 | return {c.split("=")[0]: c.split("=")[1] for c in dsn.split() if "=" in c} 13 | 14 | class DynatraceCursor(psycopg2.extensions.cursor): 15 | def __init__(self, *args, **kwargs): 16 | self._dynatrace_db_info = kwargs.pop("dynatrace_db_info", None) 17 | super(DynatraceCursor, self).__init__(*args, **kwargs) 18 | 19 | def execute(self, query, vars=None): 20 | if hasattr(self, "_dynatrace_db_info") and self._dynatrace_db_info is not None: 21 | with sdk.trace_sql_database_request(self._dynatrace_db_info, "{}".format(query)) as tracer: 22 | try: 23 | logger.debug("Tracing psycopg2 query: '{}'".format(query)) 24 | return super(DynatraceCursor, self).execute(query, vars) 25 | finally: 26 | tracer.set_rows_returned(self.rowcount) 27 | 28 | return super(DynatraceCursor, self).execute(query, vars) 29 | 30 | class DynatraceConnection(psycopg2.extensions.connection): 31 | def __init__(self, *args, **kwargs): 32 | super(DynatraceConnection, self).__init__(*args, **kwargs) 33 | 34 | dsn = parse_dsn(self.dsn) 35 | self._dynatrace_db_info = sdk.create_database_info( 36 | dsn.get("dbname", "unknown"), 37 | oneagent.sdk.DatabaseVendor.POSTGRESQL, 38 | oneagent.sdk.Channel( 39 | oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(dsn.get("host", "localhost"), dsn.get("port", 5432)) 40 | ), 41 | ) 42 | 43 | self._dynatrace_cursor = functools.partial(DynatraceCursor, dynatrace_db_info=self._dynatrace_db_info) 44 | 45 | def cursor(self, *args, **kwargs): 46 | kwargs.setdefault("cursor_factory", self._dynatrace_cursor) 47 | return super(DynatraceConnection, self).cursor(*args, **kwargs) 48 | 49 | @wrapt.patch_function_wrapper("psycopg2", "connect") 50 | def dynatrace_connect(wrapped, instance, args, kwargs): 51 | kwargs.setdefault("connection_factory", DynatraceConnection) 52 | return wrapped(*args, **kwargs) 53 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pymongo/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pymongo/wrapper.py: -------------------------------------------------------------------------------- 1 | from pymongo import monitoring 2 | import oneagent 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | 8 | def instrument(): 9 | class DynatraceMongoListener(monitoring.CommandListener): 10 | def __init__(self): 11 | self.vendor = "MongoDB" 12 | self._tracer_dict = {} 13 | 14 | def started(self, event): 15 | 16 | try: 17 | db_name = event.database_name 18 | collection_name = event.command.get(event.command_name) 19 | operation = "{} at {}".format(event.command_name, collection_name) 20 | host, port = event.connection_id 21 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(host, port)) 22 | 23 | db_info = sdk.create_database_info(db_name, self.vendor, channel) 24 | tracer = sdk.trace_sql_database_request(db_info, "{}".format(operation)) 25 | self._tracer_dict[_get_tracer_dict_key(event)] = tracer 26 | tracer.start() 27 | logger.debug("Tracing Mongo call: {}({})@{}:{}".format(operation, db_name, host, port)) 28 | 29 | except Exception as e: 30 | logger.debug("Error instrumenting MongoDB: {}".format(e)) 31 | 32 | def succeeded(self, event): 33 | self.end(False, event) 34 | 35 | def failed(self, event): 36 | self.end(True, event, message="{}".format(event.failure)) 37 | 38 | def end(self, failed, event, message=""): 39 | tracer = self._tracer_dict.get(_get_tracer_dict_key(event)) 40 | if tracer is not None: 41 | if event and not isinstance(event, monitoring.CommandSucceededEvent): 42 | logger.debug("Got bad pymongo event: {}".format(event)) 43 | tracer.mark_failed("MongoDB Command", message) 44 | 45 | logger.debug("Ending Mongo call: {}({})@{}:{}".format(event.command_name, event.database_name, event.connection_id[0], event.connection_id[1])) 46 | tracer.end() 47 | self._tracer_dict.pop(_get_tracer_dict_key(event)) 48 | 49 | monitoring.register(DynatraceMongoListener()) 50 | 51 | def _get_tracer_dict_key(event): 52 | if event.connection_id is not None: 53 | return event.request_id, event.connection_id 54 | return event.request_id 55 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pysnmp/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/pysnmp/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | from pysnmp.proto.rfc1905 import GetRequestPDU, GetBulkRequestPDU 7 | 8 | 9 | def instrument(): 10 | @wrapt.patch_function_wrapper("pysnmp.proto.rfc3412", "MsgAndPduDispatcher.sendPdu") 11 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 12 | try: 13 | host, port = args[2] 14 | request = args[10] 15 | 16 | oids = [] 17 | if isinstance(request, (GetBulkRequestPDU, GetRequestPDU)): 18 | oids = [str(vb["name"]) for vb in request["variable-bindings"]] 19 | 20 | url = "snmp://{}:{}?query={}".format(host, port, ",".join(oids)) 21 | 22 | except Exception as e: 23 | logger.debug("Could not instrument sendPdu: {}".format(e)) 24 | return wrapped(*args, **kwargs) 25 | 26 | with sdk.trace_outgoing_web_request(url, "POST") as tracer: 27 | logger.debug("Tracing pysnmp sendPdu, url: '{}'".format(url)) 28 | return wrapped(*args, **kwargs) 29 | 30 | @wrapt.patch_function_wrapper("pysnmp.proto.rfc3412", "MsgAndPduDispatcher.receiveMessage") 31 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 32 | with sdk.trace_custom_service("receiveMessage", "SNMP"): 33 | logger.debug("Tracing pysnmp receiveMessage") 34 | return wrapped(*args, **kwargs) 35 | 36 | @wrapt.patch_function_wrapper("pysnmp.proto.rfc3412", "MsgAndPduDispatcher.returnResponsePdu") 37 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 38 | with sdk.trace_custom_service("returnResponsePdu", "SNMP"): 39 | logger.debug("Tracing pysnmp returnResponsePdu") 40 | return wrapped(*args, **kwargs) 41 | 42 | @wrapt.patch_function_wrapper("pysnmp.entity.rfc3413.cmdgen", "CommandGenerator.processResponsePdu") 43 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 44 | with sdk.trace_custom_service("processResponsePdu", "SNMP"): 45 | logger.debug("Tracing pysnmp processResponsePdu") 46 | return wrapped(*args, **kwargs) 47 | 48 | @wrapt.patch_function_wrapper("pysnmp.entity.rfc3413.cmdgen", "BulkCommandGeneratorSingleRun.sendVarBinds") 49 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 50 | with sdk.trace_custom_service("sendVarBinds", "SNMP"): 51 | logger.debug("Tracing pysnmp sendVarBinds") 52 | return wrapped(*args, **kwargs) 53 | 54 | @wrapt.patch_function_wrapper("pysnmp.hlapi.asyncore.cmdgen", "bulkCmd") 55 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 56 | with sdk.trace_custom_service("cmdgen.bulkCmd", "SNMP"): 57 | logger.debug("Tracing pysnmp cmdgen.bulkCmd") 58 | return wrapped(*args, **kwargs) 59 | 60 | @wrapt.patch_function_wrapper("pysnmp.hlapi.asyncore", "bulkCmd") 61 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 62 | with sdk.trace_custom_service("asyncore.bulkCmd", "SNMP"): 63 | logger.debug("Tracing pysnmp asyncore.bulkCmd") 64 | return wrapped(*args, **kwargs) 65 | 66 | @wrapt.patch_function_wrapper("pysnmp.hlapi.asyncore", "getCmd") 67 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 68 | with sdk.trace_custom_service("asyncore.getCmd", "SNMP"): 69 | logger.debug("Tracing pysnmp asyncore.getCmd") 70 | return wrapped(*args, **kwargs) 71 | 72 | @wrapt.patch_function_wrapper("pysnmp.hlapi", "bulkCmd") 73 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 74 | with sdk.trace_custom_service("sync.bulkCmd", "SNMP"): 75 | logger.debug("Tracing pysnmp sync.bulkCmd") 76 | return wrapped(*args, **kwargs) 77 | 78 | @wrapt.patch_function_wrapper("pysnmp.entity.engine", "SnmpEngine.__init__") 79 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 80 | with sdk.trace_custom_service("SnmpEngine()", "SNMP"): 81 | logger.debug("Tracing pysnmp SnmpEngine.__init__") 82 | return wrapped(*args, **kwargs) 83 | 84 | @wrapt.patch_function_wrapper("pysnmp.hlapi.lcd", "CommandGeneratorLcdConfigurator.configure") 85 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 86 | with sdk.trace_custom_service("configure", "SNMP"): 87 | logger.debug("Tracing pysnmp CommandGeneratorLcdConfigurator.configure") 88 | return wrapped(*args, **kwargs) 89 | 90 | @wrapt.patch_function_wrapper("pysnmp.hlapi.varbinds", "CommandGeneratorVarBinds.makeVarBinds") 91 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 92 | with sdk.trace_custom_service("makeVarBinds", "SNMP"): 93 | logger.debug("Tracing pysnmp CommandGeneratorVarBinds.makeVarBinds") 94 | return wrapped(*args, **kwargs) 95 | 96 | @wrapt.patch_function_wrapper("pysnmp.hlapi.asyncore.cmdgen", "getCmd") 97 | def send_pdu_dynatrace(wrapped, instance, args, kwargs): 98 | with sdk.trace_custom_service("cmdgen.getCmd", "SNMP"): 99 | logger.debug("Tracing pysnmp cmdgen.getCmd") 100 | return wrapped(*args, **kwargs) 101 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/redis/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/redis/utils.py: -------------------------------------------------------------------------------- 1 | from six import text_type 2 | 3 | VALUE_PLACEHOLDER = "?" 4 | VALUE_MAX_LEN = 100 5 | VALUE_TOO_LONG_MARK = "..." 6 | CMD_MAX_LEN = 1000 7 | 8 | 9 | def format_command_args(args): 10 | 11 | length = 0 12 | out = [] 13 | for arg in args: 14 | try: 15 | cmd = text_type(arg) 16 | 17 | if len(cmd) > VALUE_MAX_LEN: 18 | cmd = cmd[:VALUE_MAX_LEN] + VALUE_TOO_LONG_MARK 19 | 20 | if length + len(cmd) > CMD_MAX_LEN: 21 | prefix = cmd[: CMD_MAX_LEN - length] 22 | out.append("%s%s" % (prefix, VALUE_TOO_LONG_MARK)) 23 | break 24 | 25 | out.append(cmd) 26 | length += len(cmd) 27 | except Exception: 28 | out.append(VALUE_PLACEHOLDER) 29 | break 30 | 31 | return " ".join(out) 32 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/redis/wrapper.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import wrapt 3 | import oneagent 4 | import socket 5 | 6 | from ...log import logger 7 | from ...sdk import sdk 8 | 9 | from .utils import format_command_args 10 | 11 | 12 | def instrument(): 13 | if redis.VERSION < (3, 0, 0): 14 | wrapt.wrap_function_wrapper("redis", "StrictRedis.execute_command", dynatrace_execute_command) 15 | wrapt.wrap_function_wrapper("redis.client", "BasePipeline.execute", dynatrace_execute_pipeline) 16 | wrapt.wrap_function_wrapper("redis.client", "BasePipeline.immediate_execute_command", dynatrace_execute_command) 17 | else: 18 | wrapt.wrap_function_wrapper("redis", "Redis.execute_command", dynatrace_execute_command) 19 | wrapt.wrap_function_wrapper("redis.client", "Pipeline.execute", dynatrace_execute_command) 20 | wrapt.wrap_function_wrapper("redis.client", "Pipeline.immediate_execute_command", dynatrace_execute_command) 21 | 22 | 23 | def dynatrace_execute_command(func, instance, args, kwargs): 24 | host = instance.connection_pool.connection_kwargs.get("host", None) 25 | port = instance.connection_pool.connection_kwargs.get("port", None) 26 | 27 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER, None) 28 | if host and port: 29 | try: 30 | socket.inet_pton(socket.AF_INET6, host) 31 | host = "[{}]".format(host) 32 | except Exception: 33 | pass 34 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(host, port)) 35 | 36 | db_info = sdk.create_database_info("Cache", "Redis", channel) 37 | 38 | query = format_command_args(args) 39 | with sdk.trace_sql_database_request(db_info, "{}".format(query)): 40 | logger.debug("Tracing from Redis: {} {}".format(func.__name__, query)) 41 | return func(*args, **kwargs) 42 | 43 | 44 | def dynatrace_execute_pipeline(func, instance, args, kwargs): 45 | cmds = [format_command_args(c) for c, _ in instance.command_stack] 46 | with sdk.trace_custom_service(func.__name__, "Redis"): 47 | logger.debug("Tracing from Redis: {} {}".format(func.__name__, cmds)) 48 | sdk.add_custom_request_attribute("Queries", cmds) 49 | 50 | return func(*args, **kwargs) 51 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/ruxit/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/ruxit/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | 7 | def instrument(): 8 | @wrapt.patch_function_wrapper("ruxit.api.base_plugin", "BasePlugin._query_internal") 9 | def query_dynatrace(wrapped, instance, args, kwargs): 10 | plugin_name = type(instance).__name__ 11 | with sdk.trace_custom_service("query", plugin_name): 12 | logger.debug("Custom tracing - {}: {}".format(plugin_name, "query")) 13 | return wrapped(*args, **kwargs) 14 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/sqlalchemy/wrapper.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import oneagent 4 | import sqlalchemy 5 | from sqlalchemy import event 6 | from sqlalchemy.engine import Engine 7 | from sqlalchemy.engine.base import Connection 8 | import wrapt 9 | 10 | from ...log import logger 11 | from ...sdk import sdk 12 | 13 | 14 | def instrument(): 15 | @event.listens_for(Engine, "before_cursor_execute", named=True) 16 | def dynatrace_before_cursor_execute(**kw): 17 | 18 | try: 19 | conn = kw["conn"] 20 | context = kw["context"] 21 | 22 | statement = kw.get("statement", "") 23 | db_technology = conn.engine.name 24 | db_name = conn.engine.url.database 25 | 26 | if not db_name: 27 | # The connection string might not have a db name 28 | db_name = "default" 29 | 30 | db_host = conn.engine.url.host 31 | db_port = conn.engine.url.port 32 | 33 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.OTHER, None) 34 | if db_host is not None and db_port is not None: 35 | try: 36 | socket.inet_pton(socket.AF_INET6, db_host) 37 | db_host = "[{}]".format(db_host) 38 | except Exception: 39 | pass 40 | channel = oneagent.sdk.Channel(oneagent.sdk.ChannelType.TCP_IP, "{}:{}".format(db_host, db_port)) 41 | 42 | db_info = sdk.create_database_info(db_name, db_technology, channel) 43 | tracer = sdk.trace_sql_database_request(db_info, "{}".format(statement)) 44 | logger.debug("Tracing SQLAlchemy: '{}, {}@{}:{}".format(statement, db_name, db_host, db_port)) 45 | tracer.start() 46 | context.dynatrace_tracer = tracer 47 | 48 | except Exception as e: 49 | logger.debug("Error instrumenting sqlalchemy: {}".format(e)) 50 | 51 | @event.listens_for(Engine, "after_cursor_execute", named=True) 52 | def dynatrace_after_cursor_execute(**kw): 53 | 54 | try: 55 | context = kw["context"] 56 | 57 | if context is not None and hasattr(context, "dynatrace_tracer"): 58 | tracer = context.dynatrace_tracer 59 | if tracer is not None: 60 | # TODO Check if I get stats about query 61 | tracer.end() 62 | 63 | except Exception as e: 64 | logger.debug("Error instrumenting sqlalchemy: {}".format(e)) 65 | 66 | @event.listens_for(Engine, "handle_error", named=True) 67 | def dynatrace_handle_error(**kw): 68 | 69 | try: 70 | context = kw["exception_context"] 71 | 72 | if context is not None and hasattr(context, "dynatrace_tracer"): 73 | tracer = context.dynatrace_tracer 74 | if tracer is not None: 75 | logger.debug("Marking purepath as failed") 76 | tracer.mark_failed_exc() 77 | tracer.end() 78 | 79 | except Exception as e: 80 | logger.debug("Error instrumenting sqlalchemy: {}".format(e)) 81 | 82 | @wrapt.patch_function_wrapper("sqlalchemy.orm.session", "Session.__init__") 83 | def session_init_dynatrace(wrapped, instance, args, kwargs): 84 | logger.debug("Tracing sqlalchemy.Session.init") 85 | with sdk.trace_custom_service("Session.init", "sqlalchemy"): 86 | return wrapped(*args, **kwargs) 87 | 88 | @wrapt.patch_function_wrapper("sqlalchemy.orm.session", "Session.begin") 89 | def session_init_dynatrace(wrapped, instance, args, kwargs): 90 | logger.debug("Tracing sqlalchemy.Session.begin") 91 | with sdk.trace_custom_service("Session.begin", "sqlalchemy"): 92 | return wrapped(*args, **kwargs) 93 | 94 | @wrapt.patch_function_wrapper("sqlalchemy.orm.session", "Session.connection") 95 | def session_init_dynatrace(wrapped, instance, args, kwargs): 96 | logger.debug("Tracing sqlalchemy.Session.connection") 97 | with sdk.trace_custom_service("Session.connection", "sqlalchemy"): 98 | return wrapped(*args, **kwargs) 99 | 100 | @wrapt.patch_function_wrapper("sqlalchemy.orm.session", "Session.close") 101 | def session_init_dynatrace(wrapped, instance, args, kwargs): 102 | logger.debug("Tracing sqlalchemy.Session.close") 103 | with sdk.trace_custom_service("Session.close", "sqlalchemy"): 104 | return wrapped(*args, **kwargs) 105 | 106 | @wrapt.patch_function_wrapper("sqlalchemy.orm.session", "Session.query") 107 | def session_init_dynatrace(wrapped, instance, args, kwargs): 108 | logger.debug("Tracing sqlalchemy.Session.query") 109 | with sdk.trace_custom_service("Session.query", "sqlalchemy"): 110 | return wrapped(*args, **kwargs) 111 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/starlette/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/starlette/wrapper.py: -------------------------------------------------------------------------------- 1 | import starlette 2 | import asyncio 3 | 4 | import wrapt 5 | from ...log import logger 6 | from ...sdk import sdk 7 | 8 | 9 | def instrument(): 10 | @wrapt.patch_function_wrapper("starlette.responses", "Response.__init__") 11 | def starlette_parsing_dynatrace(wrapped, instance, args, kwargs): 12 | 13 | # FastAPI creates a empty response at the beginning of a request, ignore it 14 | if kwargs.get("status_code") is None: 15 | return wrapped(*args, **kwargs) 16 | 17 | logger.debug("Tracing starlette.Response.__init__") 18 | with sdk.trace_custom_service("Response.render", "starlette"): 19 | return wrapped(*args, **kwargs) 20 | 21 | # TODO - Add asyncio support 22 | # @wrapt.patch_function_wrapper("asyncio", "BaseEventLoop.create_task") 23 | # def starlette_parsing_dynatrace(wrapped, instance, args, kwargs): 24 | # logger.debug("Tracing asyncio.BaseEventLoop.create_task") 25 | # with sdk.trace_custom_service("BaseEventLoop.create_task", "asyncio"): 26 | # return wrapped(*args, **kwargs) 27 | 28 | # loop = asyncio.get_event_loop() 29 | # if not isinstance(loop.create_task, wrapt.ObjectProxy): 30 | # 31 | # @wrapt.patch_function_wrapper(loop, "create_task") 32 | # def starlette_parsing_dynatrace(wrapped, instance, args, kwargs): 33 | # logger.debug("Tracing asyncio.loop.create_task") 34 | # with sdk.trace_custom_service("loop.create_task", "asyncio"): 35 | # return wrapped(*args, **kwargs) 36 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/subprocess/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/subprocess/wrapper.py: -------------------------------------------------------------------------------- 1 | import six 2 | import wrapt 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | 8 | def instrument(): 9 | def dynatrace_trace(wrapped, instance, args, kwargs): 10 | method = wrapped.__name__ 11 | with sdk.trace_custom_service(method, "Subprocess"): 12 | logger.debug("Tracing subprocess.{}".format(method)) 13 | sdk.add_custom_request_attribute("args", str(args)) 14 | return wrapped(*args, **kwargs) 15 | 16 | if six.PY3: 17 | 18 | @wrapt.patch_function_wrapper("subprocess", "run") 19 | def dynatrace_run(wrapped, instance, args, kwargs): 20 | return dynatrace_trace(wrapped, instance, args, kwargs) 21 | 22 | @wrapt.patch_function_wrapper("subprocess", "call") 23 | def dynatrace_call(wrapped, instance, args, kwargs): 24 | return dynatrace_trace(wrapped, instance, args, kwargs) 25 | 26 | @wrapt.patch_function_wrapper("subprocess", "check_call") 27 | def dynatrace_check_call(wrapped, instance, args, kwargs): 28 | return dynatrace_trace(wrapped, instance, args, kwargs) 29 | 30 | @wrapt.patch_function_wrapper("subprocess", "check_output") 31 | def dynatrace_check_output(wrapped, instance, args, kwargs): 32 | return dynatrace_trace(wrapped, instance, args, kwargs) 33 | 34 | @wrapt.patch_function_wrapper("subprocess", "Popen") 35 | def dynatrace_run(wrapped, instance, args, kwargs): 36 | return dynatrace_trace(wrapped, instance, args, kwargs) 37 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/suds/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/suds/wrapper.py: -------------------------------------------------------------------------------- 1 | import wrapt 2 | 3 | from ...log import logger 4 | from ...sdk import sdk 5 | 6 | 7 | def instrument(): 8 | @wrapt.patch_function_wrapper("suds.client", "Client.__init__") 9 | def dynatrace_client(wrapped, instance, args, kwargs): 10 | with sdk.trace_custom_service("CreateClient", "Soap"): 11 | logger.debug("Tracing suds Client.__init__") 12 | return wrapped(*args, **kwargs) 13 | 14 | @wrapt.patch_function_wrapper("suds.client", "SoapClient.invoke") 15 | def dynatrace_client(wrapped, instance, args, kwargs): 16 | with sdk.trace_custom_service(instance.method.name, "Soap"): 17 | logger.debug("Tracing suds Client.{}".format(instance.method.name)) 18 | return wrapped(*args, **kwargs) 19 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/tornado/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/tornado/wrapper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import wrapt 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | 8 | def instrument(): 9 | @wrapt.patch_function_wrapper("tornado.web", "RequestHandler._execute") 10 | def patch_execute(wrapped, instance, args, kwargs): 11 | request = instance.request 12 | 13 | virtual_host = os.environ.get("AUTODYNATRACE_VIRTUAL_HOST", request.host) 14 | app_name = os.environ.get("AUTODYNATRACE_APPLICATION_ID", "Tornado") 15 | context_root = os.environ.get("AUTODYNATRACE_CONTEXT_ROOT", "/") 16 | wappinfo = sdk.create_web_application_info(virtual_host, app_name, context_root) 17 | 18 | dt_headers = None 19 | dt_header = instance.request.headers.get("X-Dynatrace") 20 | if os.environ.get("AUTODYNATRACE_CAPTURE_HEADERS", False): 21 | dt_headers = dict(instance.request.headers) 22 | 23 | with wappinfo: 24 | url = instance.request.full_url().rsplit("?", 1)[0] 25 | logger.debug("tornado - tracing '{}' with header: '{}'".format(url, dt_header)) 26 | method = instance.request.method 27 | tracer = sdk.trace_incoming_web_request(wappinfo, url, method, headers=dt_headers, str_tag=dt_header) 28 | tracer.start() 29 | sdk.add_custom_request_attribute("query", instance.request.query) 30 | 31 | setattr(instance.request, "__dynatrace_tracer", tracer) 32 | return wrapped(*args, **kwargs) 33 | 34 | @wrapt.patch_function_wrapper("tornado.web", "RequestHandler.on_finish") 35 | def patch_on_finish(wrapped, instance, args, kwargs): 36 | tracer = getattr(instance.request, "__dynatrace_tracer", None) 37 | if tracer: 38 | tracer.set_status_code(instance.get_status()) 39 | tracer.end() 40 | 41 | return wrapped(*args, **kwargs) 42 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/urllib/__init__.py: -------------------------------------------------------------------------------- 1 | from .wrapper import instrument 2 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/urllib/wrapper.py: -------------------------------------------------------------------------------- 1 | import six 2 | import wrapt 3 | 4 | from ...log import logger 5 | from ...sdk import sdk 6 | 7 | 8 | def instrument(): 9 | httplib = six.moves.http_client 10 | 11 | def dynatrace_putrequest(wrapped, instance, args, kwargs): 12 | method, path = args[:2] 13 | scheme = "https" if "HTTPS" in instance.__class__.__name__ else "http" 14 | url = "{}://{}{}{}".format( 15 | scheme, instance.host, ":{}".format(instance.port) if str(instance.port) not in ["80", "443"] else "", path 16 | ) 17 | tracer = sdk.trace_outgoing_web_request(url, method) 18 | tracer.start() 19 | setattr(instance, "__dynatrace_tracer", tracer) 20 | ret = wrapped(*args, **kwargs) 21 | tag = tracer.outgoing_dynatrace_string_tag 22 | logger.debug("Tracing urllib, url: '{}', tag: '{}'".format(url, tag)) 23 | instance.putheader("x-dynatrace", tag) 24 | return ret 25 | 26 | def dynatrace_getresponse(wrapped, instance, args, kwargs): 27 | tracer = getattr(instance, "__dynatrace_tracer", None) 28 | response = wrapped(*args, **kwargs) 29 | # print(traceback.print_stack()) 30 | 31 | if tracer is not None: 32 | tracer.set_status_code(response.status) 33 | tracer.end() 34 | delattr(instance, "__dynatrace_tracer") 35 | 36 | return response 37 | 38 | setattr(httplib.HTTPConnection, "putrequest", wrapt.FunctionWrapper(httplib.HTTPConnection.putrequest, dynatrace_putrequest)) 39 | setattr( 40 | httplib.HTTPConnection, "getresponse", wrapt.FunctionWrapper(httplib.HTTPConnection.getresponse, dynatrace_getresponse) 41 | ) 42 | -------------------------------------------------------------------------------- /autodynatrace/wrappers/utils.py: -------------------------------------------------------------------------------- 1 | def func_name(view_func): 2 | if hasattr(view_func, "__name__"): 3 | return view_func.__name__ 4 | return f"{view_func}" 5 | 6 | 7 | def normalize_vendor(name): 8 | return name 9 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "autodynatrace" 7 | dynamic = ["version"] 8 | description = "Auto instrumentation for the OneAgent SDK" 9 | readme = "README.md" 10 | license = "Apache-2.0" 11 | requires-python = ">=3.6" 12 | authors = [ 13 | { name = "David Lopes", email = "david.lopes@dynatrace.com" }, 14 | ] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved", 19 | "License :: OSI Approved :: Apache Software License", 20 | "Operating System :: Microsoft :: Windows", 21 | "Operating System :: POSIX :: Linux", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: Implementation :: CPython", 26 | "Topic :: System :: Monitoring", 27 | ] 28 | dependencies = [ 29 | "autowrapt>=1.0", 30 | "oneagent-sdk>=1.3.0", 31 | "six>=1.10.0", 32 | "wrapt>=1.11.2", 33 | ] 34 | 35 | [project.entry-points.autodynatrace] 36 | string = "autodynatrace:load" 37 | 38 | [project.urls] 39 | Homepage = "https://github.com/dlopes7/autodynatrace" 40 | "Issue Tracker" = "https://github.com/dlopes7/autodynatrace/issues" 41 | 42 | [tool.hatch.version] 43 | path = "autodynatrace/__about__.py" 44 | 45 | [tool.hatch.build.targets.sdist] 46 | include = [ 47 | "/autodynatrace", 48 | ] 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynatrace-oss/OneAgent-SDK-Python-AutoInstrumentation/77d817d8f96cfde7341471f9cc5af81f79de4b04/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_custom.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | import autodynatrace 5 | import autodynatrace.wrappers.custom.wrapper as custom_wrapper 6 | 7 | 8 | @autodynatrace.trace 9 | def module_function(): 10 | return 1 11 | 12 | 13 | def another_function(): 14 | pass 15 | 16 | 17 | class MyClass: 18 | def class_method(self): 19 | pass 20 | 21 | @staticmethod 22 | def static_method(self): 23 | pass 24 | 25 | 26 | @autodynatrace.trace("Service") 27 | def decorated_service_only(): 28 | return 1 29 | 30 | 31 | @autodynatrace.trace(method="method") 32 | def decorated_method_only(): 33 | return 1 34 | 35 | 36 | @autodynatrace.trace("Service", "method") 37 | def decorated_service_and_method(): 38 | return 1 39 | 40 | def test_custom_service_name(): 41 | my_class = MyClass() 42 | 43 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN", None) 44 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_NAME", None) 45 | assert custom_wrapper.generate_service_name(module_function) == "tests.test_custom" 46 | assert custom_wrapper.generate_service_name(my_class.class_method) == "MyClass" 47 | assert custom_wrapper.generate_service_name(classmethod(my_class.class_method)) == "MyClass" 48 | assert custom_wrapper.generate_service_name(module_function) == custom_wrapper.generate_service_name(another_function) 49 | 50 | if sys.version_info[0] == 2: 51 | assert custom_wrapper.generate_service_name(my_class.static_method) == "tests.test_custom" 52 | assert custom_wrapper.generate_service_name(MyClass.static_method) == "tests.test_custom" 53 | else: 54 | assert custom_wrapper.generate_service_name(my_class.static_method) == "MyClass" 55 | assert custom_wrapper.generate_service_name(MyClass.static_method) == "MyClass" 56 | 57 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_NAME"] = "CustomServiceName" 58 | assert custom_wrapper.generate_service_name(module_function) == "CustomServiceName" 59 | 60 | 61 | def test_custom_service_name_fqn_true(): 62 | my_class = MyClass() 63 | 64 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN"] = "TRUE" 65 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_NAME", None) 66 | assert custom_wrapper.generate_service_name(module_function) == "tests.test_custom" 67 | assert custom_wrapper.generate_service_name(my_class.class_method) == "tests.test_custom.MyClass" 68 | assert custom_wrapper.generate_service_name(classmethod(my_class.class_method)) == "tests.test_custom.MyClass" 69 | assert custom_wrapper.generate_service_name(module_function) == custom_wrapper.generate_service_name(another_function) 70 | 71 | if sys.version_info[0] == 2: 72 | assert custom_wrapper.generate_service_name(my_class.static_method) == "tests.test_custom" 73 | assert custom_wrapper.generate_service_name(MyClass.static_method) == "tests.test_custom" 74 | else: 75 | assert custom_wrapper.generate_service_name(my_class.static_method) == "tests.test_custom.MyClass" 76 | assert custom_wrapper.generate_service_name(MyClass.static_method) == "tests.test_custom.MyClass" 77 | 78 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_NAME"] = "CustomServiceName" 79 | assert custom_wrapper.generate_service_name(module_function) == "CustomServiceName" 80 | 81 | 82 | def test_custom_method_name_fqn_false(): 83 | my_class = MyClass() 84 | 85 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN", None) 86 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_NAME", None) 87 | assert custom_wrapper.generate_method_name(module_function) == "module_function" 88 | assert custom_wrapper.generate_method_name(my_class.class_method) == "class_method" 89 | assert custom_wrapper.generate_method_name(classmethod(my_class.class_method)) == "class_method" 90 | assert custom_wrapper.generate_method_name(another_function) == "another_function" 91 | 92 | assert custom_wrapper.generate_method_name(my_class.static_method) == "static_method" 93 | assert custom_wrapper.generate_method_name(MyClass.static_method) == "static_method" 94 | 95 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_NAME"] = "CustomServiceName" 96 | assert custom_wrapper.generate_method_name(module_function) == "tests.test_custom.module_function" 97 | assert custom_wrapper.generate_method_name(my_class.class_method) == "MyClass.class_method" 98 | assert custom_wrapper.generate_method_name(classmethod(my_class.class_method)) == "MyClass.class_method" 99 | assert custom_wrapper.generate_method_name(another_function) == "tests.test_custom.another_function" 100 | 101 | if sys.version_info[0] == 2: 102 | assert custom_wrapper.generate_method_name(my_class.static_method) == "tests.test_custom.static_method" 103 | assert custom_wrapper.generate_method_name(MyClass.static_method) == "tests.test_custom.static_method" 104 | else: 105 | assert custom_wrapper.generate_method_name(my_class.static_method) == "MyClass.static_method" 106 | assert custom_wrapper.generate_method_name(MyClass.static_method) == "MyClass.static_method" 107 | 108 | 109 | def test_custom_method_name_fqn_true(): 110 | my_class = MyClass() 111 | 112 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_USE_FQN"] = "TRUE" 113 | os.environ.pop("AUTODYNATRACE_CUSTOM_SERVICE_NAME", None) 114 | assert custom_wrapper.generate_method_name(module_function) == "tests.test_custom.module_function" 115 | assert custom_wrapper.generate_method_name(my_class.class_method) == "tests.test_custom.MyClass.class_method" 116 | assert custom_wrapper.generate_method_name(classmethod(my_class.class_method)) == "tests.test_custom.MyClass.class_method" 117 | assert custom_wrapper.generate_method_name(another_function) == "tests.test_custom.another_function" 118 | 119 | os.environ["AUTODYNATRACE_CUSTOM_SERVICE_NAME"] = "CustomServiceName" 120 | assert custom_wrapper.generate_method_name(module_function) == "tests.test_custom.module_function" 121 | assert custom_wrapper.generate_method_name(my_class.class_method) == "tests.test_custom.MyClass.class_method" 122 | assert custom_wrapper.generate_method_name(classmethod(my_class.class_method)) == "tests.test_custom.MyClass.class_method" 123 | assert custom_wrapper.generate_method_name(another_function) == "tests.test_custom.another_function" 124 | 125 | if sys.version_info[0] == 2: 126 | assert custom_wrapper.generate_method_name(my_class.static_method) == "tests.test_custom.static_method" 127 | assert custom_wrapper.generate_method_name(MyClass.static_method) == "tests.test_custom.static_method" 128 | else: 129 | assert custom_wrapper.generate_method_name(my_class.static_method) == "tests.test_custom.MyClass.static_method" 130 | assert custom_wrapper.generate_method_name(MyClass.static_method) == "tests.test_custom.MyClass.static_method" 131 | 132 | 133 | def test_custom_service_instrumentation(): 134 | assert module_function() == 1 135 | assert decorated_method_only() == 1 136 | assert decorated_service_only() == 1 137 | assert decorated_service_and_method() == 1 138 | 139 | 140 | def test_decorator_with_arguments(): 141 | assert custom_wrapper.generate_service_name(module_function, "Service") == "Service" 142 | assert custom_wrapper.generate_method_name(module_function, "method") == "method" 143 | -------------------------------------------------------------------------------- /tests/test_django.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | 3 | from autodynatrace.wrappers.django.utils import get_host, get_request_uri, get_app_name 4 | 5 | 6 | @patch("django.http.HttpRequest") 7 | def test_get_host(django_request_mock): 8 | django_request_mock.get_host = lambda: "myhost:80" 9 | assert get_host(django_request_mock) == "myhost:80" 10 | 11 | django_request_mock.get_host = lambda: None 12 | django_request_mock.META = {"HTTP_HOST": "myhost:80"} 13 | assert get_host(django_request_mock) == "myhost:80" 14 | 15 | django_request_mock.META = {"SERVER_NAME": "myhost", "SERVER_PORT": 80} 16 | assert get_host(django_request_mock) == "myhost:80" 17 | 18 | django_request_mock.META = {} 19 | assert get_host(django_request_mock) == "unknown" 20 | 21 | django_request_mock = None 22 | assert get_host(django_request_mock) == "unknown" 23 | 24 | 25 | @patch("django.http.HttpRequest") 26 | def test_get_request_uri(django_request_mock): 27 | django_request_mock.get_host = lambda: "myhost:80" 28 | django_request_mock.scheme = "http" 29 | django_request_mock.path = "/path" 30 | 31 | assert get_request_uri(django_request_mock) == "http://myhost:80/path" 32 | 33 | 34 | @patch("django.http.HttpRequest") 35 | def test_get_app_name(django_request_mock): 36 | django_request_mock.path = "/path" 37 | django_request_mock.META = {"SERVER_NAME": "myhost", "SERVER_PORT": 80} 38 | assert get_app_name(django_request_mock) == "Django (myhost:80)" 39 | -------------------------------------------------------------------------------- /tests/test_motor.py: -------------------------------------------------------------------------------- 1 | # docker run --name mongo -d -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=password -e MONGO_INITDB_DATABASE=test_database -p 27017:27017 mongo 2 | import autodynatrace 3 | import motor.motor_asyncio 4 | 5 | 6 | def test_mongo_connection(): 7 | uri = "mongodb://root:password@localhost:27017" 8 | client = motor.motor_asyncio.AsyncIOMotorClient(uri) 9 | db = client.test_database 10 | 11 | async def do_insert(): 12 | document = {"key": "value"} 13 | result = await db.test_collection.insert_one(document) 14 | print("result %s" % repr(result.inserted_id)) 15 | 16 | loop = client.get_io_loop() 17 | loop.run_until_complete(do_insert()) 18 | 19 | 20 | if __name__ == "__main__": 21 | test_mongo_connection() 22 | -------------------------------------------------------------------------------- /tests/test_psycopg2.py: -------------------------------------------------------------------------------- 1 | import autodynatrace 2 | 3 | import psycopg2 4 | from psycopg2.extras import RealDictCursor 5 | 6 | 7 | def test_instrumentation(): 8 | conn = psycopg2.connect("dbname=invictus user=invictus password=password") 9 | cur = conn.cursor(cursor_factory=RealDictCursor) 10 | cur.execute("SELECT tablename, tableowner FROM pg_catalog.pg_tables") 11 | for row in cur: 12 | print(row['tablename']) --------------------------------------------------------------------------------