├── test ├── __init__.py ├── test_pyversion.py ├── onesdksamplepy.py ├── test_threading.py ├── urllib_sandbox_py3.py ├── sdk_diag_prog.py ├── test_load_old_agent.py ├── test_version_checks.py ├── test_util.py ├── test_init_public_sdk.py ├── test_import_all.py └── test_public_sdk.py ├── .gitattributes ├── src └── oneagent │ ├── _impl │ ├── __init__.py │ ├── native │ │ ├── __init__.py │ │ ├── sdkdllinfo.py │ │ ├── nativeagent.py │ │ ├── sdkversion.py │ │ └── sdknulliface.py │ └── util.py │ ├── version.py │ ├── __init__.py │ ├── common.py │ └── sdk │ └── tracers.py ├── pytest.ini ├── setup.cfg ├── constraints.txt ├── samples ├── basic-sdk-sample │ ├── README.md │ ├── setup.py │ └── basic_sdk_sample.py └── fork-sdk-sample │ ├── README.md │ ├── setup.py │ └── fork_sdk_sample.py ├── project.toml ├── MANIFEST.in ├── .gitignore ├── docs ├── quickstart.rst ├── index.rst ├── sdkref.rst ├── encoding.rst ├── tagging.rst └── conf.py ├── pylintrc ├── tox.ini ├── test-util-src ├── testconfig.py ├── testhelpers.py └── sdkmockiface.py ├── LICENSE └── setup.py /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /src/oneagent/_impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/oneagent/_impl/native/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = dependsnative : marks a test to depend on the stub binary 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # Py 2 AND 3 compatible 3 | universal=1 4 | 5 | [metadata] 6 | license_file = LICENSE 7 | -------------------------------------------------------------------------------- /constraints.txt: -------------------------------------------------------------------------------- 1 | tox<4 2 | pylint<2.5; python_version >= '3.0' 3 | pylint<1.10; python_version < '3.0' 4 | sphinx<6 5 | pytest<5; python_version < '3.5' 6 | pytest<7.2; python_version >= '3.5' 7 | -------------------------------------------------------------------------------- /samples/basic-sdk-sample/README.md: -------------------------------------------------------------------------------- 1 | # Basic OneAgent SDK for Python sample 2 | 3 | This example demonstrates how to use the [OneAgent SDK for 4 | Python](https://github.com/Dynatrace/OneAgent-SDK-for-Python). 5 | -------------------------------------------------------------------------------- /project.toml: -------------------------------------------------------------------------------- 1 | # https://www.python.org/dev/peps/pep-0518/ 2 | [build-system] 3 | # http://setuptools.readthedocs.io/en/latest/history.html#v36-4-0 4 | # Required for Description-Content-Type 5 | requires = ["setuptools>=36.4"] 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune test 2 | graft src 3 | global-exclude onesdk_shared.* 4 | global-exclude *.pyc 5 | global-exclude *.pyo 6 | global-exclude __pycache__/* 7 | include MANIFEST.in 8 | include README.md 9 | include LICENSE 10 | -------------------------------------------------------------------------------- /samples/fork-sdk-sample/README.md: -------------------------------------------------------------------------------- 1 | # OneAgent SDK for Python forking sample 2 | 3 | This example demonstrates how to use the [OneAgent SDK for 4 | Python](https://github.com/Dynatrace/OneAgent-SDK-for-Python) with forked child processes. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.lib 4 | *.pdb 5 | *.so 6 | *.a 7 | *.launch 8 | build/ 9 | __pycache__/ 10 | onesdk_shared.dll 11 | .tox/ 12 | dist/ 13 | /MANIFEST 14 | .cache/ 15 | /tmp/ 16 | *.egg-info 17 | .pytest_cache/ 18 | venv/ 19 | .venv/ 20 | .tox/ 21 | .vscode 22 | *.whl 23 | db.sqlite3 24 | /.checkstyle 25 | /.project 26 | /.pydevproject 27 | .settings/ 28 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | The following example shows most features of the Python SDK: 5 | 6 | .. literalinclude:: ../samples/basic-sdk-sample/basic_sdk_sample.py 7 | 8 | See the rest of the documentation and the `README on GitHub 9 | `_ 10 | for more information. 11 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | ignore-patterns=six 3 | ignore=wrapt 4 | load-plugins=pylint.extensions.docparams 5 | 6 | [BASIC] 7 | const-rgx=[a-z0-9_]{3,30}|[A-Z0-9_]{3,30} 8 | include-naming-hint=y 9 | good-names=i,j,e,_ 10 | 11 | [FORMAT] 12 | max-line-length=100 13 | 14 | [TYPECHECK] 15 | ignored-classes=oneagent._impl.native.sdkctypesiface.SDKDllInterface,oneagent.sdk.tracers.Tracer 16 | redefining-builtins-modules=oneagent._impl.six.moves 17 | disable=missing-docstring,fixme,too-few-public-methods,missing-return-doc,useless-object-inheritance,cyclic-import,import-outside-toplevel 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to the documentation for Dynatrace OneAgent SDK for Python 2 | ================================================================== 3 | 4 | If you are new to the Dynatrace OneAgent SDK for Python, please consult the 5 | `README on GitHub 6 | `_. 7 | It also contains installation instructions. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | quickstart 14 | sdkref 15 | encoding 16 | tagging 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /test/test_pyversion.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections import namedtuple 3 | 4 | import oneagent 5 | 6 | # pylint:disable=protected-access 7 | 8 | MockVerInfo = namedtuple("MockVerInfo", "major minor patch releaselevel serial") 9 | 10 | def test_pyversion_beta(monkeypatch): 11 | monkeypatch.setattr(sys, 'version_info', MockVerInfo(5, 4, 3, 'beta', 123)) 12 | assert oneagent._get_py_version() == '5.4.3beta123' 13 | 14 | def test_pyversion_final(monkeypatch): 15 | monkeypatch.setattr(sys, 'version_info', MockVerInfo(5, 4, 3, 'final', 0)) 16 | assert oneagent._get_py_version() == '5.4.3' 17 | 18 | def test_py_edition(): 19 | assert oneagent._get_py_edition() 20 | -------------------------------------------------------------------------------- /test/onesdksamplepy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import basic_sdk_sample 17 | from basic_sdk_sample import main #pylint:disable=unused-import 18 | 19 | basic_sdk_sample.input = lambda prompt: None 20 | -------------------------------------------------------------------------------- /docs/sdkref.rst: -------------------------------------------------------------------------------- 1 | oneagent-sdk API Reference 2 | ========================== 3 | 4 | For a high-level overview of SDK concepts, see 5 | https://github.com/Dynatrace/OneAgent-SDK. 6 | 7 | Module :code:`oneagent` (initialization) 8 | ---------------------------------------- 9 | 10 | .. automodule:: oneagent 11 | :members: sdkopts_from_commandline, get_sdk, initialize, shutdown 12 | 13 | .. 14 | .. autofunction:: oneagent.start_agent 15 | .. autofunction:: oneagent.try_start_agent 16 | 17 | Module :code:`oneagent.sdk` 18 | --------------------------- 19 | 20 | .. automodule:: oneagent.sdk 21 | :members: SDK 22 | :show-inheritance: 23 | 24 | Module :code:`oneagent.sdk.tracers` 25 | ----------------------------------- 26 | 27 | .. automodule:: oneagent.sdk.tracers 28 | :members: 29 | :show-inheritance: 30 | 31 | Module :code:`oneagent.common` 32 | ---------------------------------- 33 | 34 | .. automodule:: oneagent.common 35 | :members: 36 | :show-inheritance: 37 | 38 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint, py{34,35,36,37,37,39,310,311,312}-test, docs 3 | skip_missing_interpreters=true 4 | 5 | [testenv] 6 | setenv = 7 | test: PYTHONDONTWRITEBYTECODE = 1 8 | PYTHONPATH = {toxinidir}/test-util-src{:}{toxinidir}/samples/basic-sdk-sample 9 | py34: VIRTUALENV_PIP=19.1.* 10 | py34: VIRTUALENV_SETUPTOOLS = 43.* 11 | py35: VIRTUALENV_SETUPTOOLS = 44.* 12 | py34: VIRTUALENV_WHEEL = 0.33.* 13 | py35,py36: VIRTUALENV_WHEEL = 0.37.* 14 | passenv = DT_AGENTLIBRARY DT_OLDAGENTLIBRARY 15 | changedir = 16 | test: test 17 | commands = 18 | test: pytest . -p testconfig {posargs} 19 | lint: pylint oneagent 20 | lint: pylint ./setup.py 21 | lint: pylint samples/basic-sdk-sample/setup.py samples/basic-sdk-sample/basic_sdk_sample.py 22 | lint-!py27-!pypy: pylint test --disable=redefined-outer-name,unidiomatic-typecheck 23 | lint-py27,lint-pypy: pylint test --disable=redefined-outer-name,unidiomatic-typecheck --ignore-patterns=.*py3.* 24 | deps = 25 | -c constraints.txt 26 | lint-!pypy: pylint<9999 27 | pytest<9999 28 | mock<9999 29 | 30 | [testenv:docs] 31 | commands = 32 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {distdir}/oneagent-docs-html 33 | changedir = docs 34 | deps = 35 | -c constraints.txt 36 | sphinx 37 | -------------------------------------------------------------------------------- /src/oneagent/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | '''Version defines related to the OneAgent SDK for Python. 17 | ''' 18 | 19 | from oneagent._impl.native.sdkversion import OnesdkStubVersion 20 | 21 | # That's the OneAgent SDK for Python version. 22 | # See https://www.python.org/dev/peps/pep-0440/ "Version Identification and 23 | # Dependency Specification" 24 | __version__ = '1.5.1' 25 | 26 | # Define the OneAgent SDK for C/C++ version which should be shipped with this 27 | # Python SDK version. 28 | shipped_c_stub_version = '1.7.1' 29 | 30 | # Below are the minimum and maximum required/supported OneAgent SDK for C/C++ versions. 31 | min_stub_version = OnesdkStubVersion(1, 7, 1) 32 | max_stub_version = OnesdkStubVersion(2, 0, 0) 33 | -------------------------------------------------------------------------------- /src/oneagent/_impl/native/sdkdllinfo.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | '''Support for :mod:`.sdkctypesiface` and :code:`setup.py`.''' 17 | # This file is imported from setup.py so it must not refer to any other local 18 | # modules 19 | 20 | import os 21 | from os import path 22 | import sys 23 | 24 | WIN32 = os.name == 'nt' 25 | IS64BIT = sys.maxsize > 2 ** 32 26 | 27 | DLL_BASENAME = 'onesdk_shared' 28 | def dll_name(): 29 | if WIN32: 30 | pfx = '' 31 | ext = '.dll' 32 | else: 33 | pfx = 'lib' 34 | ext = '.so' 35 | return pfx + DLL_BASENAME + ext 36 | 37 | def _dll_name_in_home(home, libfname=None): 38 | if libfname is None: 39 | libfname = dll_name() 40 | libsubdir = 'lib64' if IS64BIT else 'lib' 41 | return path.join(home, 'agent', libsubdir, libfname) 42 | -------------------------------------------------------------------------------- /src/oneagent/_impl/util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Dynatrace LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | 17 | if hasattr(lambda: None, '__qualname__'): 18 | def getqualname(val): 19 | return val.__qualname__ 20 | else: 21 | def getqualname(val): 22 | return val.__name__ 23 | 24 | def getfullname(val): 25 | """Get the module-qualified name of type or function val.""" 26 | return (val.__module__ or '') + '.' + getqualname(val) 27 | 28 | def error_from_exc(nsdk, tracer_h, e_val=None, e_ty=None): 29 | """Attach appropriate error information to tracer_h. 30 | 31 | If e_val and e_ty are None, the current exception is used.""" 32 | 33 | if not tracer_h: 34 | return 35 | 36 | if e_ty is None and e_val is None: 37 | e_ty, e_val = sys.exc_info()[:2] 38 | if e_ty is None and e_val is not None: 39 | e_ty = type(e_val) 40 | nsdk.tracer_error(tracer_h, getfullname(e_ty), str(e_val)) 41 | -------------------------------------------------------------------------------- /test/test_threading.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import traceback 17 | import threading 18 | from testhelpers import create_dummy_entrypoint, get_nsdk 19 | 20 | def thread_worker(err, sdk): 21 | try: 22 | with create_dummy_entrypoint(sdk): 23 | pass 24 | except Exception: #pylint:disable=broad-except 25 | err.append(traceback.format_exc()) 26 | 27 | 28 | 29 | def test_threading(sdk): 30 | """Regression test for bug where the paththread local was only created on 31 | the thread where the constructor of the mock sdk was called.""" 32 | err = [] 33 | thread = threading.Thread( 34 | target=thread_worker, 35 | args=(err, sdk)) 36 | thread.start() 37 | with create_dummy_entrypoint(sdk): 38 | pass 39 | thread.join() 40 | if err: 41 | raise RuntimeError('Exception on ' + thread.name + ': ' + err[0]) 42 | 43 | assert len(get_nsdk(sdk).finished_paths) == 2 44 | -------------------------------------------------------------------------------- /test-util-src/testconfig.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | import logging 19 | 20 | import pytest 21 | from oneagent import logger 22 | from oneagent import sdk as onesdk 23 | import sdkmockiface 24 | 25 | @pytest.fixture(scope='session', autouse=True) 26 | def setup_logging(): 27 | logger.setLevel(logging.DEBUG) 28 | 29 | @pytest.fixture 30 | def native_sdk_noinit(): 31 | nsdk = sdkmockiface.SDKMockInterface() 32 | yield nsdk 33 | for i, root in enumerate(nsdk.finished_paths): 34 | itag = root.in_tag_as_id 35 | if itag is not None and nsdk.get_finished_node_by_id(itag): 36 | rootstr = str(root) + ' linked under ' + str(root.linked_parent) 37 | else: 38 | rootstr = root.dump() 39 | print('root#{:2}: {}'.format(i, rootstr)) 40 | 41 | @pytest.fixture 42 | def native_sdk(native_sdk_noinit): 43 | native_sdk_noinit.initialize() 44 | yield native_sdk_noinit 45 | 46 | 47 | 48 | @pytest.fixture 49 | def sdk(native_sdk): 50 | return onesdk.SDK(native_sdk) 51 | -------------------------------------------------------------------------------- /test/urllib_sandbox_py3.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import urllib.request as ur 17 | from timeit import default_timer as gtm 18 | 19 | import oneagent 20 | 21 | def bench(): 22 | with oneagent.get_sdk().trace_incoming_remote_call('a', 'b', 'c'): 23 | tdf = 0 24 | t_tot = 0 25 | t_inner = None 26 | do_req = ur.AbstractHTTPHandler.do_request_ 27 | def wrap_req(self, req): 28 | nonlocal tdf 29 | req.host # pylint:disable=pointless-statement 30 | tdf += gtm() - t_inner 31 | return do_req(self, req) 32 | ur.AbstractHTTPHandler.do_request_ = wrap_req 33 | ur.HTTPHandler.http_request = wrap_req 34 | for _ in range(1): 35 | t_inner = gtm() 36 | ur.urlopen('http://localhost:8000/polls').close() 37 | t_tot += gtm() - t_inner 38 | return t_tot, tdf 39 | 40 | if __name__ == '__main__': 41 | oneagent.initialize() 42 | 43 | t_total, t_diff = bench() 44 | 45 | oneagent.shutdown() 46 | print(t_total, t_diff, t_diff / t_total) 47 | -------------------------------------------------------------------------------- /src/oneagent/_impl/native/nativeagent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''Provides access to the native SDK object and entry point of it's 18 | initialization for higher levels. 19 | ''' 20 | 21 | from oneagent.common import SDKError 22 | 23 | _sdk = None 24 | 25 | def _force_initialize(sdkinit): 26 | global _sdk #pylint:disable=global-statement 27 | _sdk = sdkinit 28 | return _sdk 29 | 30 | def initialize(sdkinit=None): 31 | if _sdk: 32 | raise ValueError('Agent is already initialized.') 33 | if not sdkinit or isinstance(sdkinit, str): 34 | from .sdkctypesiface import loadsdk 35 | return _force_initialize(loadsdk(sdkinit)) 36 | return _force_initialize(sdkinit) 37 | 38 | def checkresult(nsdk, error_code, msg=None): 39 | if error_code == 0: 40 | return error_code 41 | emsg = nsdk.strerror(error_code) 42 | if msg: 43 | raise SDKError(error_code, msg + ': ' + emsg) 44 | raise SDKError(error_code, emsg) 45 | 46 | def get_sdk(): 47 | if not _sdk: 48 | raise ValueError('SDK not loaded') 49 | return _sdk 50 | 51 | def try_get_sdk(): 52 | return _sdk 53 | -------------------------------------------------------------------------------- /test/sdk_diag_prog.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | '''Helper module for .test_run_public_sdk.test_sdk_callback_smoke''' 17 | 18 | from __future__ import print_function 19 | 20 | import sys 21 | import gc 22 | 23 | import oneagent 24 | from oneagent._impl import six 25 | from oneagent import sdk as onesdk 26 | 27 | def main(): 28 | call_msg = [] 29 | 30 | init_result = oneagent.initialize() 31 | if not init_result: 32 | print(init_result, file=sys.stderr) 33 | sys.exit(1) 34 | 35 | 36 | def diag_cb(msg): 37 | sys.stderr.flush() 38 | call_msg.append(msg) 39 | 40 | sdk = oneagent.get_sdk() 41 | try: 42 | sdk.set_diagnostic_callback(diag_cb) 43 | sdk.create_database_info(None, None, onesdk.Channel(0)) 44 | gc.collect() 45 | gc.collect() 46 | gc.collect() 47 | print(call_msg) 48 | n_msgs = len(call_msg) 49 | 50 | # Database name must not be null (from CSDK), leaked db info handle 51 | assert n_msgs == 2 52 | 53 | assert all(isinstance(m, six.text_type) for m in call_msg) 54 | sdk.set_diagnostic_callback(None) 55 | sdk.create_database_info(None, None, onesdk.Channel(0)) 56 | print(call_msg[n_msgs:]) 57 | assert len(call_msg) == n_msgs 58 | finally: 59 | oneagent.shutdown() 60 | 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /test/test_load_old_agent.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | 18 | import pytest 19 | 20 | import oneagent 21 | from oneagent.version import shipped_c_stub_version 22 | from oneagent import InitResult 23 | from oneagent.common import AgentState 24 | 25 | @pytest.mark.dependsnative 26 | @pytest.mark.skipif( 27 | 'DT_OLDAGENTLIBRARY' not in os.environ, 28 | reason="No easy way to download these very old agent versions") 29 | def test_load_old_agent(): 30 | saved_path = os.environ.get('DT_AGENTLIBRARY', '') 31 | try: 32 | assert os.environ['DT_OLDAGENTLIBRARY'] is not None 33 | assert os.environ['DT_OLDAGENTLIBRARY'] != '' 34 | os.environ['DT_AGENTLIBRARY'] = os.environ.get('DT_OLDAGENTLIBRARY', '') 35 | 36 | sdk_options = oneagent.sdkopts_from_commandline(remove=True) 37 | init_result = oneagent.initialize(sdk_options) 38 | assert init_result.error is not None 39 | assert init_result.status == InitResult.STATUS_INIT_ERROR 40 | 41 | sdk = oneagent.get_sdk() 42 | 43 | assert sdk.agent_state == AgentState.NOT_INITIALIZED 44 | assert sdk.agent_found 45 | assert not sdk.agent_is_compatible 46 | 47 | assert sdk.agent_version_string == '1.141.246.20180604-140607/' + shipped_c_stub_version 48 | finally: 49 | oneagent.shutdown() 50 | os.environ['DT_AGENTLIBRARY'] = saved_path 51 | -------------------------------------------------------------------------------- /test-util-src/testhelpers.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import subprocess 17 | import os 18 | from os import path 19 | import sys 20 | from types import ModuleType 21 | 22 | def get_nsdk(sdk): 23 | return sdk._nsdk #pylint:disable=protected-access 24 | 25 | def create_dummy_entrypoint(sdk): 26 | return sdk.trace_incoming_remote_call('ENTRY', 'ENTRY', 'ENTRY') 27 | 28 | def run_in_new_interpreter(target, extra_args=(), interpreter_args=()): 29 | if isinstance(target, ModuleType): 30 | tmod = target 31 | else: 32 | tmod = sys.modules.get(target) 33 | 34 | if tmod: 35 | pyfile = path.splitext(tmod.__file__)[0] + '.py' 36 | else: 37 | pyfile = target 38 | 39 | try: 40 | args = [sys.executable] 41 | args.extend(interpreter_args) 42 | args.append(pyfile) 43 | args.extend(extra_args) 44 | env = os.environ.copy() 45 | env['PYTHONPATH'] = os.pathsep.join(sys.path) 46 | return subprocess.check_output( 47 | args, 48 | env=env, 49 | stderr=subprocess.STDOUT, 50 | universal_newlines=True) # Force text mode 51 | except subprocess.CalledProcessError as e: 52 | raise RuntimeError( 53 | 'Error ' + str(e.returncode) + ': ' + e.output) 54 | 55 | def exec_with_dummy_tracer(sdk, func): 56 | nsdk = get_nsdk(sdk) 57 | lbefore = len(nsdk.finished_paths) 58 | tracer = create_dummy_entrypoint(sdk) 59 | func(tracer) 60 | lafter = len(nsdk.finished_paths) 61 | assert lbefore + 1 == lafter 62 | return nsdk.finished_paths[-1] 63 | -------------------------------------------------------------------------------- /src/oneagent/_impl/native/sdkversion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import ctypes 18 | 19 | 20 | class OnesdkStubVersion(ctypes.Structure): 21 | _fields_ = [ 22 | ("major", ctypes.c_uint32), 23 | ("minor", ctypes.c_uint32), 24 | ("patch", ctypes.c_uint32)] 25 | 26 | def __gt__(self, version): 27 | if not isinstance(version, OnesdkStubVersion): 28 | raise TypeError 29 | 30 | if self.major > version.major: 31 | return True 32 | if self.major == version.major: 33 | if self.minor > version.minor: 34 | return True 35 | if self.minor == version.minor: 36 | return self.patch > version.patch 37 | 38 | return False 39 | 40 | def __eq__(self, version): 41 | if not isinstance(version, OnesdkStubVersion): 42 | raise TypeError 43 | 44 | return self.major == version.major and \ 45 | self.minor == version.minor and self.patch == version.patch 46 | 47 | def __ne__(self, version): 48 | return not self == version 49 | 50 | def __lt__(self, version): 51 | if not isinstance(version, OnesdkStubVersion): 52 | raise TypeError 53 | 54 | return not (self > version or self == version) 55 | 56 | def __ge__(self, version): 57 | return self > version or self == version 58 | 59 | def __le__(self, version): 60 | return self < version or self == version 61 | 62 | def __str__(self): 63 | return str(self.major) + '.' + str(self.minor) + '.' + str(self.patch) 64 | -------------------------------------------------------------------------------- /samples/fork-sdk-sample/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import io 18 | 19 | from setuptools import setup 20 | 21 | with io.open('README.md', encoding='utf-8') as readmefile: 22 | long_description = readmefile.read() 23 | del readmefile 24 | 25 | setup( 26 | py_modules=['fork_sdk_sample'], 27 | zip_safe=True, 28 | name='oneagent-sdk-fork-sample', 29 | version='0.0', # This sample is not separately versioned 30 | 31 | install_requires=['oneagent-sdk==1.*,>=1.4'], 32 | 33 | description='OneAgent SDK for Python: Fork sample application', 34 | long_description=long_description, 35 | long_description_content_type='text/markdown', 36 | url='https://github.com/Dynatrace/OneAgent-SDK-for-Python', 37 | maintainer='Dynatrace LLC', 38 | maintainer_email='dynatrace.oneagent.sdk@dynatrace.com', 39 | license='Apache License 2.0', 40 | entry_points={ 41 | 'console_scripts': ['oneagent-sdk-fork-sample=fork_sdk_sample:main'], 42 | }, 43 | classifiers=[ 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved', 46 | 'License :: OSI Approved :: Apache Software License', # 2.0 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: Implementation :: CPython', 52 | 'Operating System :: POSIX :: Linux', 53 | 'Operating System :: Microsoft :: Windows', 54 | 'Topic :: System :: Monitoring' 55 | ]) 56 | -------------------------------------------------------------------------------- /samples/basic-sdk-sample/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import io 18 | 19 | from setuptools import setup 20 | 21 | with io.open('README.md', encoding='utf-8') as readmefile: 22 | long_description = readmefile.read() 23 | del readmefile 24 | 25 | setup( 26 | py_modules=['basic_sdk_sample'], 27 | zip_safe=True, 28 | name='oneagent-sdk-basic-sample', 29 | version='0.0', # This sample is not separately versioned 30 | 31 | install_requires=['oneagent-sdk==1.*,>=1.4'], 32 | 33 | description='OneAgent SDK for Python: Basic sample application', 34 | long_description=long_description, 35 | long_description_content_type='text/markdown', 36 | url='https://github.com/Dynatrace/OneAgent-SDK-for-Python', 37 | maintainer='Dynatrace LLC', 38 | maintainer_email='dynatrace.oneagent.sdk@dynatrace.com', 39 | license='Apache License 2.0', 40 | entry_points={ 41 | 'console_scripts': ['oneagent-sdk-basic-sample=basic_sdk_sample:main'], 42 | }, 43 | classifiers=[ 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved', 46 | 'License :: OSI Approved :: Apache Software License', # 2.0 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2', 49 | 'Programming Language :: Python :: 2.7', 50 | 'Programming Language :: Python :: 3', 51 | 'Programming Language :: Python :: Implementation :: CPython', 52 | 'Operating System :: POSIX :: Linux', 53 | 'Operating System :: Microsoft :: Windows', 54 | 'Topic :: System :: Monitoring' 55 | ]) 56 | -------------------------------------------------------------------------------- /docs/encoding.rst: -------------------------------------------------------------------------------- 1 | .. _encoding: 2 | 3 | String encoding and unicode issues 4 | ================================== 5 | 6 | This section mostly concerns Python 2. 7 | 8 | When the documentation says that a :class:`str` is accepted, an :class:`unicode` 9 | is also always accepted on Python 2. What is more, when the documentation says 10 | that a :class:`str` is returned or passed to a callback, on Python 2 it is 11 | actually a :class:`unicode` (you mostly don't need to care about that though, 12 | because most string operations on Python 2 allow mixing :class:`str` and 13 | :class:`unicode`). 14 | 15 | When passing a :class:`bytes` object (or, equivalently, a :code:`str` object on 16 | Python 2) to a SDK function that says that it accepts a :class:`str`, the bytes 17 | will be interpreted as being UTF-8 encoded. Beware: If the string has invalid 18 | UTF-8 (e.g., Latin-1/ISO-8859-1, as it may occur in :ref:`HTTP headers 19 | `), the function to which it was passed may fail either 20 | partially or fully. Such failures are guaranteed to neither throw exceptions nor 21 | violate any invariants of the involved objects, but some or all of the 22 | information passed in that function call may be lost (for example, a single 23 | invalid HTTP header passed to 24 | :meth:`oneagent.sdk.SDK.trace_incoming_web_request` may cause an null-tracer to 25 | be returned -- but it is also allowed to, for example, truncate that HTTP header 26 | and discard all that follow; the exact failure mode is undefined and you should 27 | take care to not pass invalid strings). Also, the diagnostic callback 28 | (:meth:`oneagent.sdk.SDK.set_diagnostic_callback`) may be invoked (but is not 29 | guaranteed to). 30 | 31 | .. _http-encoding-warning: 32 | 33 | HTTP Request and Response Header Encoding 34 | ----------------------------------------- 35 | 36 | The strings passed to the :code:`add_request_header()`, :code:`add_request_headers()`, 37 | :code:`add_parameter()`, :code:`add_parameters()`, :code:`add_response_header()` and 38 | :code:`add_response_headers()` methods of the :class:`oneagent.sdk.tracers.IncomingWebRequestTracer` 39 | and :class:`oneagent.sdk.tracers.OutgoingWebRequestTracer` classes follow the usual SDK 40 | :ref:`encoding ` conventions, i.e., must be either unicode strings or UTF-8 bytes objects. 41 | 42 | .. warning:: However, `HTTP `_ and `Python's WSGI 43 | `_ will use the Latin-1 encoding on 44 | Python 2. Before passing such Python 2 native strings to these methods, use 45 | :code:`s.decode('Latin-1')` (see :meth:`bytes.decode`) to convert the string to unicode, which 46 | can be correctly handled. 47 | -------------------------------------------------------------------------------- /test/test_version_checks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import pytest 18 | 19 | from oneagent._impl.native.sdkversion import OnesdkStubVersion 20 | 21 | def _check_helper(version, higher_version, equal_version): 22 | assert version < higher_version 23 | assert not higher_version < version 24 | assert version <= higher_version 25 | assert not higher_version <= version 26 | assert higher_version > version 27 | assert not version > higher_version 28 | assert not version == higher_version 29 | assert version <= equal_version 30 | assert version >= equal_version 31 | assert version == equal_version 32 | assert version != higher_version 33 | assert higher_version != equal_version 34 | assert not version != equal_version 35 | 36 | def test_version_major(): 37 | version1 = OnesdkStubVersion(1, 4, 0) 38 | version2 = OnesdkStubVersion(2, 2, 0) 39 | version3 = OnesdkStubVersion(1, 4, 0) 40 | _check_helper(version1, version2, version3) 41 | 42 | def test_version_minor(): 43 | version1 = OnesdkStubVersion(2, 2, 4) 44 | version2 = OnesdkStubVersion(2, 3, 1) 45 | version3 = OnesdkStubVersion(2, 2, 4) 46 | _check_helper(version1, version2, version3) 47 | 48 | def test_version_patch(): 49 | version1 = OnesdkStubVersion(2, 2, 1) 50 | version2 = OnesdkStubVersion(2, 2, 4) 51 | version3 = OnesdkStubVersion(2, 2, 1) 52 | _check_helper(version1, version2, version3) 53 | 54 | def test_none_instance(): 55 | version = OnesdkStubVersion(2, 2, 1) 56 | assert not version is None 57 | with pytest.raises(TypeError): 58 | assert version > None 59 | with pytest.raises(TypeError): 60 | assert version < None 61 | with pytest.raises(TypeError): 62 | assert version >= None 63 | with pytest.raises(TypeError): 64 | assert version <= None 65 | 66 | def test_wrong_instance(): 67 | version = OnesdkStubVersion(2, 2, 1) 68 | with pytest.raises(TypeError): 69 | assert not version == '2.2.1' #pylint:disable=unneeded-not 70 | assert str(version) == '2.2.1' 71 | with pytest.raises(TypeError): 72 | assert version > 'huhu' 73 | with pytest.raises(TypeError): 74 | assert version < 'huhu' 75 | with pytest.raises(TypeError): 76 | assert version >= 'huhu' 77 | with pytest.raises(TypeError): 78 | assert version <= 'huhu' 79 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from oneagent._impl import util 18 | from oneagent._impl import six 19 | from testhelpers import get_nsdk, exec_with_dummy_tracer 20 | 21 | class Foo(object): 22 | class Inner(object): 23 | pass 24 | 25 | FooError = type('FooError', (RuntimeError,), {}) 26 | FooError.__module__ = None 27 | FOO_ERROR_NAME = '.FooError' 28 | 29 | def test_getfullname_inner(): 30 | name = util.getfullname(Foo.Inner) 31 | if six.PY3: 32 | assert name == __name__ + '.Foo.Inner' 33 | else: 34 | assert name == __name__ + '.Inner' 35 | 36 | def test_getfullname_dyn(): 37 | assert util.getfullname(FooError) == FOO_ERROR_NAME 38 | 39 | def exec_with_dummy_node(sdk, func): 40 | def do_exec_with_dummy_node(tracer): 41 | with tracer: 42 | func(tracer) 43 | return exec_with_dummy_tracer(sdk, do_exec_with_dummy_node) 44 | 45 | 46 | def test_error_from_exc(sdk): 47 | nsdk = get_nsdk(sdk) 48 | def check_exc(func): 49 | err_code, err_msg = exec_with_dummy_node(sdk, func).err_info 50 | assert err_code == ValueError.__module__ + '.ValueError' 51 | assert err_msg == 'test' 52 | 53 | def do_raise_auto(tracer): 54 | try: 55 | raise ValueError('test') 56 | except ValueError: 57 | util.error_from_exc(nsdk, tracer.handle) 58 | 59 | def do_raise_val_only(tracer): 60 | util.error_from_exc(nsdk, tracer.handle, ValueError('test')) 61 | 62 | def do_raise_val_ty(tracer): 63 | util.error_from_exc(nsdk, tracer.handle, ValueError('test'), ValueError) 64 | 65 | def do_raise_val_other_ty(tracer): 66 | util.error_from_exc( 67 | nsdk, tracer.handle, ValueError(), RuntimeError) 68 | 69 | check_exc(do_raise_auto) 70 | check_exc(do_raise_val_only) 71 | check_exc(do_raise_val_ty) 72 | err_code, err_msg = exec_with_dummy_node( 73 | sdk, do_raise_val_other_ty).err_info 74 | assert err_code == RuntimeError.__module__ + '.RuntimeError' 75 | assert err_msg == '' 76 | 77 | if six.PY3: 78 | DifficultClass = type(u'DîffîcültClâss', (Exception,), {}) 79 | 80 | def test_getfullname_unicode(): 81 | assert util.getfullname(DifficultClass) == __name__ + u'.DîffîcültClâss' 82 | 83 | def test_error_from_unicode_exc(sdk): 84 | nsdk = get_nsdk(sdk) 85 | def check_exc(func): 86 | err_code, _ = exec_with_dummy_node(sdk, func).err_info 87 | assert err_code == __name__ + '.' + DifficultClass.__name__ 88 | 89 | def do_raise(tracer): 90 | util.error_from_exc(nsdk, tracer.handle, DifficultClass('test')) 91 | 92 | check_exc(do_raise) 93 | -------------------------------------------------------------------------------- /test/test_init_public_sdk.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | import sys 19 | 20 | import pytest 21 | 22 | #pylint:disable=wrong-import-order 23 | from testhelpers import run_in_new_interpreter 24 | 25 | import oneagent 26 | 27 | #pylint:disable=unsupported-membership-test 28 | 29 | @pytest.mark.dependsnative 30 | def test_run_public_sdk(): 31 | '''Test that using the native SDK without any special options works.''' 32 | out = run_in_new_interpreter(__name__) 33 | print('OUTPUT:\n' + out) 34 | assert '-main' in out 35 | assert 'DONE.' in out 36 | 37 | @pytest.mark.dependsnative 38 | def test_run_public_sdk_checkinit(): 39 | '''Test that using the native SDK, additionally checking init return 40 | value.''' 41 | out = run_in_new_interpreter(__name__, ['tryinit']) 42 | print('OUTPUT:\n' + out) 43 | assert '-main' in out 44 | assert 'DONE.' in out 45 | 46 | def test_run_public_sdk_noc(): 47 | '''Test that using the native SDK without the stub does not crash''' 48 | out = run_in_new_interpreter(__name__, ['noc']) 49 | assert 'Error during SDK initialization' in out 50 | assert '-main' in out 51 | assert 'DONE.' in out 52 | 53 | out = run_in_new_interpreter(__name__, ['tryinit noc']) 54 | assert 'Error during SDK initialization' not in out 55 | assert '-main' in out 56 | assert 'DONE.' in out 57 | 58 | def test_mk_sdkopts_alldefaults(): 59 | oldargs = sys.argv 60 | newargs = ['--dt_foo=bar', 'qu', '--dt_bla=off', 'dt_quak=on'] 61 | try: 62 | sys.argv = list(newargs) # copy 63 | opts = oneagent.sdkopts_from_commandline() 64 | assert sys.argv == newargs # Nothing should be removed 65 | finally: 66 | sys.argv = oldargs 67 | assert opts == ['foo=bar', 'bla=off'] 68 | 69 | def test_mk_sdkopts_customprefix(): 70 | argv_orig = ['/SDKfoo=bar', 'qu', '/SDKbla=off', '/sdkquak=on'] 71 | argv = list(argv_orig) # copy 72 | opts = oneagent.sdkopts_from_commandline(argv, prefix='/SDK') 73 | assert argv == argv_orig 74 | assert opts == ['foo=bar', 'bla=off'] 75 | 76 | def test_mk_sdkopts_remove(): 77 | argv_orig = ['/SDKfoo=bar', 'qu', '/SDKbla=off', '--SDKquak=on'] 78 | argv = list(argv_orig) # copy 79 | opts = oneagent.sdkopts_from_commandline(argv, remove=True, prefix='/SDK') 80 | assert argv == [argv_orig[1], argv_orig[3]] 81 | assert opts == ['foo=bar', 'bla=off'] 82 | 83 | 84 | #pylint:enable=unsupported-membership-test 85 | 86 | def main(): 87 | import logging 88 | logger = logging.getLogger('py_sdk') 89 | logger.addHandler(logging.StreamHandler()) 90 | logger.setLevel(logging.DEBUG) 91 | 92 | flags = sys.argv[1].split() if len(sys.argv) > 1 else () 93 | if 'noc' in flags: 94 | sys.modules['oneagent._impl.native.sdkctypesiface'] = False 95 | 96 | if 'tryinit' in flags: 97 | init_result = oneagent.initialize(['loglevelsdk=finest']) 98 | if 'noc' in flags: 99 | assert not init_result 100 | assert init_result.status == init_result.STATUS_STUB_LOAD_ERROR 101 | assert isinstance(init_result.error, ImportError) 102 | else: 103 | assert init_result 104 | assert init_result.status == init_result.STATUS_INITIALIZED 105 | 106 | 107 | from test import onesdksamplepy 108 | onesdksamplepy.main() 109 | print('DONE.') 110 | 111 | if __name__ == '__main__': 112 | main() 113 | -------------------------------------------------------------------------------- /test/test_import_all.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | import os 19 | from os import path 20 | import inspect 21 | import pytest 22 | 23 | import oneagent 24 | from oneagent._impl.native import nativeagent 25 | from oneagent._impl import six 26 | from oneagent import common as sdkcommon 27 | import oneagent._impl.native.sdkctypesiface as csdk 28 | import oneagent._impl.native.sdknulliface as nsdk 29 | 30 | import sdkmockiface as msdk 31 | from sdkmockiface import SDKMockInterface 32 | 33 | try: 34 | getfullargspec = inspect.getfullargspec 35 | except AttributeError: 36 | getfullargspec = inspect.getargspec 37 | 38 | ignoredmods = set([ 39 | 'oneagent.launch_by_import', 40 | ]) 41 | 42 | @pytest.fixture(scope='module', autouse=True) 43 | def set_sdk(): 44 | nativeagent.initialize(SDKMockInterface()) 45 | yield set_sdk 46 | nativeagent._force_initialize(None) #pylint:disable=protected-access 47 | 48 | def test_import_all(): 49 | pkgroot = path.dirname(oneagent.__file__) 50 | for root, dirs, files in os.walk(pkgroot): 51 | try: 52 | dirs.remove('__pycache__') 53 | except ValueError: 54 | pass 55 | for fname in files: 56 | name, ext = path.splitext(fname) 57 | if ext != '.py': 58 | continue 59 | fullmodname = root[len(pkgroot):].strip('/\\') 60 | fullmodname = fullmodname.replace('/', '.').replace('\\', '.') 61 | fullmodname = 'oneagent.' + fullmodname 62 | if name != '__init__': 63 | if not fullmodname.endswith('.'): 64 | fullmodname += '.' 65 | fullmodname += name 66 | fullmodname = fullmodname.strip('.') 67 | if fullmodname in ignoredmods: 68 | continue 69 | if six.PY2 and 'py3' in fullmodname: 70 | continue 71 | print('Importing', fullmodname) 72 | __import__(fullmodname) 73 | 74 | 75 | def pubnames(obj): 76 | return frozenset(name for name in dir(obj) if not name.startswith('_')) 77 | 78 | @pytest.fixture(scope='module') 79 | def csdkinst(): 80 | sdk = csdk.loadsdk() 81 | assert isinstance(sdk, csdk.SDKDllInterface) 82 | return sdk 83 | 84 | @pytest.mark.dependsnative 85 | def test_sdkctypesiface_smoke(csdkinst): 86 | sdk = csdkinst 87 | nativeagent.checkresult( 88 | sdk, sdk.stub_set_variable('agentactive=true', False)) 89 | state = sdk.agent_get_current_state() 90 | assert state == sdkcommon.AgentState.NOT_INITIALIZED 91 | sdk.stub_free_variables() 92 | 93 | def argstr(func): 94 | while hasattr(func, '__wrapped__'): 95 | func = func.__wrapped__ 96 | #pylint:disable=deprecated-method 97 | if not inspect.ismethod(func) and not inspect.isfunction(func): 98 | return '({})'.format(', '.join('_arg' + str(i) for i in range(len(func.argtypes)))) 99 | sig = inspect.signature(func) 100 | iparams = iter(sig.parameters) 101 | if sig.parameters and next(iparams) == 'self': 102 | #pylint:disable=no-member,protected-access 103 | sig = sig.replace(parameters=[sig.parameters[k] for k in iparams]) 104 | nparams = [] 105 | for i, param in enumerate(sig.parameters.values()): 106 | if param.kind != inspect.Parameter.KEYWORD_ONLY: 107 | param = param.replace(name="_arg" + str(i)) 108 | nparams.append(param) 109 | 110 | return str(sig.replace(parameters=nparams)) 111 | 112 | def check_sdk_iface(csdkinst, actual): 113 | cnames = pubnames(csdkinst) 114 | anames = pubnames(actual) 115 | missing = cnames - anames 116 | assert not missing 117 | for name in cnames: 118 | cfunc = getattr(csdkinst, name) 119 | if not callable(cfunc): 120 | continue 121 | cargspec = argstr(cfunc) 122 | aargspec = argstr(getattr(actual, name)) 123 | try: 124 | assert cargspec == aargspec 125 | except AssertionError as e: 126 | raise AssertionError('Mismatch for {}: {}'.format(name, str(e))) 127 | 128 | 129 | return anames - cnames 130 | 131 | @pytest.mark.dependsnative 132 | def test_mock_sdk_impl_match(csdkinst): 133 | print( 134 | 'Additional names in SDKMockInterface: ', 135 | ', '.join(check_sdk_iface(csdkinst, msdk.SDKMockInterface))) 136 | 137 | @pytest.mark.dependsnative 138 | def test_null_sdk_impl_match(csdkinst): 139 | nulliface = nsdk.SDKNullInterface() 140 | # No additional names allowed in SDKNullInterface, it should be minimal 141 | assert not check_sdk_iface(csdkinst, nulliface) 142 | -------------------------------------------------------------------------------- /docs/tagging.rst: -------------------------------------------------------------------------------- 1 | .. _tagging: 2 | 3 | Tagging 4 | ======= 5 | 6 | .. currentmodule:: oneagent.sdk.tracers 7 | 8 | Tagging allows you to link paths from different threads or processes 9 | (potentially even running on completely different hosts) together, i.e. telling 10 | Dynatrace that a particular path branched off from a particular other path node. 11 | Semantically, this usually indicates some kind of remote-call, cross-thread call 12 | or message flow. 13 | 14 | The APIs for tagging are defined in the :class:`OutgoingTaggable` base class, 15 | which allows retrieving a tag for attaching another path to that node, and in 16 | the form of two optional and mutually exclusive :code:`byte_tag` or 17 | :code:`str_tag` parameters to some :code:`trace_*` methods of 18 | :class:`oneagent.sdk.SDK` (these are :dfn:`incoming-taggable`), which allows 19 | attaching that node to the one with the given tag. 20 | 21 | The following classes are incoming-taggable: 22 | 23 | - :class:`IncomingRemoteCallTracer` / 24 | :meth:`oneagent.sdk.SDK.trace_incoming_remote_call` 25 | - :class:`IncomingWebRequestTracer` / 26 | :meth:`oneagent.sdk.SDK.trace_incoming_web_request` 27 | - :class:`IncomingMessageProcessTracer` / 28 | :meth:`oneagent.sdk.SDK.trace_incoming_message_process` 29 | 30 | The following classes are :class:`OutgoingTaggable`: 31 | 32 | - :class:`OutgoingRemoteCallTracer` 33 | - :class:`OutgoingWebRequestTracer` 34 | - :class:`OutgoingMessageTracer` 35 | 36 | You first use either :attr:`OutgoingTaggable.outgoing_dynatrace_string_tag` or 37 | :attr:`OutgoingTaggable.outgoing_dynatrace_byte_tag` to retrieve a string or 38 | binary tag from the tracer where you want branch off from. Then you somehow 39 | forward the tag to the location you want to link. At this location, you pass 40 | that tag as the corresponding :code:`tag` argument for a supported 41 | :code:`trace_*` method. 42 | 43 | Whether you use string or byte tags does not matter for the resulting paths (you 44 | must not mix them, i.e. pass a byte tag as :code:`str_tag` or vice versa). Use 45 | the one that is more convenient for you (e.g. if the tag must be transmitted via 46 | text-based protocol, string tags are the natural choice). If you have no reason 47 | to prefer string tags, byte tags are preferable because they are potentially 48 | smaller and more efficient to process by Dynatrace. 49 | 50 | 51 | Example 52 | ------- 53 | 54 | For example let's say you want to gain insights into a client/server application 55 | that communicates using a custom-built remoting protocol (e.g. over TCP/IP). 56 | First you would instrument the applications separately using, e.g. using the 57 | `trace_*` functions from :class:`oneagent.sdk.SDK` together with `with`-blocks:: 58 | 59 | from oneagent.sdk import SDK, ChannelType, Channel 60 | 61 | # In the client: 62 | def call_remote(message): 63 | tracer = SDK.get().trace_outgoing_remote_call( 64 | 'my_remote_function', 'MyRemoteService', 'MyRemoteEndpoint', 65 | Channel(ChannelType.TCP_IP, 'example.com:12345')) 66 | with tracer: 67 | myremotingchannel.send('my_remote_function', message) 68 | 69 | # In the server: 70 | def my_remote_function(message): 71 | tracer = SDK.get().trace_incoming_remote_call( 72 | 'my_remote_function', 'MyRemoteService', 'MyRemoteEndpoint') 73 | with tracer: 74 | do_actual_work() 75 | 76 | Now you will get two paths that will be completely unrelated from Dynatrace's 77 | point of view. To change that, you do the following: 78 | 79 | 1. Obtain the outgoing tag at the call site. 80 | 2. Forward that tag through to the remote function. 81 | 3. In the remote function, pass the tag (which is now an incoming tag) when 82 | creating the tracer. 83 | 84 | The result after doing this could look like this: 85 | 86 | .. code-block:: python 87 | :emphasize-lines: 9-10,15,18 88 | 89 | from oneagent.sdk import SDK, ChannelType, Channel 90 | 91 | # In the client: 92 | def call_remote(message): 93 | tracer = SDK.get().trace_outgoing_remote_call( 94 | 'my_remote_function', 'MyRemoteService', 'MyRemoteEndpoint', 95 | Channel(ChannelType.TCP_IP, 'example.com:12345')) 96 | with tracer: # Starts the tracer; required for obtaining a tag! 97 | tag = tracer.outgoing_dynatrace_byte_tag 98 | message.add_header('Dynatrace-Tag', tag) # Or some such 99 | myremotingchannel.send('my_remote_function', message) 100 | 101 | # In the server: 102 | def my_remote_function(message): 103 | tag = message.get_header_optional('Dynatrace-Tag') 104 | tracer = SDK.get().trace_incoming_remote_call( 105 | 'my_remote_function', 'MyRemoteService', 'MyRemoteEndpoint', 106 | byte_tag=tag) 107 | with tracer: 108 | do_actual_work() 109 | 110 | Note these points: 111 | 112 | * You are responsible for forwarding the tag yourself. Apart from initially 113 | giving you a binary or string tag, the SDK can give you no help here. E.g. in 114 | the example above it is assumed that our remoting protocol allows adding 115 | arbitrary headers. 116 | * Do not depend on the availability of the tag on the receiver side: you should 117 | always be careful to not introduce additional failure modes into your 118 | application when using the Dynatrace SDK. 119 | * The outgoing tag can only be obtained between starting and ending the tracer. 120 | * You should take care that obtaining the incoming tag is not an expensive 121 | operation, as it can not be accounted for in the timings of the resulting 122 | path. 123 | * Both the string and byte tag are returned as a :class:`bytes` object 124 | (:code:`str` bytestring in Python 2). 125 | This is due to an oversight that cannot be fixed anymore without breaking compatibility. 126 | -------------------------------------------------------------------------------- /samples/fork-sdk-sample/fork_sdk_sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2019 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''This example demonstrates how to use the OneAgent SDK for Python in a 18 | parent/child process environment. The OneAgent SDK for Python will be initialized 19 | in the parent process and the child processes can then use the SDK. 20 | 21 | Note: this example will only work on Linux. There's no Windows support available. 22 | ''' 23 | 24 | import os 25 | import sys 26 | 27 | import oneagent # SDK initialization functions 28 | import oneagent.sdk as onesdk # All other SDK functions. 29 | 30 | try: # Python 2 compatibility. 31 | input = raw_input #pylint:disable=redefined-builtin 32 | except NameError: 33 | pass 34 | 35 | 36 | getsdk = oneagent.get_sdk # Just to make the code shorter. 37 | 38 | 39 | def do_some_fancy_stuff(proc_number): 40 | sdk = getsdk() 41 | 42 | # Initially, the state in the child will be PRE_INITIALIZED (2). 43 | print('Agent fork state (child process before SDK call):', sdk.agent_fork_state) 44 | 45 | # The agent state in the child process should be ACTIVE (0). 46 | print('Agent state (child process #{}): {}'.format(proc_number, sdk.agent_state), flush=True) 47 | 48 | # After calling any SDK function but agent_fork_state, 49 | # the state in the child will be FULLY_INITIALIZED (3). 50 | # In this case the SDK function called was the agent_state property accessed above. 51 | print('Agent fork state (child process after SDK call):', sdk.agent_fork_state) 52 | 53 | print('Agent found:', sdk.agent_found) 54 | print('Agent is compatible:', sdk.agent_is_compatible) 55 | print('Agent version:', sdk.agent_version_string) 56 | 57 | # This call below will complete the OneAgent for Python SDK initialization and then it 58 | # will start the tracer for tracing the custom service 59 | with sdk.trace_custom_service('my_fancy_transaction', 'MyFancyService #{}'.format(proc_number)): 60 | print('do some fancy stuff') 61 | 62 | def create_child_process(proc_number): 63 | pid = os.fork() 64 | if pid == 0: 65 | print('child #{} is running ...'.format(proc_number)) 66 | do_some_fancy_stuff(proc_number) 67 | print('child #{} is exiting ...'.format(proc_number)) 68 | sys.exit(0) 69 | 70 | return pid 71 | 72 | def fork_children(): 73 | print('now starting children ...', flush=True) 74 | pid_1 = create_child_process(1) 75 | pid_2 = create_child_process(2) 76 | 77 | print('waiting for child #1 ...', flush=True) 78 | os.waitpid(pid_1, 0) 79 | print('child #1 exited', flush=True) 80 | 81 | print('waiting for child #2 ...', flush=True) 82 | os.waitpid(pid_2, 0) 83 | print('child #2 exited', flush=True) 84 | 85 | print('all children exited', flush=True) 86 | 87 | def main(): 88 | # This gathers arguments prefixed with '--dt_' from sys.argv into the 89 | # returned list. See also the basic-sdk-sample. 90 | sdk_options = oneagent.sdkopts_from_commandline(remove=True) 91 | 92 | # Before using the SDK you have to initialize the OneAgent. In this scenario, we 93 | # initialize the SDK and prepare it for forking. 94 | # 95 | # Passing in the sdk_options is entirely optional and usually not required 96 | # as all settings will be automatically provided by the Dynatrace OneAgent 97 | # that is installed on the host. 98 | # 99 | # To activate the forking support add the optional 'forkable' parameter and set it to True. 100 | # 101 | # If you run this example on Windows then you'll get an "Invalid Argument" error back 102 | # because there's no forking support for Windows available. 103 | init_result = oneagent.initialize(sdk_options, forkable=True) 104 | try: 105 | if init_result.error is not None: 106 | print('Error during SDK initialization:', init_result.error) 107 | 108 | # While not by much, it is a bit faster to cache the result of 109 | # oneagent.get_sdk() instead of calling the function multiple times. 110 | sdk = getsdk() 111 | 112 | # The agent state is one of the integers in oneagent.sdk.AgentState. 113 | # Since we're using the 'forkable' mode the state will be TEMPORARILY_INACTIVE (1) on Linux. 114 | print('Agent state (parent process):', sdk.agent_state) 115 | 116 | # In the parent, the state will be PARENT_INITIALIZED (1). 117 | print('Agent fork state (parent process):', sdk.agent_fork_state) 118 | 119 | # The instance attribute 'agent_found' indicates whether an agent could be found or not. 120 | print('Agent found:', sdk.agent_found) 121 | 122 | # If an agent was found but it is incompatible with this version of the SDK for Python 123 | # then 'agent_is_compatible' would be set to false. 124 | print('Agent is compatible:', sdk.agent_is_compatible) 125 | 126 | # The agent version is a string holding both the OneAgent version and the 127 | # OneAgent SDK for C/C++ version separated by a '/'. 128 | print('Agent version:', sdk.agent_version_string) 129 | 130 | if init_result.error is None: 131 | fork_children() 132 | input('Now wait until the path appears in the UI ...') 133 | finally: 134 | shutdown_error = oneagent.shutdown() 135 | if shutdown_error: 136 | print('Error shutting down SDK:', shutdown_error) 137 | 138 | if __name__ == '__main__': 139 | main() 140 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2018 Dynatrace LLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | # OneAgent and SDK for Python documentation build configuration file, created by 19 | # sphinx-quickstart on Mon Dec 4 14:43:27 2017. 20 | # 21 | # This file is execfile()d with the current directory set to its 22 | # containing dir. 23 | # 24 | # Note that not all possible configuration values are present in this 25 | # autogenerated file. 26 | # 27 | # All configuration values have a default; values that are commented out 28 | # serve to show the default. 29 | 30 | # If extensions (or modules to document with autodoc) are in another directory, 31 | # add these directories to sys.path here. If the directory is relative to the 32 | # documentation root, use os.path.abspath to make it absolute, like shown here. 33 | # 34 | # import os 35 | # import sys 36 | # sys.path.insert(0, os.path.abspath('.')) 37 | 38 | import datetime 39 | 40 | from oneagent.version import __version__ as version 41 | 42 | # -- General configuration ------------------------------------------------ 43 | 44 | # If your documentation needs a minimal Sphinx version, state it here. 45 | # 46 | # needs_sphinx = '1.0' 47 | 48 | # Add any Sphinx extension module names here, as strings. They can be 49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 50 | # ones. 51 | extensions = [ 52 | 'sphinx.ext.autodoc', 53 | 'sphinx.ext.intersphinx', 54 | 'sphinx.ext.coverage', 55 | 'sphinx.ext.githubpages'] 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ['_templates'] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | # source_suffix = ['.rst', '.md'] 64 | source_suffix = '.rst' 65 | 66 | # The master toctree document. 67 | master_doc = 'index' 68 | 69 | # General information about the project. 70 | project = 'Dynatrace OneAgent and SDK for Python' 71 | copyright = str(datetime.datetime.now().year) + ', Dynatrace LLC' 72 | author = 'Dynatrace LLC' 73 | 74 | # The version info for the project you're documenting, acts as replacement for 75 | # |version| and |release|, also used in various other places throughout the 76 | # built documents. 77 | # 78 | # The short X.Y version. 79 | #version = '' # see import above 80 | # The full version, including alpha/beta/rc tags. 81 | release = version 82 | 83 | # The language for content autogenerated by Sphinx. Refer to documentation 84 | # for a list of supported languages. 85 | # 86 | # This is also used if you do content translation via gettext catalogs. 87 | # Usually you set "language" from the command line for these cases. 88 | language = 'en' 89 | 90 | # List of patterns, relative to source directory, that match files and 91 | # directories to ignore when looking for source files. 92 | # This patterns also effect to html_static_path and html_extra_path 93 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # If true, `todo` and `todoList` produce output, else they produce nothing. 99 | todo_include_todos = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | # 107 | html_theme = 'nature' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | # 113 | html_theme_options = {'sidebarwidth': 500} 114 | 115 | # Add any paths that contain custom static files (such as style sheets) here, 116 | # relative to this directory. They are copied after the builtin static files, 117 | # so a file named "default.css" will overwrite the builtin "default.css". 118 | #html_static_path = ['_static'] 119 | 120 | # Custom sidebar templates, must be a dictionary that maps document names 121 | # to template names. 122 | # 123 | # This is required for the alabaster theme 124 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 125 | html_sidebars = { 126 | '**': [ 127 | 'searchbox.html', 128 | 'localtoc.html', 129 | 'relations.html', # needs 'show_related': True theme option to display 130 | ] 131 | } 132 | 133 | 134 | # -- Options for HTMLHelp output ------------------------------------------ 135 | 136 | # Output file base name for HTML help builder. 137 | htmlhelp_basename = 'OneAgentandSDKforPythondoc' 138 | 139 | 140 | # -- Options for LaTeX output --------------------------------------------- 141 | 142 | latex_elements = { 143 | # The paper size ('letterpaper' or 'a4paper'). 144 | # 145 | # 'papersize': 'letterpaper', 146 | 147 | # The font size ('10pt', '11pt' or '12pt'). 148 | # 149 | # 'pointsize': '10pt', 150 | 151 | # Additional stuff for the LaTeX preamble. 152 | # 153 | # 'preamble': '', 154 | 155 | # Latex figure (float) alignment 156 | # 157 | # 'figure_align': 'htbp', 158 | } 159 | 160 | # Grouping the document tree into LaTeX files. List of tuples 161 | # (source start file, target name, title, 162 | # author, documentclass [howto, manual, or own class]). 163 | latex_documents = [ 164 | (master_doc, 'OneAgentandSDKforPython.tex', 'OneAgent and SDK for Python Documentation', 165 | 'Dynatrace LLC', 'manual'), 166 | ] 167 | 168 | 169 | # -- Options for manual page output --------------------------------------- 170 | 171 | # One entry per manual page. List of tuples 172 | # (source start file, name, description, authors, manual section). 173 | man_pages = [ 174 | (master_doc, 'oneagentandsdkforpython', 'OneAgent and SDK for Python Documentation', 175 | [author], 1) 176 | ] 177 | 178 | 179 | # -- Options for Texinfo output ------------------------------------------- 180 | 181 | # Grouping the document tree into Texinfo files. List of tuples 182 | # (source start file, target name, title, author, 183 | # dir menu entry, description, category) 184 | texinfo_documents = [ 185 | (master_doc, 'OneAgentandSDKforPython', 'OneAgent and SDK for Python Documentation', 186 | author, 'OneAgentandSDKforPython', 'Dynatrace OneAgent and SDK for Python', 187 | 'Miscellaneous'), 188 | ] 189 | 190 | # -- Other ---------------------------------------------------------------- 191 | 192 | intersphinx_mapping = {'python': ('https://docs.python.org/3/', None)} 193 | 194 | # http://www.sphinx-doc.org/en/master/config.html#confval-nitpicky 195 | # Sphinx will warn about all references where the target cannot be found. 196 | nitpicky = True 197 | nitpick_ignore = [ 198 | ('envvar', 'DT_HOME'), 199 | ('py:class', 'callable'), 200 | ('py:class', 'unicode'), 201 | ('py:class', 'typing.Union'), 202 | ('py:class', 'typing.Tuple'), 203 | ('py:class', 'oneagent.common._Uninstantiable') 204 | ] 205 | 206 | autodoc_member_order = 'bysource' 207 | -------------------------------------------------------------------------------- /src/oneagent/_impl/native/sdknulliface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''SDK interface with null implementation, for testing etc.''' 18 | 19 | from __future__ import print_function 20 | 21 | import sys 22 | 23 | from oneagent._impl import six 24 | 25 | from oneagent.common import ErrorCode, AgentState, AgentForkState 26 | 27 | NULL_HANDLE = 0 28 | 29 | class SDKNullInterface(object): #pylint:disable=too-many-public-methods 30 | def __init__(self, version='-/-'): 31 | self._diag_cb = None 32 | self._log_cb = None 33 | self._agent_version = version 34 | 35 | #pylint:disable=no-self-use,unused-argument 36 | 37 | def stub_is_sdk_cmdline_arg(self, arg): 38 | return arg.startswith('--dt_') 39 | 40 | def stub_process_cmdline_arg(self, arg, replace): 41 | return ErrorCode.AGENT_NOT_ACTIVE 42 | 43 | def stub_set_variable(self, assignment, replace): 44 | return ErrorCode.AGENT_NOT_ACTIVE 45 | 46 | def stub_set_logging_level(self, level): 47 | pass 48 | 49 | def stub_default_logging_function(self, level, msg): 50 | print('[OneSDK:NULL]', level, msg, file=sys.stderr) 51 | 52 | def stub_set_logging_callback(self, sink): 53 | self._log_cb = sink 54 | 55 | def stub_free_variables(self): 56 | pass 57 | 58 | def agent_get_version_string(self): 59 | return self._agent_version 60 | 61 | def agent_found(self): 62 | return False 63 | 64 | def agent_is_compatible(self): 65 | return False 66 | 67 | def initialize(self, init_flags=0): 68 | return ErrorCode.AGENT_NOT_ACTIVE 69 | 70 | def shutdown(self): 71 | return ErrorCode.SUCCESS 72 | 73 | def agent_get_current_state(self): 74 | # Although PERMANENTLY_INACTIVE might be more fitting, NOT_INITIALIZED 75 | # is what the stub also returns if it cannot find the agent. 76 | return AgentState.NOT_INITIALIZED 77 | 78 | def agent_get_fork_state(self): 79 | return AgentForkState.ERROR 80 | 81 | def ex_agent_add_process_technology(self, tech_type, tech_edition, tech_version): 82 | pass 83 | 84 | 85 | def agent_set_warning_callback(self, callback): 86 | self._diag_cb = callback 87 | return ErrorCode.SUCCESS 88 | 89 | def agent_set_verbose_callback(self, callback): 90 | return ErrorCode.SUCCESS 91 | 92 | def agent_get_logging_callback(self): 93 | return self._diag_cb 94 | 95 | def strerror(self, error_code): 96 | if error_code == ErrorCode.AGENT_NOT_ACTIVE: 97 | return u'The agent is inactive.' 98 | return u'Unknown error ' + str(error_code) 99 | 100 | def webapplicationinfo_create(self, vhost, appid, ctxroot): 101 | return NULL_HANDLE 102 | 103 | def webapplicationinfo_delete(self, handle): 104 | pass 105 | 106 | def incomingwebrequesttracer_create(self, wapp_h, uri, http_method): 107 | return NULL_HANDLE 108 | 109 | #pylint:disable=invalid-name 110 | 111 | def incomingwebrequesttracer_add_request_headers( 112 | self, tracer_h, keys, vals, count): 113 | pass 114 | 115 | def incomingwebrequesttracer_add_request_header(self, tracer_h, key, val): 116 | pass 117 | 118 | def incomingwebrequesttracer_add_response_headers( 119 | self, tracer_h, keys, vals, count): 120 | pass 121 | 122 | def incomingwebrequesttracer_add_response_header(self, tracer_h, key, val): 123 | pass 124 | 125 | def incomingwebrequesttracer_add_parameters( 126 | self, tracer_h, keys, vals, count): 127 | pass 128 | 129 | def incomingwebrequesttracer_add_parameter(self, tracer_h, key, val): 130 | pass 131 | 132 | def incomingwebrequesttracer_set_remote_address(self, tracer_h, addr): 133 | pass 134 | 135 | def incomingwebrequesttracer_set_status_code(self, tracer_h, code): 136 | pass 137 | 138 | #pylint:enable=invalid-name 139 | 140 | def outgoingwebrequesttracer_create(self, uri, http_method): 141 | return NULL_HANDLE 142 | 143 | #pylint:disable=invalid-name 144 | 145 | def outgoingwebrequesttracer_add_request_headers( 146 | self, tracer_h, keys, vals, count): 147 | pass 148 | 149 | def outgoingwebrequesttracer_add_request_header(self, tracer_h, key, val): 150 | pass 151 | 152 | def outgoingwebrequesttracer_add_response_headers( 153 | self, tracer_h, keys, vals, count): 154 | pass 155 | 156 | def outgoingwebrequesttracer_add_response_header(self, tracer_h, key, val): 157 | pass 158 | 159 | def outgoingwebrequesttracer_set_status_code(self, tracer_h, code): 160 | pass 161 | 162 | #pylint:enable=invalid-name 163 | 164 | def databaseinfo_create(self, dbname, dbvendor, chan_ty, chan_ep): 165 | return NULL_HANDLE 166 | 167 | #pylint:disable=invalid-name 168 | 169 | def databaserequesttracer_create_sql( 170 | self, dbh, sql): 171 | return NULL_HANDLE 172 | 173 | def databaserequesttracer_set_returned_row_count( 174 | self, tracer_h, count): 175 | pass 176 | 177 | def databaserequesttracer_set_round_trip_count( 178 | self, tracer_h, count): 179 | pass 180 | 181 | #pylint:enable=invalid-name 182 | 183 | def databaseinfo_delete(self, dbh): 184 | pass 185 | 186 | def outgoingremotecalltracer_create( #pylint:disable=too-many-arguments 187 | self, svc_method, svc_name, svc_endpoint, chan_ty, chan_ep): 188 | return NULL_HANDLE 189 | 190 | def outgoingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name 191 | self, tracer_h, protocol_name): 192 | pass 193 | 194 | def incomingremotecalltracer_create( 195 | self, svc_method, svc_name, svc_endpoint): 196 | return NULL_HANDLE 197 | 198 | def incomingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name 199 | self, tracer_h, protocol_name): 200 | pass 201 | 202 | def tracer_start(self, tracer_h): 203 | pass 204 | 205 | def tracer_end(self, tracer_h): 206 | pass 207 | 208 | def tracer_error(self, tracer_h, error_class, error_message): 209 | pass 210 | 211 | def tracer_get_outgoing_tag(self, tracer_h, use_byte_tag=False): 212 | # This was originally meant to return a string for use_byte_tag=False 213 | # but the real implementation doesn't do it that way. 214 | return six.binary_type() 215 | 216 | def tracer_set_incoming_string_tag(self, tracer_h, tag): 217 | pass 218 | 219 | def tracer_set_incoming_byte_tag(self, tracer_h, tag): 220 | pass 221 | 222 | #pylint:disable=invalid-name 223 | 224 | def customrequestattribute_add_integers(self, keys, values, count): 225 | pass 226 | 227 | def customrequestattribute_add_integer(self, key, value): 228 | pass 229 | 230 | def customrequestattribute_add_floats(self, keys, values, count): 231 | pass 232 | 233 | def customrequestattribute_add_float(self, key, value): 234 | pass 235 | 236 | def customrequestattribute_add_strings(self, keys, values, count): 237 | pass 238 | 239 | def customrequestattribute_add_string(self, key, value): 240 | pass 241 | 242 | def trace_in_process_link(self, link_bytes): 243 | pass 244 | 245 | def create_in_process_link(self): 246 | pass 247 | 248 | # Messaging API 249 | 250 | #pylint:disable=too-many-arguments 251 | def messagingsysteminfo_create(self, vendor_name, destination_name, destination_type, 252 | channel_type, channel_endpoint): 253 | return NULL_HANDLE 254 | 255 | def messagingsysteminfo_delete(self, handle): 256 | pass 257 | 258 | def outgoingmessagetracer_create(self, handle): 259 | return NULL_HANDLE 260 | 261 | def outgoingmessagetracer_set_vendor_message_id(self, handle, vendor_message_id): 262 | pass 263 | 264 | def outgoingmessagetracer_set_correlation_id(self, handle, correlation_id): 265 | pass 266 | 267 | def incomingmessagereceivetracer_create(self, handle): 268 | return NULL_HANDLE 269 | 270 | def incomingmessageprocesstracer_create(self, handle): 271 | return NULL_HANDLE 272 | 273 | def incomingmessageprocesstracer_set_vendor_message_id(self, handle, message_id): 274 | pass 275 | 276 | def incomingmessageprocesstracer_set_correlation_id(self, handle, correlation_id): 277 | pass 278 | 279 | #pylint:enable=too-many-arguments 280 | #pylint:enable=invalid-name 281 | 282 | def customservicetracer_create(self, service_method, service_name): 283 | return NULL_HANDLE 284 | 285 | def tracecontext_get_current(self): 286 | return (ErrorCode.NO_DATA, '00000000000000000000000000000000', '0000000000000000') 287 | -------------------------------------------------------------------------------- /test/test_public_sdk.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | import pytest 19 | 20 | import oneagent 21 | from oneagent import sdk as onesdk 22 | from oneagent._impl import six 23 | from oneagent._impl.native import nativeagent 24 | 25 | import sdkmockiface 26 | 27 | from testhelpers import ( 28 | get_nsdk, 29 | create_dummy_entrypoint, 30 | run_in_new_interpreter) 31 | from . import sdk_diag_prog 32 | 33 | RTERR_QNAME = RuntimeError.__module__ + '.RuntimeError' 34 | 35 | def test_trace_in_remote_call(sdk): 36 | with sdk.trace_incoming_remote_call('a', 'b', 'c'): 37 | pass 38 | nsdk = get_nsdk(sdk) 39 | assert len(nsdk.finished_paths) == 1 40 | root = nsdk.finished_paths[0] 41 | assert isinstance(root, sdkmockiface.InRemoteCallHandle) 42 | assert root.vals == ('a', 'b', 'c') 43 | 44 | def test_trace_out_remote_call(sdk): 45 | print('SDK:', sdk) 46 | with create_dummy_entrypoint(sdk): 47 | print('SDK2:', sdk) 48 | tracer = sdk.trace_outgoing_remote_call( 49 | 'a', 50 | 'b', 51 | 'c', 52 | onesdk.Channel(onesdk.ChannelType.OTHER, 'e'), 53 | protocol_name='foo') 54 | tracer.start() 55 | tracer.end() 56 | nsdk = get_nsdk(sdk) 57 | assert len(nsdk.finished_paths) == 1 58 | _, root = nsdk.finished_paths[0].children[0] 59 | assert isinstance(root, sdkmockiface.OutRemoteCallHandle) 60 | assert root.vals == ('a', 'b', 'c', onesdk.ChannelType.OTHER, 'e') 61 | assert root.protocol_name == 'foo' 62 | 63 | def test_trace_error(sdk): 64 | try: 65 | with sdk.trace_incoming_remote_call('a', 'b', 'c'): 66 | raise RuntimeError('bla') 67 | except RuntimeError: 68 | pass 69 | else: 70 | assert not 'Exception seems to have been swallowed!' 71 | nsdk = get_nsdk(sdk) 72 | assert len(nsdk.finished_paths) == 1 73 | root = nsdk.finished_paths[0] 74 | assert isinstance(root, sdkmockiface.InRemoteCallHandle) 75 | assert root.vals == ('a', 'b', 'c') 76 | assert root.err_info == (RTERR_QNAME, 'bla') 77 | 78 | 79 | DUMMY_SQL = 'SELECT * FROM tbl;' 80 | 81 | def test_trace_sql_database_request(sdk): 82 | with create_dummy_entrypoint(sdk): 83 | dbi = sdk.create_database_info( 84 | 'dbn', 'dbv', onesdk.Channel(onesdk.ChannelType.OTHER, 'ce')) 85 | hdbi = dbi.handle 86 | with dbi: 87 | tracer = sdk.trace_sql_database_request(dbi, DUMMY_SQL) 88 | with tracer: 89 | tracer.set_round_trip_count(1) 90 | tracer.set_rows_returned(42) 91 | nsdk = get_nsdk(sdk) 92 | assert len(nsdk.finished_paths) == 1 93 | _, root = nsdk.finished_paths[0].children[0] # Strip dummy entrypoint 94 | assert isinstance(root, sdkmockiface.DbRequestHandle) 95 | assert root.vals[0] is hdbi 96 | assert root.vals[1] == DUMMY_SQL 97 | assert root.round_trip_count == 1 98 | assert root.returned_row_count == 42 99 | 100 | DUMMY_URL = 'http://a/b/c' 101 | 102 | def test_trace_iwr_minimal(sdk): 103 | with sdk.create_web_application_info('a', 'b', '/b') as wapp: 104 | wreq = sdk.trace_incoming_web_request(wapp, DUMMY_URL, 'GET') 105 | with wreq: 106 | pass 107 | nsdk = get_nsdk(sdk) 108 | assert len(nsdk.finished_paths) == 1 109 | root = nsdk.finished_paths[0] 110 | assert isinstance(root, sdkmockiface.InWebReqHandle) 111 | assert root.vals[1:] == (DUMMY_URL, 'GET') 112 | assert root.vals[0].vals == ('a', 'b', '/b') 113 | 114 | def test_trace_iwr_full(sdk): 115 | '''Tests an incoming web request with all optional properties (excluding 116 | tag) set.''' 117 | dummy_hdrs = { 118 | 'X-MyHeader': 'my-value', 119 | 'X-MyOtherHeader': 'another value'} 120 | dummy_len = 2 121 | dummy_params = ( 122 | ['username', 'password', 'csrf'], 123 | ('heinz', 'seb2009', 'dummy', 'overlong'), 124 | dummy_len # Skip additional 125 | ) 126 | dummy_params_x = (iter(dummy_params[0]), dummy_params[1], 2) 127 | with sdk.create_web_application_info('a', 'b', '/b') as wapp: 128 | wreq = sdk.trace_incoming_web_request( 129 | wapp, 130 | DUMMY_URL, 131 | 'GET', 132 | headers=dummy_hdrs, 133 | remote_address='127.0.0.1') 134 | with wreq: 135 | wreq.add_parameters(*dummy_params) 136 | wreq.add_parameter('p2', 'vp2') 137 | wreq.add_response_header('Location', DUMMY_URL) 138 | wreq.add_response_headers(*dummy_params) 139 | wreq.set_status_code(200) 140 | nsdk = get_nsdk(sdk) 141 | assert len(nsdk.finished_paths) == 1 142 | root = nsdk.finished_paths[0] 143 | assert isinstance(root, sdkmockiface.InWebReqHandle) 144 | assert root.vals[1:] == (DUMMY_URL, 'GET') 145 | assert root.vals[0].vals == ('a', 'b', '/b') 146 | assert root.req_hdrs == list(dummy_hdrs.items()) 147 | assert root.params == [ 148 | ('username', 'heinz'), 149 | ('password', 'seb2009'), 150 | ('p2', 'vp2')] 151 | assert root.resp_hdrs == [ 152 | ('Location', DUMMY_URL), 153 | ('username', 'heinz'), 154 | ('password', 'seb2009')] 155 | assert root.resp_code == 200 156 | assert root.remote_addr == '127.0.0.1' 157 | 158 | def test_trace_iwr_autocount(sdk): 159 | with sdk.create_web_application_info('a', 'b', '/b') as wapp: 160 | wreq = sdk.trace_incoming_web_request( 161 | wapp, DUMMY_URL, 'GET', headers=(['x', 'y'], ['xv', 'yv'])) 162 | with wreq: 163 | wreq.add_response_headers(['u'], ['uv']) 164 | nsdk = get_nsdk(sdk) 165 | assert len(nsdk.finished_paths) == 1 166 | root = nsdk.finished_paths[0] 167 | assert root.req_hdrs == [('x', 'xv'), ('y', 'yv')] 168 | assert root.resp_hdrs == [('u', 'uv')] 169 | 170 | def assert_resolve_all(nsdk): 171 | unresolved = nsdk.process_finished_paths_tags() 172 | if not unresolved: 173 | return 174 | raise AssertionError('{} nodes with unresolved in-tags: {}'.format( 175 | len(unresolved), ', '.join(map(str, unresolved)))) 176 | 177 | def exec_chk(chk, arg): 178 | try: 179 | result = chk(arg) 180 | if result is not None and result is not True: 181 | raise ValueError('{} returned.'.format(result)) 182 | except AssertionError: 183 | raise 184 | except Exception as e: 185 | six.raise_from( 186 | AssertionError('{!r}({!r}) failed: {}'.format(chk, arg, e)), e) 187 | raise # Shut up pylint 188 | return result 189 | 190 | def chk_seq(vals, chks): 191 | assert len(chks) == len(vals) 192 | for chk, val in zip(chks, vals): 193 | exec_chk(chk, val) 194 | 195 | def chk_all(vals, chk): 196 | for val in vals: 197 | exec_chk(chk, val) 198 | 199 | def check_remote_node(node): 200 | assert node.vals[:3] == ( 201 | 'dummyPyMethod', 'DummyPyService', 202 | 'dupypr://localhost/dummyEndpoint') 203 | assert node.protocol_name == 'DUMMY_PY_PROTOCOL' 204 | 205 | def check_root(root): 206 | assert type(root) is sdkmockiface.InRemoteCallHandle 207 | assert root.vals == ('main', 'main', 'main') 208 | 209 | chk_seq( 210 | root.children, 211 | [check_remote_child_ok] * 2 + [check_remote_child_err]) 212 | 213 | def check_remote_child(child): 214 | link, node = child 215 | assert link == sdkmockiface.TracerHandle.LINK_CHILD 216 | assert type(node) is sdkmockiface.OutRemoteCallHandle 217 | check_remote_node(node) 218 | assert node.vals[3:] == ( 219 | onesdk.ChannelType.IN_PROCESS, 'localhost') 220 | 221 | def check_remote_child_err(child): 222 | check_remote_child(child) 223 | node = child[1] 224 | assert node.err_info == ( 225 | RTERR_QNAME, 'Remote call failed on the server side.') 226 | 227 | def check_linked_remote_thread_err(rmchild): 228 | rmlink, rmnode = rmchild 229 | assert rmlink == sdkmockiface.TracerHandle.LINK_TAG 230 | assert type(rmnode) is sdkmockiface.InRemoteCallHandle 231 | check_remote_node(rmnode) 232 | assert not rmnode.children 233 | 234 | chk_seq(node.children, [check_linked_remote_thread_err]) 235 | 236 | def check_remote_child_ok(child): 237 | check_remote_child(child) 238 | node = child[1] 239 | assert node.err_info is None 240 | 241 | def check_linked_remote_thread(rmchild): 242 | rmlink, rmnode = rmchild 243 | assert rmlink == sdkmockiface.TracerHandle.LINK_TAG 244 | assert type(rmnode) is sdkmockiface.InRemoteCallHandle 245 | check_remote_node(rmnode) 246 | assert rmnode.children 247 | 248 | def chk_dbcall(dbchild): 249 | dblnk, dbnode = dbchild 250 | assert dblnk == sdkmockiface.TracerHandle.LINK_CHILD 251 | assert type(dbnode) is sdkmockiface.DbRequestHandle 252 | 253 | chk_all(rmnode.children, chk_dbcall) 254 | 255 | chk_seq(node.children, [check_linked_remote_thread]) 256 | 257 | def test_public_sdk_sample(native_sdk_noinit, monkeypatch): 258 | # pylint:disable=protected-access 259 | monkeypatch.setattr( 260 | nativeagent, 261 | "initialize", 262 | lambda libname: nativeagent._force_initialize(native_sdk_noinit)) 263 | from . import onesdksamplepy 264 | onesdksamplepy.main() 265 | native_sdk = native_sdk_noinit 266 | 267 | pysdk_tech = sdkmockiface.ProcessTech( 268 | oneagent._PROCESS_TECH_ONEAGENT_SDK, 'Python', oneagent.__version__) 269 | assert pysdk_tech in native_sdk.techs 270 | assert any(tt for tt in native_sdk.techs if tt.type == oneagent._PROCESS_TECH_PYTHON) 271 | 272 | assert_resolve_all(native_sdk) 273 | 274 | 275 | def check_is_linked(root): 276 | assert root.linked_parent 277 | 278 | def check_is_inprocess_link(root): 279 | assert type(root) is sdkmockiface.InProcessLinkTracerHandle 280 | 281 | def check_incoming_web_request(root): 282 | assert type(root) is sdkmockiface.InWebReqHandle 283 | assert len(root.custom_attribs) == 3 284 | key, value = root.custom_attribs[0] 285 | assert key == 'custom int attribute' 286 | assert value == 42 287 | key, value = root.custom_attribs[1] 288 | assert key == 'custom float attribute' 289 | assert value == 1.778 290 | key, value = root.custom_attribs[2] 291 | assert key == 'custom string attribute' 292 | assert value == 'snow is falling' 293 | 294 | def check_outgoing_web_request(root): 295 | assert type(root) is sdkmockiface.OutWebReqHandle 296 | assert root.resp_code == 200 297 | assert len(root.req_hdrs) == 1 298 | header, value = root.req_hdrs[0] 299 | assert header == 'X-not-a-useful-header' 300 | assert value == 'python-was-here' 301 | assert len(root.resp_hdrs) == 1 302 | header, value = root.resp_hdrs[0] 303 | assert header == 'Content-Length' 304 | assert value == '1234' 305 | 306 | def check_is_outgoing_messaging(root): 307 | assert type(root) is sdkmockiface.OutMsgTracerHandle 308 | assert root.vendor_message_id == 'msgId' 309 | assert root.correlation_id == 'corrId' 310 | 311 | def check_custom_service(root): 312 | assert type(root) is sdkmockiface.CustomServiceTracerHandle 313 | assert root.service_method == 'my_fancy_transaction' 314 | assert root.service_name == 'MyFancyService' 315 | 316 | chk_seq( 317 | native_sdk.finished_paths, 318 | ([check_incoming_web_request] + [check_outgoing_web_request] 319 | + [check_is_outgoing_messaging] + [check_custom_service] 320 | + [check_is_linked] * 3 + [check_root] + [check_is_linked] + [check_is_inprocess_link])) 321 | 322 | @pytest.mark.dependsnative 323 | def test_sdk_callback_smoke(): 324 | print(run_in_new_interpreter(sdk_diag_prog)) 325 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Files: All contained files that aren't explicitly mentioned below 2 | Library: oneagent-sdk 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 | License: 214 | 215 | Copyright (c) 2010-2020 Benjamin Peterson 216 | 217 | Permission is hereby granted, free of charge, to any person obtaining a copy of 218 | this software and associated documentation files (the "Software"), to deal in 219 | the Software without restriction, including without limitation the rights to 220 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 221 | the Software, and to permit persons to whom the Software is furnished to do so, 222 | subject to the following conditions: 223 | 224 | The above copyright notice and this permission notice shall be included in all 225 | copies or substantial portions of the Software. 226 | 227 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 228 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 229 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 230 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 231 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 232 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 233 | -------------------------------------------------------------------------------- /samples/basic-sdk-sample/basic_sdk_sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''This example demonstrates instrumenting a (mocked) application that executes 18 | a remote call that sometimes fails and does some database operations.''' 19 | 20 | from __future__ import print_function # Python 2 compatibility. 21 | 22 | import threading 23 | 24 | import oneagent # SDK initialization functions 25 | import oneagent.sdk as onesdk # All other SDK functions. 26 | 27 | from oneagent.common import MessagingDestinationType 28 | 29 | 30 | try: # Python 2 compatibility. 31 | input = raw_input #pylint:disable=redefined-builtin 32 | except NameError: 33 | pass 34 | 35 | IN_DEV_ENVIRONMENT = True # Let's assume we are *not* in production here... 36 | 37 | getsdk = oneagent.get_sdk # Just to make the code shorter. 38 | 39 | def traced_db_operation(dbinfo, sql): 40 | print('+db', dbinfo, sql) 41 | 42 | # Entering the with block automatically start the tracer. 43 | with getsdk().trace_sql_database_request(dbinfo, sql) as tracer: 44 | 45 | # In real-world code, you would do the actual database operation here, 46 | # i.e. call the database's API. 47 | 48 | # Set an optional "exit"-field on the tracer. Whenever there is a 49 | # setter available on a tracer (as opposed to an optional parameter to a 50 | # trace_* function), it may be called anytime between creating and 51 | # ending the tracer (i.e. also after starting it). 52 | tracer.set_round_trip_count(3) 53 | 54 | print('-db', dbinfo, sql) 55 | 56 | def outgoing_remote_call(success): 57 | print('+remote') 58 | 59 | # We use positional arguments to specify required values and named arguments 60 | # to specify optional values. 61 | call = getsdk().trace_outgoing_remote_call( 62 | 'dummyPyMethod', 'DummyPyService', 'dupypr://localhost/dummyEndpoint', 63 | onesdk.Channel(onesdk.ChannelType.IN_PROCESS, 'localhost'), 64 | protocol_name='DUMMY_PY_PROTOCOL') 65 | try: 66 | with call: 67 | 68 | # Note that this property can only be accessed after starting the 69 | # tracer. See the documentation on tagging for more information. 70 | strtag = call.outgoing_dynatrace_string_tag 71 | do_remote_call(strtag, success) 72 | except RuntimeError: # Swallow the exception raised above. 73 | pass 74 | print('-remote') 75 | 76 | failed = [None] 77 | 78 | def do_remote_call_thread_func(strtag, success): 79 | try: 80 | print('+thread') 81 | # We use positional arguments to specify required values and named 82 | # arguments to specify optional values. 83 | incall = getsdk().trace_incoming_remote_call( 84 | 'dummyPyMethod', 'DummyPyService', 85 | 'dupypr://localhost/dummyEndpoint', 86 | protocol_name='DUMMY_PY_PROTOCOL', str_tag=strtag) 87 | with incall: 88 | if not success: 89 | raise RuntimeError('Remote call failed on the server side.') 90 | dbinfo = getsdk().create_database_info( 91 | 'Northwind', onesdk.DatabaseVendor.SQLSERVER, 92 | onesdk.Channel(onesdk.ChannelType.TCP_IP, '10.0.0.42:6666')) 93 | 94 | # This with-block will automatically free the database info handle 95 | # at the end. Note that the handle is used for multiple tracers. In 96 | # general, it is recommended to reuse database (and web application) 97 | # info handles as often as possible (for efficiency reasons). 98 | with dbinfo: 99 | traced_db_operation( 100 | dbinfo, "BEGIN TRAN;") 101 | traced_db_operation( 102 | dbinfo, 103 | "SELECT TOP 1 qux FROM baz ORDER BY quux;") 104 | traced_db_operation( 105 | dbinfo, 106 | "SELECT foo, bar FROM baz WHERE qux = 23") 107 | traced_db_operation( 108 | dbinfo, 109 | "UPDATE baz SET foo = foo + 1 WHERE qux = 23;") 110 | traced_db_operation(dbinfo, "COMMIT;") 111 | print('-thread') 112 | except Exception as e: 113 | failed[0] = e 114 | raise 115 | 116 | 117 | def do_remote_call(strtag, success): 118 | # This function simulates doing a remote call by calling a function 119 | # do_remote_call_thread_func in another thread, passing a string tag. See 120 | # the documentation on tagging for more information. 121 | 122 | failed[0] = None 123 | workerthread = threading.Thread( 124 | target=do_remote_call_thread_func, 125 | args=(strtag, success)) 126 | workerthread.start() 127 | 128 | # Note that we need to join the thread, as all tagging assumes synchronous 129 | # calls. 130 | workerthread.join() 131 | 132 | if failed[0] is not None: 133 | raise failed[0] #pylint:disable=raising-bad-type 134 | 135 | def mock_incoming_web_request(): 136 | sdk = getsdk() 137 | wappinfo = sdk.create_web_application_info( 138 | virtual_host='example.com', # Logical name of the host server. 139 | application_id='MyWebApplication', # Unique web application ID. 140 | context_root='/my-web-app/') # App's prefix of the path part of the URL. 141 | 142 | with wappinfo: 143 | # This with-block will automatically free web application info handle 144 | # at the end. Note that the handle can be used for multiple tracers. In 145 | # general, it is recommended to reuse web application info handles as 146 | # often as possible (for efficiency reasons). For example, if you use 147 | # WSGI, the web application info could be stored as an attribute of the 148 | # application object. 149 | # 150 | # Note that different ways to specify headers, response headers and 151 | # parameter (form fields) not shown here also exist. Consult the 152 | # documentation for trace_incoming_web_request and 153 | # IncomingWebRequestTracer. 154 | wreq = sdk.trace_incoming_web_request( 155 | wappinfo, 156 | 'http://example.com/my-web-app/foo?bar=baz', 157 | 'GET', 158 | headers={'Host': 'example.com', 'X-foo': 'bar'}, 159 | remote_address='127.0.0.1:12345') 160 | with wreq: 161 | wreq.add_parameter('my_form_field', '1234') 162 | # Process web request 163 | wreq.add_response_headers({'Content-Length': '1234'}) 164 | wreq.set_status_code(200) # OK 165 | 166 | # Add 3 different custom attributes. 167 | sdk.add_custom_request_attribute('custom int attribute', 42) 168 | sdk.add_custom_request_attribute('custom float attribute', 1.778) 169 | sdk.add_custom_request_attribute('custom string attribute', 'snow is falling') 170 | 171 | # This call will trigger the diagnostic callback. 172 | sdk.add_custom_request_attribute('another key', None) 173 | 174 | # This call simulates incoming messages. 175 | mock_process_incoming_message() 176 | 177 | def _process_my_outgoing_request(_tag): 178 | pass 179 | 180 | def mock_outgoing_web_request(): 181 | sdk = getsdk() 182 | 183 | # Create tracer and and request headers. 184 | tracer = sdk.trace_outgoing_web_request('http://example.com/their-web-app/bar?foo=foz', 'GET', 185 | headers={'X-not-a-useful-header': 'python-was-here'}) 186 | 187 | with tracer: 188 | # Now get the outgoing dynatrace tag. You have to add this tag as request header to your 189 | # request if you want that the path is continued on the receiving site. Use the constant 190 | # oneagent.common.DYNATRACE_HTTP_HEADER_NAME as request header name. 191 | tag = tracer.outgoing_dynatrace_string_tag 192 | 193 | # Here you process and send your web request. 194 | _process_my_outgoing_request(tag) 195 | 196 | # As soon as the response is received, you can add the response headers to the 197 | # tracer and you shouldn't forget to set the status code, too. 198 | tracer.add_response_headers({'Content-Length': '1234'}) 199 | tracer.set_status_code(200) # OK 200 | 201 | def mock_process_incoming_message(): 202 | sdk = getsdk() 203 | 204 | # Create the messaging system info object. 205 | msi_handle = sdk.create_messaging_system_info( 206 | 'MyPythonSenderVendor', 'MyPythonDestination', MessagingDestinationType.QUEUE, 207 | onesdk.Channel(onesdk.ChannelType.UNIX_DOMAIN_SOCKET, 'MyPythonChannelEndpoint')) 208 | 209 | with msi_handle: 210 | # Create the receive tracer for incoming messages. 211 | with sdk.trace_incoming_message_receive(msi_handle): 212 | print('here we wait for incoming messages ...') 213 | 214 | # Create the tracer for processing incoming messages. 215 | tracer = sdk.trace_incoming_message_process(msi_handle) 216 | 217 | # Now we can set the vendor message and correlation IDs. It's possible to set them 218 | # either before the tracer is started or afterwards. But they have to be set before 219 | # the tracer ends. 220 | tracer.set_vendor_message_id('message_id') 221 | with tracer: 222 | 223 | # Use tracecontext_get_current to log a trace/span ID identifiying the current node. 224 | tinfo = sdk.tracecontext_get_current() 225 | print('[!dt dt.trace_id={},dt.span_id={}] handle incoming message'.format( 226 | tinfo.trace_id, tinfo.span_id)) 227 | 228 | tracer.set_correlation_id('correlation_id') 229 | 230 | def mock_outgoing_message(): 231 | sdk = getsdk() 232 | 233 | # Create the messaging system info object. 234 | msi_handle = sdk.create_messaging_system_info( 235 | 'MyPythonReceiverVendor', 'MyPythonDestination', MessagingDestinationType.TOPIC, 236 | onesdk.Channel(onesdk.ChannelType.TCP_IP, '10.11.12.13:1415')) 237 | 238 | with msi_handle: 239 | # Create the outgoing message tracer; 240 | with sdk.trace_outgoing_message(msi_handle) as tracer: 241 | # Set the message and correlation IDs. 242 | tracer.set_vendor_message_id('msgId') 243 | tracer.set_correlation_id('corrId') 244 | 245 | print('handle outgoing message') 246 | 247 | def mock_custom_service(): 248 | sdk = getsdk() 249 | 250 | with sdk.trace_custom_service('my_fancy_transaction', 'MyFancyService'): 251 | print('do some fancy stuff') 252 | 253 | def _diag_callback(text): 254 | print(text) 255 | 256 | def main(): 257 | print('+main') 258 | 259 | # This gathers arguments prefixed with '--dt_' from sys.argv into the 260 | # returned list. See initialize below. 261 | sdk_options = oneagent.sdkopts_from_commandline(remove=True) 262 | 263 | # Before using the SDK you have to initialize the OneAgent. You can call oneagent.initialize() 264 | # as often as you want, but you also have to call oneagent.shutdown() for every call to 265 | # initialize() as well. 266 | # 267 | # Passing in the sdk_options is entirely optional and usually not required 268 | # as all settings will be automatically provided by the Dynatrace OneAgent 269 | # that is installed on the host. 270 | init_result = oneagent.initialize(sdk_options) 271 | try: 272 | if init_result.error is not None: 273 | print('Error during SDK initialization:', init_result.error) 274 | 275 | # While not by much, it is a bit faster to cache the result of 276 | # oneagent.get_sdk() instead of calling the function multiple times. 277 | sdk = getsdk() 278 | 279 | # Set the diagnostic callback. Strongly recommended. 280 | sdk.set_diagnostic_callback(_diag_callback) 281 | 282 | # Set the verbose callback. 283 | # Not recommended in production as lots of messages can be emitted. 284 | if IN_DEV_ENVIRONMENT: 285 | sdk.set_verbose_callback(_diag_callback) 286 | 287 | # The agent state is one of the integers in oneagent.sdk.AgentState. 288 | print('Agent state:', sdk.agent_state) 289 | 290 | # The instance attribute 'agent_found' indicates whether an agent could be found or not. 291 | print('Agent found:', sdk.agent_found) 292 | 293 | # If an agent was found but it is incompatible with this version of the SDK for Python 294 | # then 'agent_is_compatible' would be set to false. 295 | print('Agent is compatible:', sdk.agent_is_compatible) 296 | 297 | # The agent version is a string holding both the OneAgent version and the 298 | # OneAgent SDK for C/C++ version separated by a '/'. 299 | print('Agent version:', sdk.agent_version_string) 300 | 301 | mock_incoming_web_request() 302 | 303 | mock_outgoing_web_request() 304 | 305 | mock_outgoing_message() 306 | 307 | mock_custom_service() 308 | 309 | # We use trace_incoming_remote_call here, because it is one of the few 310 | # calls that create a new path if none is running yet. 311 | with sdk.trace_incoming_remote_call('main', 'main', 'main'): 312 | # We want to start an asynchronous execution at this time, so we create an 313 | # in-process link which we will use afterwards (or in a different thread). 314 | link = sdk.create_in_process_link() 315 | 316 | # Simulate some remote calls 317 | outgoing_remote_call(success=True) 318 | outgoing_remote_call(success=True) 319 | outgoing_remote_call(success=False) 320 | 321 | # Now the asynchronous execution starts. So we create an in-process tracer. We're using 322 | # the in-process link which we've created above. This link specifies where the traced 323 | # actions below will show up in the path. 324 | with sdk.trace_in_process_link(link): 325 | outgoing_remote_call(success=True) 326 | 327 | print('-main') 328 | input('Now wait until the path appears in the UI...') 329 | finally: 330 | shutdown_error = oneagent.shutdown() 331 | if shutdown_error: 332 | print('Error shutting down SDK:', shutdown_error) 333 | 334 | if __name__ == '__main__': 335 | main() 336 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # 3 | # Copyright 2019 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Beware when cross-building 64/32 bit: 18 | # When using --plat-name to override this, make sure to `rm -r build` othewise 19 | # files from the wrong platform might be reused. 20 | # Also, on Linux because of https://bugs.python.org/issue18987, using a 32 bit 21 | # Python on a 64 bit OS is not enough to change the platform tag. 22 | 23 | from __future__ import print_function 24 | 25 | import io 26 | import os 27 | from os import path 28 | import re 29 | 30 | # https://github.com/PyCQA/pylint/issues/73 31 | #pylint:disable=no-name-in-module,import-error 32 | from distutils.util import get_platform 33 | from distutils.command.build import build 34 | from distutils import log as distlog 35 | #pylint:enable=no-name-in-module,import-error 36 | 37 | 38 | from setuptools import setup, find_packages, Distribution 39 | from setuptools.command.install import install 40 | from setuptools.command.build_ext import build_ext 41 | 42 | from pkg_resources import parse_version 43 | 44 | 45 | try: 46 | from wheel.bdist_wheel import bdist_wheel 47 | except ImportError: 48 | bdist_wheel = None 49 | 50 | CSDK_ENV_NAME = 'DT_PYSDK_CSDK_PATH' 51 | 52 | _THIS_DIR = path.dirname(path.abspath(__file__)) 53 | 54 | with io.open(path.join(_THIS_DIR, 'README.md'), encoding='utf-8') as readmefile: 55 | long_description = readmefile.read() 56 | del readmefile 57 | 58 | 59 | def get_version_from_pkg_info(): 60 | pkg_info_path = path.join(_THIS_DIR, 'PKG-INFO') 61 | 62 | if not path.isfile(pkg_info_path): 63 | return None 64 | 65 | ver_re = re.compile(r"^Version: (.+)$") 66 | 67 | with io.open(pkg_info_path, encoding='utf-8') as pinfofile: 68 | for line in pinfofile: 69 | match = ver_re.match(line) 70 | if match: 71 | return match.group(1) 72 | 73 | return None 74 | 75 | def get_version_from_version_py(): 76 | ver_re = re.compile(r"^__version__ = '([^']+)'$") 77 | 78 | verfilepath = path.join(_THIS_DIR, 'src/oneagent/version.py') 79 | with io.open(verfilepath, encoding='utf-8') as verfile: 80 | for line in verfile: 81 | match = ver_re.match(line) 82 | if match: 83 | version = match.group(1) 84 | break 85 | else: 86 | raise AssertionError('Version not found in src/oneagent/version.py') 87 | 88 | build_timestamp = os.environ.get('BUILD_TIMESTAMP') 89 | if build_timestamp: 90 | version = '{}.{}'.format(version, re.sub(r'-0*', '.', build_timestamp)) 91 | 92 | return version 93 | 94 | 95 | __version__ = get_version_from_pkg_info() 96 | if not __version__: 97 | __version__ = get_version_from_version_py() 98 | 99 | if __version__ != str(parse_version(__version__)): 100 | raise AssertionError( 101 | 'Version {} normalizes to {}'.format( 102 | __version__, parse_version(__version__))) 103 | 104 | # This function was adapted from https://www.python.org/dev/peps/pep-0513/ 105 | # (public domain) 106 | def get_glibc_version_string(): 107 | import ctypes 108 | 109 | try: 110 | process_namespace = ctypes.CDLL(None) 111 | gnu_get_libc_version = process_namespace.gnu_get_libc_version 112 | 113 | # Call gnu_get_libc_version, which returns a string like "2.5". 114 | gnu_get_libc_version.restype = ctypes.c_char_p 115 | version_str = gnu_get_libc_version() 116 | 117 | # py2 / py3 compatibility: 118 | if not isinstance(version_str, str): 119 | version_str = version_str.decode("ascii") 120 | return version_str 121 | except Exception: #pylint:disable=broad-except 122 | # Symbol doesn't exist -> therefore, we are not linked to 123 | # glibc. 124 | return None 125 | 126 | 127 | 128 | def unsupported_msg(plat_name): 129 | try: 130 | import pip 131 | pipver = pip.__version__ 132 | except Exception: #pylint:disable=broad-except 133 | pipver = 'Unknown or not using pip' 134 | 135 | glibc_ver = get_glibc_version_string() 136 | if glibc_ver: 137 | glibc = '\nGNU libc version: ' + glibc_ver + '\n' 138 | elif os.name != 'nt': 139 | glibc = '\nNot using GNU libc. Note that musl libc is not supported.\n' 140 | else: 141 | glibc = '' 142 | 143 | return ''' 144 | 145 | ****************************************************************************** 146 | *** You are trying to build the Python SDK from source. *** 147 | *** This could mean that you are using an outdated version of pip (older *** 148 | *** than 8.1.0) or you are attempting to install the SDK on an *** 149 | *** unsupported platform. Please check the requirements at *** 150 | *** https://github.com/Dynatrace/OneAgent-SDK-for-Python#requirements *** 151 | ****************************************************************************** 152 | Your pip version: {pipver} 153 | Your target platform: {plat}{glibc} 154 | 155 | If you are intentionally building from source, download the OneAgent SDK for 156 | C/C++ that corresponds to this Python SDK (v{v}; see table at 157 | https://github.com/Dynatrace/OneAgent-SDK-for-Python#requirements) from 158 | https://github.com/Dynatrace/OneAgent-SDK-for-C and set the environment variable 159 | {env} to the path to the shared library/DLL correponding to the platform you are 160 | building for.'''.format( 161 | v=__version__, plat=plat_name, env=CSDK_ENV_NAME, pipver=pipver, glibc=glibc) 162 | 163 | 164 | def compilefile(fname, mode='exec'): 165 | with open(fname) as srcfile: 166 | codestr = srcfile.read() 167 | return compile(codestr, fname, mode) 168 | 169 | def adjust_plat_name(self): 170 | #pylint:disable=access-member-before-definition 171 | if self.plat_name is not None: 172 | return 173 | baseplat = get_platform() 174 | if baseplat.startswith('linux'): 175 | platname = baseplat.split('-', 2) 176 | platname[0] = 'manylinux1' 177 | #pylint:disable=attribute-defined-outside-init 178 | self.plat_name = '-'.join(platname) 179 | else: 180 | self.plat_name = baseplat 181 | 182 | if bdist_wheel is not None: 183 | class BdistWheel(bdist_wheel): 184 | def finalize_options(self): 185 | adjust_plat_name(self) 186 | bdist_wheel.finalize_options(self) 187 | 188 | def get_tag(self): 189 | plat_name = self.plat_name or get_platform() 190 | plat_name = plat_name.replace('-', '_').replace('.', '_') 191 | return ( 192 | 'py2.py3' if self.universal else self.python_tag, # impl-tag 193 | 'none', # abi-tag 194 | plat_name) 195 | 196 | def get_dll_info(plat_name): 197 | dll_info = {} 198 | infopath = path.join(_THIS_DIR, 'src/oneagent/_impl/native/sdkdllinfo.py') 199 | exec(compilefile(infopath), dll_info) #pylint:disable=exec-used 200 | if plat_name: 201 | is_win32 = plat_name.startswith('win') 202 | is_64bit = plat_name.endswith('64') 203 | dll_info['IS64BIT'] = is_64bit 204 | dll_info['WIN32'] = is_win32 205 | return dll_info 206 | 207 | def get_dll_input_path(plat_name): 208 | dll_info = get_dll_info(plat_name) # Do this before defaulting plat_name 209 | plat_name = plat_name or get_platform() 210 | sdkpath = os.getenv(CSDK_ENV_NAME) 211 | if not sdkpath: 212 | warn_msg = unsupported_msg(plat_name) 213 | distlog.error(warn_msg) 214 | distlog.warn( 215 | 'Continuning installation, but resulting package will always be in' 216 | ' no-op mode (no connection to Dynatrace will be possible)') 217 | return None 218 | 219 | if path.isfile(sdkpath): 220 | return sdkpath 221 | 222 | if not path.exists(sdkpath): 223 | raise ValueError( 224 | '****** Path "{}" in ${} does not exist. ******'.format( 225 | sdkpath, CSDK_ENV_NAME)) 226 | 227 | # A folder is specified in skdpath. We can try to find some well-known 228 | # binaries in a folder with one of two well-known structures. 229 | 230 | if not dll_info['WIN32'] and 'linux' not in plat_name: 231 | raise ValueError( 232 | '****** Your platform ({}) is not supported by the ' 233 | 'native SDK (its OS is neither Linux nor Windows). ******'.format( 234 | plat_name)) 235 | if '86' not in plat_name and 'amd64' not in plat_name.lower() \ 236 | and plat_name != 'win32': 237 | raise ValueError( 238 | '****** Your platform ({}) is not supported by the ' 239 | 'native SDK (its CPU is not x86/AMD64-based). ******'.format( 240 | plat_name)) 241 | 242 | # Try native SDK distribution package-like 243 | nsdk_platname = '{}-x86_{}'.format( 244 | 'windows' if dll_info['WIN32'] else 'linux', 245 | '64' if dll_info['IS64BIT'] else '32') 246 | 247 | basename = dll_info['dll_name']() 248 | fname = path.join(sdkpath, 'lib', nsdk_platname, basename) 249 | if path.exists(fname): 250 | return fname 251 | 252 | fname = path.join(sdkpath, nsdk_platname, basename) 253 | if path.exists(fname): 254 | return fname 255 | 256 | # Try DT_HOME-like path. 257 | fname = dll_info['_dll_name_in_home'](sdkpath) 258 | if path.exists(fname): 259 | return fname 260 | 261 | # Recommended, however, is setting the environment variable to the filename, 262 | # which is the only way we recommend here. 263 | raise ValueError( 264 | '****** ${} is set to a directory with unknown content.' 265 | ' Please set it to the full path to {}' 266 | ' (including filename) instead. ******'.format(CSDK_ENV_NAME, basename)) 267 | 268 | class PostBuildCommand(build): 269 | __base = build 270 | 271 | def finalize_options(self): 272 | #pylint:disable=access-member-before-definition 273 | has_build_lib = self.build_lib is not None 274 | self.__base.finalize_options(self) 275 | if not has_build_lib: 276 | #pylint:disable=attribute-defined-outside-init 277 | self.build_lib = self.build_platlib 278 | 279 | 280 | class PostBuildExtCommand(build_ext): 281 | __base = build_ext 282 | 283 | def finalize_options(self): 284 | self.__base.finalize_options(self) 285 | 286 | def get_dll_output_path(self): 287 | targetdir = path.join( 288 | self.build_lib, 289 | 'oneagent', 290 | '_impl', 291 | 'native') 292 | return path.join(targetdir, get_dll_info(self.plat_name)['dll_name']()) 293 | 294 | def get_outputs(self): 295 | return self.__base.get_outputs(self) + [self.get_dll_output_path()] 296 | 297 | def get_inputs(self): 298 | extra_inputs = [] 299 | try: 300 | dll_input = get_dll_input_path(self.plat_name) 301 | except ValueError: 302 | pass 303 | else: 304 | if dll_input: 305 | extra_inputs.append(dll_input) 306 | 307 | return self.__base.get_inputs(self) + extra_inputs 308 | 309 | def run(self): 310 | src = get_dll_input_path(self.plat_name) 311 | dst = self.get_dll_output_path() 312 | self.__base.run(self) 313 | self.mkpath(path.dirname(dst)) 314 | if src: 315 | self.copy_file(src, dst) 316 | 317 | def copy_extensions_to_source(self): 318 | self.__base.copy_extensions_to_source(self) 319 | 320 | build_py = self.get_finalized_command('build_py') 321 | src_filename = get_dll_input_path(self.plat_name) 322 | package = 'oneagent._impl.native' 323 | package_dir = build_py.get_package_dir(package) 324 | if src_filename: 325 | dest_filename = path.join(package_dir, path.basename(src_filename)) 326 | self.copy_file(src_filename, dest_filename) 327 | 328 | 329 | 330 | class PostInstallCommand(install): 331 | def finalize_options(self): 332 | #pylint:disable=access-member-before-definition 333 | if self.install_lib is None: 334 | #pylint:disable=attribute-defined-outside-init 335 | self.install_lib = self.install_platlib 336 | return install.finalize_options(self) 337 | 338 | class BinaryDistribution(Distribution): 339 | def has_ext_modules(self): #pylint:disable=no-self-use 340 | return True 341 | 342 | cmdclss = { 343 | 'build': PostBuildCommand, 344 | 'build_ext': PostBuildExtCommand, 345 | 'install': PostInstallCommand, 346 | } 347 | 348 | if bdist_wheel is not None: 349 | cmdclss['bdist_wheel'] = BdistWheel 350 | 351 | def main(): 352 | setup( 353 | packages=find_packages('src'), 354 | package_dir={'': 'src'}, 355 | include_package_data=True, 356 | zip_safe=True, 357 | python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', 358 | cmdclass=cmdclss, 359 | name='oneagent-sdk', 360 | version=__version__, 361 | distclass=BinaryDistribution, 362 | 363 | description='Dynatrace OneAgent SDK for Python', 364 | long_description=long_description, 365 | long_description_content_type='text/markdown', 366 | url='https://github.com/Dynatrace/OneAgent-SDK-for-Python', 367 | download_url='https://pypi.org/project/oneagent-sdk/', 368 | maintainer='Dynatrace LLC', 369 | maintainer_email='dynatrace.oneagent.sdk@dynatrace.com', 370 | license='Apache License 2.0', 371 | classifiers=[ 372 | 'Development Status :: 5 - Production/Stable', 373 | 'Intended Audience :: Developers', 374 | 'License :: OSI Approved', 375 | 'License :: OSI Approved :: Apache Software License', # 2.0 376 | 'Programming Language :: Python', 377 | 'Programming Language :: Python :: 2', 378 | 'Programming Language :: Python :: 2.7', 379 | 'Programming Language :: Python :: 3', 380 | 'Programming Language :: Python :: 3.4', 381 | 'Programming Language :: Python :: 3.5', 382 | 'Programming Language :: Python :: 3.6', 383 | 'Programming Language :: Python :: Implementation :: CPython', 384 | #'Programming Language :: Python :: Implementation :: PyPy', 385 | 'Operating System :: POSIX :: Linux', 386 | 'Operating System :: Microsoft :: Windows', 387 | 'Topic :: System :: Monitoring' 388 | ], 389 | project_urls={ 390 | 'Issue Tracker': 391 | 'https://github.com/Dynatrace/OneAgent-SDK-for-Python/issues', 392 | 'Documentation': 393 | 'https://dynatrace.github.io/OneAgent-SDK-for-Python/', 394 | }) 395 | 396 | if __name__ == '__main__': 397 | main() 398 | -------------------------------------------------------------------------------- /src/oneagent/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''oneagent main module. Contains initialization and logging functionality. 18 | 19 | .. data:: logger 20 | 21 | The :class:`logging.Logger` on which managed Python SDK messages will be 22 | written. 23 | 24 | This logger has set the log level so that no messages are displayed by 25 | default. Use, for example, :code:`oneagent.logger.setLevel(logging.INFO)` to 26 | see them. See :ref:`logging-basic-tutorial` in the official Python 27 | documentation for more on configuring the logger. 28 | 29 | .. class:: InitResult 30 | 31 | Information about the success of a call to :func:`.initialize`. Instances of 32 | this class are falsy iff :attr:`.status` is negative. 33 | 34 | .. attribute:: status 35 | 36 | A :class:`int` with more information about the status. One of the 37 | :code:`.STATUS_*` constants in this class. 38 | 39 | .. attribute:: error 40 | 41 | A :class:`Exception` that occured during this initialization attempt. 42 | Usually, but not necessarily a :class:`oneagent.common.SDKError`. 43 | Another exception class might indicate an incompatible Python version. 44 | 45 | .. data:: STATUS_STUB_LOAD_ERROR 46 | 47 | A negative status code, meaning that the native SDK stub could not be 48 | loaded. This usually indicates that the Dynatrace OneAgent SDK for 49 | Python was incorrectly installed (but see :attr:`error`). Note that even 50 | in this case, a dummy implementation of the SDK is available so that 51 | calls to SDK functions do not fail (but they will be no-ops). 52 | 53 | .. data:: STATUS_INIT_ERROR 54 | 55 | A negative status code, meaning that error occurred during 56 | initialization of the SDK. This usually indicates a problem with the 57 | Dynatrace OneAgent installation on the host (but see :attr:`error`). 58 | 59 | .. data:: STATUS_INITIALIZED 60 | 61 | This status code is equal to zero and means that the SDK has 62 | successfully been initialized. :attr:`error` is always :data:`None` with 63 | this status. 64 | 65 | .. data:: STATUS_INITIALIZED_WITH_WARNING 66 | 67 | A positive status code meaning that the SDK has sucessfully been 68 | initialized, but there have been some warnings (e.g., some options could 69 | not be processed, or the agent is permanently inactive). 70 | 71 | .. data:: STATUS_ALREADY_INITIALIZED 72 | 73 | A positive status code meaning that the SDK has already been initialized 74 | (not necessarily with success), i.e., :func:`initialize` has already been 75 | called. 76 | :attr:`error` is always :data:`None` with this status. 77 | ''' 78 | 79 | import logging 80 | import sys 81 | from collections import namedtuple 82 | from threading import Lock 83 | 84 | from oneagent._impl.six.moves import range #pylint:disable=import-error 85 | from oneagent.version import __version__ 86 | 87 | from .common import ( 88 | SDKError, SDKInitializationError, ErrorCode, 89 | _ONESDK_INIT_FLAG_FORKABLE, _add_enum_helpers) 90 | from ._impl.native import nativeagent 91 | from ._impl.native.nativeagent import try_get_sdk 92 | from ._impl.native.sdknulliface import SDKNullInterface 93 | from ._impl.native.sdkdllinfo import WIN32 94 | 95 | if hasattr(sys, 'implementation'): 96 | def _get_py_edition(): 97 | return sys.implementation.name # pylint:disable=no-member 98 | else: 99 | import platform 100 | 101 | def _get_py_edition(): 102 | return platform.python_implementation() 103 | 104 | logger = logging.getLogger('py_sdk') 105 | logger.setLevel(logging.CRITICAL + 1) # Disabled by default 106 | 107 | _PROCESS_TECH_PYTHON = 28 108 | _PROCESS_TECH_ONEAGENT_SDK = 118 109 | 110 | def _get_py_version(): 111 | return '.'.join(map(str, sys.version_info[:3])) + ( 112 | '' if sys.version_info.releaselevel == "final" 113 | else sys.version_info.releaselevel + str(sys.version_info.serial)) 114 | 115 | @_add_enum_helpers 116 | class InitResult(namedtuple('InitResult', 'status error')): 117 | __slots__ = () 118 | 119 | STATUS_STUB_LOAD_ERROR = -2 120 | STATUS_INIT_ERROR = -1 121 | STATUS_INITIALIZED = 0 122 | STATUS_INITIALIZED_WITH_WARNING = 1 123 | STATUS_ALREADY_INITIALIZED = 2 124 | 125 | __nonzero__ = __bool__ = lambda self: self.status >= 0 126 | 127 | def __repr__(self): 128 | return "InitResult(status={}, error={!r})".format( 129 | self._value_name(self.status), self.error) #pylint:disable=no-member 130 | 131 | 132 | _sdk_ref_lk = Lock() 133 | _sdk_ref_count = 0 134 | _should_shutdown = False 135 | 136 | _sdk_instance = None 137 | 138 | def sdkopts_from_commandline(argv=None, remove=False, prefix='--dt_'): 139 | '''Creates a SDK option list for use with the :code:`sdkopts` parameter of 140 | :func:`.initialize` from a list :code:`argv` of command line parameters. 141 | 142 | An element in :code:`argv` is treated as an SDK option if starts with 143 | :code:`prefix`. The return value of this function will then contain the 144 | remainder of that parameter (without the prefix). If :code:`remove` is 145 | :data:`True`, these arguments will be removed from :code:`argv`. 146 | 147 | :param argv: An iterable of command line parameter 148 | strings. Defaults to :data:`sys.argv`. Must be a 149 | :obj:`~typing.MutableSequence` if :code:`remove` is :data:`True`. 150 | :type argv: ~typing.Iterable[str] or ~typing.MutableSequence[str] 151 | :param bool remove: Whether to remove a command line parameter that was 152 | recognized as an SDK option from :code:`argv` (if :data:`True`) or leave 153 | :code:`argv` unmodified (if :data:`False`). If :data:`True`, 154 | :code:`argv` must be a :obj:`~typing.MutableSequence`. 155 | :param str prefix: The prefix string by which SDK options are recognized and 156 | which is removed from the copy of the command line parameter that is 157 | added to the return value. 158 | 159 | :rtype: list[str] 160 | ''' 161 | 162 | if argv is None: 163 | argv = sys.argv 164 | 165 | if not remove: 166 | return [param[len(prefix):] for param in argv 167 | if param.startswith(prefix)] 168 | result = [] 169 | for i in range(len(argv) - 1, -1, -1): 170 | if argv[i].startswith(prefix): 171 | result.append(argv[i][len(prefix):]) 172 | del argv[i] 173 | result.reverse() 174 | return result 175 | 176 | def get_sdk(): 177 | '''Returns a shared :class:`oneagent.sdk.SDK` instance. 178 | 179 | Repeated calls to this function are supported and will always return the 180 | same object. 181 | 182 | .. note:: You have to initialize the SDK first using :meth:`initialize` 183 | before this function will return a valid SDK instance. 184 | 185 | .. versionadded:: 1.1.0 186 | ''' 187 | global _sdk_instance #pylint:disable=global-statement 188 | 189 | if _sdk_instance is None: 190 | return SDK(SDKNullInterface()) 191 | 192 | return _sdk_instance 193 | 194 | def initialize(sdkopts=(), sdklibname=None, forkable=False): 195 | '''Attempts to initialize the SDK with the specified options. 196 | 197 | Even if initialization fails, a dummy SDK will be available so that SDK 198 | functions can be called but will do nothing. 199 | 200 | If you call this function multiple times, you must call :func:`shutdown` 201 | just as many times. The options from all but the first :code:`initialize` call 202 | will be ignored (the return value will have the 203 | :data:`InitResult.STATUS_ALREADY_INITIALIZED` status code in that case). 204 | 205 | When setting the ``forkable`` flag the OneAgent SDK for Python will only be partly 206 | initialized. In this special **parent-initialized** initialization state, only the following 207 | functions can be called: 208 | 209 | * All functions that are valid to call before calling initialize remain valid. 210 | * :meth:`oneagent.sdk.SDK.agent_version_string` works as expected. 211 | * :meth:`oneagent.sdk.SDK.agent_state` will return 212 | :data:`oneagent.common.AgentState.TEMPORARILY_INACTIVE` - but see the note below. 213 | * :meth:`oneagent.sdk.SDK.set_diagnostic_callback` and 214 | :meth:`oneagent.sdk.SDK.set_verbose_callback` work as expected, 215 | the callback will be carried over to forked child processes. 216 | * It is recommended you call :func:`shutdown` when the original process will not fork any more 217 | children that want to use the SDK. 218 | 219 | After you fork, the child becomes **pre-initialized**: the first call to an SDK function that 220 | needs a **fully initialized** agent will automatically complete the initialization. 221 | 222 | You can still fork another child (e.g. in a double-fork scenario) in the **pre-initialized** 223 | state. However if you fork another child in the **fully initialized** state, it will not be 224 | able to use the SDK - not even if it tries to shut down the SDK and initialize it again. 225 | 226 | .. note:: Calling :meth:`oneagent.sdk.SDK.agent_state` in the **pre-initialized** state will 227 | cause the agent to become **fully initialized**. 228 | 229 | All children forked from a **parent-initialized** process will use the same agent. That agent 230 | will shut down when all child processes and the original **parent-initialized** process have 231 | terminated or called shutdown. Calling :func:`shutdown` in a **pre-initialized** process is 232 | not required otherwise. 233 | 234 | :param sdkopts: A sequence of strings of the form 235 | :samp:`{NAME}={VALUE}` that set the given SDK options. Ignored in all but 236 | the first :code:`initialize` call. 237 | :type sdkopts: ~typing.Iterable[str] 238 | :param str sdklibname: The file or directory name of the native C SDK 239 | DLL. If None, the shared library packaged directly with the agent is 240 | used. Using a value other than None is only acceptable for debugging. 241 | You are responsible for providing a native SDK version that matches the 242 | Python SDK version. 243 | :param bool forkable: Use the SDK in 'forkable' mode. 244 | 245 | :rtype: InitResult 246 | ''' 247 | 248 | global _sdk_ref_count #pylint:disable=global-statement 249 | global _sdk_instance #pylint:disable=global-statement 250 | 251 | with _sdk_ref_lk: 252 | logger.debug("initialize: ref count = %d", _sdk_ref_count) 253 | result = _try_init_noref(sdkopts, sdklibname, forkable) 254 | if _sdk_instance is None: 255 | _sdk_instance = SDK(try_get_sdk()) 256 | _sdk_ref_count += 1 257 | return result 258 | 259 | 260 | def _try_init_noref(sdkopts=(), sdklibname=None, forkable=False): 261 | global _should_shutdown #pylint:disable=global-statement 262 | 263 | sdk = nativeagent.try_get_sdk() 264 | if sdk: 265 | logger.debug( 266 | 'Attempt to re-initialize agent' 267 | ' with options=%s, libname=%s only increases' 268 | ' reference count.', 269 | sdkopts, 270 | sdklibname) 271 | 272 | return InitResult(InitResult.STATUS_ALREADY_INITIALIZED, None) 273 | 274 | try: 275 | logger.info( 276 | 'Initializing SDK on Python=%s with options=%s, libname=%s.', 277 | (sys.version or '?').replace('\n', ' ').replace('\r', ''), sdkopts, sdklibname) 278 | sdk = nativeagent.initialize(sdklibname) 279 | 280 | have_warning = False 281 | for opt in sdkopts: 282 | err = sdk.stub_set_variable(opt, False) 283 | if err: 284 | have_warning = True 285 | logger.warning( 286 | 'stub_set_variable failed for "%s" with error 0x%x: %s', 287 | opt, 288 | err, 289 | sdk.strerror(err)) 290 | 291 | if WIN32 and forkable: 292 | logger.warning('SDK can''t be initialized in forkable mode on Windows and Solaris') 293 | 294 | flags = _ONESDK_INIT_FLAG_FORKABLE if forkable else 0 295 | 296 | nativeagent.checkresult(sdk, sdk.initialize(flags), 'onesdk_initialize_2') 297 | _should_shutdown = True 298 | logger.debug('initialize successful, adding tech types...') 299 | sdk.ex_agent_add_process_technology(_PROCESS_TECH_ONEAGENT_SDK, 'Python', __version__) 300 | sdk.ex_agent_add_process_technology( 301 | _PROCESS_TECH_PYTHON, _get_py_edition(), _get_py_version()) 302 | logger.debug('tech type reporting complete') 303 | return InitResult( 304 | (InitResult.STATUS_INITIALIZED_WITH_WARNING if have_warning else 305 | InitResult.STATUS_INITIALIZED), 306 | None) 307 | except Exception as e: #pylint:disable=broad-except 308 | _should_shutdown = False 309 | #pylint:disable=no-member 310 | if isinstance(e, SDKError) and e.code == ErrorCode.AGENT_NOT_ACTIVE: 311 | #pylint:enable=no-member 312 | logger.debug('initialized, but agent not active') 313 | return InitResult(InitResult.STATUS_INITIALIZED_WITH_WARNING, e) 314 | logger.exception('Failed initializing agent.') 315 | sdk = nativeagent.try_get_sdk() 316 | if sdk: 317 | logger.warning('Continuing with stub-SDK only.') 318 | return InitResult(InitResult.STATUS_INIT_ERROR, e) 319 | 320 | _v = '-/-' if not isinstance(e, SDKInitializationError) else e.agent_version 321 | 322 | nativeagent.initialize(SDKNullInterface(version=_v)) 323 | 324 | logger.warning('Continuing with NULL-SDK only.') 325 | return InitResult(InitResult.STATUS_STUB_LOAD_ERROR, e) 326 | 327 | def shutdown(): 328 | '''Shut down the SDK. 329 | 330 | :returns: An exception object if an error occurred, a falsy value otherwise. 331 | 332 | :rtype: Exception 333 | ''' 334 | global _sdk_ref_count #pylint:disable=global-statement 335 | global _sdk_instance #pylint:disable=global-statement 336 | global _should_shutdown #pylint:disable=global-statement 337 | 338 | with _sdk_ref_lk: 339 | logger.debug("shutdown: ref count = %d, should_shutdown = %s", \ 340 | _sdk_ref_count, _should_shutdown) 341 | nsdk = nativeagent.try_get_sdk() 342 | if not nsdk: 343 | logger.warning('shutdown: SDK not initialized or already shut down') 344 | _sdk_ref_count = 0 345 | return None 346 | if _sdk_ref_count > 1: 347 | logger.debug('shutdown: reference count is now %d', _sdk_ref_count) 348 | _sdk_ref_count -= 1 349 | return None 350 | logger.info('shutdown: Shutting down SDK.') 351 | try: 352 | if _should_shutdown: 353 | _rc = nsdk.shutdown() 354 | if _rc == ErrorCode.NOT_INITIALIZED: 355 | logger.warning('shutdown: native SDK was not initialized') 356 | else: 357 | nativeagent.checkresult(nsdk, _rc, 'shutdown') 358 | _should_shutdown = False 359 | except SDKError as e: 360 | logger.warning('shutdown failed', exc_info=sys.exc_info()) 361 | return e 362 | _sdk_ref_count = 0 363 | _sdk_instance = None 364 | nativeagent._force_initialize(None) #pylint:disable=protected-access 365 | logger.debug('shutdown: completed') 366 | return None 367 | 368 | #pylint:disable=wrong-import-position 369 | from .sdk import SDK # Public 370 | -------------------------------------------------------------------------------- /src/oneagent/common.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 Dynatrace LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | '''Defines basic SDK constants and classes. 17 | 18 | All public names here are also re-exported from :mod:`oneagent.sdk` and should 19 | preferably be used from there. 20 | ''' 21 | 22 | import os 23 | 24 | _DEBUG_LEAKS = False 25 | 26 | if _DEBUG_LEAKS: 27 | import traceback 28 | 29 | #: The Dynatrace Tag request header name which is used to transport the tag between agents 30 | #: (as a string tag). 31 | DYNATRACE_HTTP_HEADER_NAME = 'X-dynaTrace' 32 | 33 | #: The Dynatrace Tag messaging property name which is is used to transport the tag between agents 34 | #: (as a byte tag). 35 | #: 36 | #: .. versionadded:: 1.3 37 | DYNATRACE_MESSAGE_PROPERTY_NAME = "dtdTraceTagInfo" 38 | 39 | #: DEPRECATED alias for :data:`DYNATRACE_MESSAGE_PROPERTY_NAME` 40 | #: 41 | #: .. deprecated:: 1.3 42 | DYNATRACE_MESSAGE_PROPERTYNAME = DYNATRACE_MESSAGE_PROPERTY_NAME 43 | 44 | 45 | #: Allow SDK to be used in forked child processes. 46 | _ONESDK_INIT_FLAG_FORKABLE = 1 47 | class _Uninstantiable(object): 48 | '''Classes deriving from this class cannot be instantiated.''' 49 | 50 | def __new__(cls): 51 | raise ValueError('Attempt to instantiate') 52 | 53 | def _add_enum_helpers(decorated_cls): 54 | # pylint:disable=protected-access 55 | decorated_cls._enum_name_by_val = dict() 56 | for key in dir(decorated_cls): 57 | val = getattr(decorated_cls, key) 58 | if isinstance(val, int): 59 | decorated_cls._enum_name_by_val.setdefault(val, key) 60 | 61 | @classmethod 62 | def _value_name(cls, val): 63 | result = cls._enum_name_by_val.get(val) # pylint:disable=no-member 64 | if result is None: 65 | return "" 66 | return cls.__name__ + "." + result 67 | decorated_cls._value_name = _value_name 68 | return decorated_cls 69 | 70 | 71 | class AgentState(_Uninstantiable): 72 | '''Constants for the agent's state. See 73 | :attr:`oneagent.sdk.SDK.agent_state`.''' 74 | 75 | #: The SDK stub is connected to the agent, which is currently active. 76 | ACTIVE = 0 77 | 78 | #: The SDK stub is connected to the agent, which is temporarily inactive. 79 | TEMPORARILY_INACTIVE = 1 80 | 81 | #: The SDK stub is connected to the agent, which is permanently inactive. 82 | PERMANENTLY_INACTIVE = 2 83 | 84 | 85 | #: The agent has not been initialized. 86 | NOT_INITIALIZED = 3 87 | 88 | #: Some unexpected error occurred while trying to determine the agent state. 89 | ERROR = -1 90 | 91 | class ErrorCode(_Uninstantiable): 92 | '''Constants for error codes of the native agent, as may be contained in 93 | :attr:`.SDKError.code`.''' 94 | 95 | # Same bit pattern if interpreted in 32 bit unsigned / two's complement 96 | _ERROR_BASE = 0xAFFE0000 if os.name == 'nt' else -0x50020000 97 | 98 | #: The operation completed successfully. You usually won't get any object 99 | #: with error code at all in that case. 100 | SUCCESS = 0 101 | 102 | #: The operation failed, but no more specific error code fits the failure. 103 | GENERIC = _ERROR_BASE + 1 104 | 105 | #: A function was called with an invalid argument. 106 | INVALID_ARGUMENT = _ERROR_BASE + 2 107 | 108 | NOT_IMPLEMENTED = _ERROR_BASE + 3 #: The called function is not implemented. 109 | 110 | NOT_INITIALIZED = _ERROR_BASE + 4 #: The SDK has not been initialized. 111 | 112 | #: There is not enough available memory to complete the operation. 113 | OUT_OF_MEMORY = _ERROR_BASE + 5 114 | 115 | #: The native SDK stub was configured to _not_ try to load the actual agent 116 | #: module. 117 | AGENT_NOT_ACTIVE = _ERROR_BASE + 6 118 | 119 | #: Either the OneAgent SDK for C/C++ or the OneAgent binary could not be loaded. 120 | LOAD_AGENT = _ERROR_BASE + 7 121 | 122 | #: The expected exports could not be found either in the OneAgent SDK for C/C++ 123 | #: or the OneAgent binary. 124 | INVALID_AGENT_BINARY = _ERROR_BASE + 8 125 | 126 | #: The operation failed because of an unexpected error. 127 | UNEXPECTED = _ERROR_BASE + 9 128 | 129 | #: The command line argument / stub variable definition was ignored because 130 | #: an entry with the same key was already present. 131 | ENTRY_ALREADY_EXISTS = _ERROR_BASE + 10 132 | 133 | #: The SDK agent module doesn't support the feature level required by this 134 | #: version of the SDK stub. 135 | FEATURE_LEVEL_NOT_SUPPORTED = _ERROR_BASE + 11 136 | 137 | #: The SDK agent module doesn't support the SDK interface required by this 138 | #: version of the SDK stub 139 | INTERFACE_NOT_SUPPORTED = _ERROR_BASE + 12 140 | 141 | #: The operation failed because this is the child process of a fork that 142 | #: occurred while the SDK was initialized. 143 | FORK_CHILD = _ERROR_BASE + 13 144 | 145 | #: The operation completed without error, but there was no data to return. 146 | NO_DATA = _ERROR_BASE + 14 147 | 148 | class AgentForkState(_Uninstantiable): 149 | '''Constants for the agent's fork state. See 150 | :attr:`oneagent.sdk.SDK.agent_fork_state`.''' 151 | 152 | #: SDK cannot be used in this process, but forked processes may use the SDK. 153 | #: This is the state of the process 154 | #: that called :func:`oneagent.initialize` with :code:`forkable=True` 155 | PARENT_INITIALIZED = 1 156 | 157 | #: Forked processes can use the SDK. 158 | #: Using the SDK in this process is allowed but 159 | #: changes the state to :attr:`.FULLY_INITIALIZED` 160 | #: This is the state of all child processes 161 | #: of a process that is :attr:`.PARENT_INITIALIZED`. 162 | PRE_INITIALIZED = 2 163 | 164 | #: SDK can be used, forked processes may not use the SDK. 165 | #: This is the state of a process that was previously :attr:`.PRE_INITIALIZED` 166 | #: and then called an SDK function. 167 | FULLY_INITIALIZED = 3 168 | 169 | #: SDK can be used, forked processes may not use the SDK, 170 | #: :func:`oneagent.initialize` was called without :code:`forkable=True`. 171 | NOT_FORKABLE = 4 172 | 173 | #: Some error occurred while trying to determine the agent fork state. 174 | ERROR = -1 175 | 176 | class MessageSeverity(_Uninstantiable): # Private 177 | '''Constants for the severity of log messages. 178 | 179 | The levels with the lower numerical values include all messages of the ones 180 | with the higher values. Note that :attr:`.DEBUG` is the highest severity, 181 | contrary to usual conventions.''' 182 | 183 | FINEST = 0 #: Most verbose logging (higly detailed tracing). 184 | FINER = 1 #: Slightly less verbose logging (fairly detailed tracing). 185 | FINE = 2 #: Still verbose logging (informational tracing messages). 186 | CONFIG = 3 #: Log configuration messages. 187 | INFO = 4 #: Log informational messages. 188 | WARNING = 5 #: Log conditions that indicate a potential problem. 189 | SEVERE = 6 #: Log messages indicating a serious failure. 190 | 191 | #: Debug message. None should be logged by default, unless they are 192 | #: specifically enabled with special debug options. Note that contrary to 193 | #: usual conventions, this is the highest severity. 194 | DEBUG = 7 195 | 196 | #: No messages of this level exist, so using this level disables all log 197 | #: messages. 198 | NONE = 8 199 | 200 | class MessagingDestinationType(_Uninstantiable): 201 | '''Messaging Destination Type Constants 202 | ''' 203 | QUEUE = 1 #: A message queue: a message sent to this destination will be (successfully) 204 | #: received by only one consumer. 205 | TOPIC = 2 #: A message topic: a message sent to this destination will be received by all 206 | #: subscribed consumers. 207 | 208 | class MessagingVendor(_Uninstantiable): 209 | '''Messaging System Vendor Strings 210 | ''' 211 | HORNETQ = "HornetQ" #: vendor string for HornetQ 212 | ACTIVE_MQ = "ActiveMQ" #: vendor string for ActiveMQ 213 | RABBIT_MQ = "RabbitMQ" #: vendor string for RabbitMQ 214 | ARTEMIS = "Artemis" #: vendor string for Artemis 215 | WEBSPHERE = "WebSphere" #: vendor string for WebSphere 216 | MQSERIES_JMS = "MQSeries JMS" #: vendor string for MQSeries JMS 217 | MQSERIES = "MQSeries" #: vendor string for MQSeries 218 | TIBCO = "Tibco" #: vendor string for Tibco 219 | 220 | class DatabaseVendor(_Uninstantiable): 221 | '''String constants for well-known database vendors. Use for the 222 | :code:`vendor` parameter of 223 | :meth:`oneagent.sdk.SDK.create_database_info`.''' 224 | 225 | APACHE_HIVE = "ApacheHive" #: Database vendor string for Apache Hive. 226 | 227 | #: Database vendor string for Apache Derby (aka. IBM Cloudscape). 228 | CLOUDSCAPE = "Cloudscape" 229 | 230 | HSQLDB = "HSQLDB" #: Database vendor string for HyperSQL DB. 231 | 232 | #: Database vendor string for OpenEdge Database (aka. Progress). 233 | PROGRESS = "Progress" 234 | 235 | MAXDB = "MaxDB" #: Database vendor string for SAP MaxDB. 236 | HANADB = "HanaDB" #: Database vendor string for SAP HANA DB. 237 | INGRES = "Ingres" #: Database vendor string for Ingres Database. 238 | FIRST_SQL = "FirstSQL" #: Database vendor string for FirstSQL. 239 | ENTERPRISE_DB = "EnterpriseDB" #: Database vendor string for EnterpriseDB. 240 | CACHE = "Cache" #: Database vendor string for InterSystems Cache. 241 | ADABAS = "Adabas" #: Database vendor string for ADABAS. 242 | FIREBIRD = "Firebird" #: Database vendor string for Firebird Database. 243 | DB2 = "DB2" #: Database vendor string for IBM Db2. 244 | 245 | #: Database vendor string for JDBC connections to Apache Derby 246 | #: (aka. IBM Cloudscape). 247 | DERBY_CLIENT = "Derby Client" 248 | 249 | #: Database vendor string for Derby Embedded. 250 | DERBY_EMBEDDED = "Derby Embedded" 251 | 252 | FILEMAKER = "Filemaker" #: Database vendor string for FileMaker Pro. 253 | INFORMIX = "Informix" #: Database vendor string for IBM Informix. 254 | INSTANT_DB = "InstantDb" #: Database vendor string for InstantDB. 255 | INTERBASE = "Interbase" #: Database vendor string for Embarcadero InterBase. 256 | MYSQL = "MySQL" #: Database vendor string for MySQL. 257 | MARIADB = "MariaDB" #: Database vendor string for MariaDB. 258 | NETEZZA = "Netezza" #: Database vendor string for IBM Netezza. 259 | ORACLE = "Oracle" #: Database vendor string for Oracle Database. 260 | PERVASIVE = "Pervasive" #: Database vendor string for Pervasive PSQL. 261 | POINTBASE = "Pointbase" #: Database vendor string for PointBase. 262 | POSTGRESQL = "PostgreSQL" #: Database vendor string for PostgreSQL. 263 | SQLSERVER = "SQL Server" #: Database vendor string for Microsoft SQL Server. 264 | SQLITE = "sqlite" #: Database vendor string for SQLite. 265 | 266 | #: Database vendor string for SAP ASE 267 | #: (aka. Sybase SQL Server, Sybase DB, Sybase ASE). 268 | SYBASE = "Sybase" 269 | 270 | TERADATA = "Teradata" #: Database vendor string for Teradata Database. 271 | VERTICA = "Vertica" #: Database vendor string for Vertica. 272 | CASSANDRA = "Cassandra" #: Database vendor string for Cassandra. 273 | H2 = "H2" #: Database vendor string for H2 Database Engine. 274 | 275 | #: Database vendor string for ColdFusion In-Memory Query 276 | #: (aka. Query of Queries). 277 | COLDFUSION_IMQ = "ColdFusion IMQ" 278 | 279 | REDSHIFT = "Amazon Redshift" #: Database vendor string for Amazon Redshift. 280 | 281 | class ChannelType(_Uninstantiable): 282 | '''Constants for communication channel types, for use as 283 | :attr:`oneagent.sdk.Channel.type_`''' 284 | OTHER = 0 #: Some other channel type or unknown channel type. 285 | 286 | #: The channel is a TCP/IP connection. 287 | #: 288 | #: The channel endpoint string should be the host name, followed by a colon, 289 | #: followed by the port number (in decimal). E.g. :code:`localhost:1234` or 290 | #: :code:`example.com:80`. 291 | TCP_IP = 1 292 | 293 | #: The channel is a connection via Unix domain sockets. 294 | #: 295 | #: The channel endpoint string should be the path of the Unix domain 296 | #: sockets. 297 | UNIX_DOMAIN_SOCKET = 2 298 | 299 | #: The channel is a named pipe. 300 | #: 301 | #: The channel endpoint string should be the pipe name. 302 | NAMED_PIPE = 3 303 | 304 | #: The channel is some in-process means of communication. 305 | IN_PROCESS = 4 306 | 307 | class SDKError(Exception): 308 | '''Exception for SDK errors (mostly during initialization, see 309 | :func:`oneagent.initialize`).''' 310 | def __init__(self, code, msg): 311 | super(SDKError, self).__init__(code, msg) 312 | 313 | #: An :class:`int` error code. Can be one of the :class:`.ErrorCode` 314 | #: constants. If not, it is a Windows error code on Windows and an errno 315 | #: number on other systems. 316 | self.code = code 317 | 318 | #: The :class:`str` error message associated with :attr:`code` 319 | #: (potentially contains more information than could be deduced from 320 | #: :attr:`code` alone). 321 | self.message = msg 322 | 323 | class SDKInitializationError(SDKError): 324 | '''Exception for initialization errors.''' 325 | def __init__(self, code, msg, agent_version='-/-'): 326 | super(SDKInitializationError, self).__init__(code, msg) 327 | 328 | #: The :class:`str` agent version associated with this error. 329 | self.agent_version = agent_version 330 | 331 | class SDKHandleBase(object): 332 | '''Base class for SDK handles that must be closed explicitly. 333 | 334 | You can use this class as a context manager (i.e. with a :code:`with`-block) 335 | to automatically close the handle.''' 336 | 337 | def __init__(self, nsdk, handle): 338 | self.handle = handle 339 | self.nsdk = nsdk 340 | if _DEBUG_LEAKS: 341 | self.alloc_at = ''.join(traceback.format_stack()) 342 | 343 | def close_handle(self, nsdk, handle): 344 | raise NotImplementedError( 345 | 'Must implement close_handle in derived class') 346 | 347 | def __del__(self): 348 | if self.handle is None: 349 | return 350 | try: 351 | warn = self.nsdk.agent_get_logging_callback() 352 | if not warn: 353 | return 354 | if _DEBUG_LEAKS: 355 | warn( 356 | 'Unclosed SDK handle ' 357 | + repr(self) 358 | + b' from ' 359 | + self.alloc_at) 360 | else: 361 | warn('Unclosed SDK handle ' + repr(self)) 362 | 363 | finally: 364 | self.close() 365 | 366 | def __str__(self): 367 | return '{}({})'.format(type(self), self.handle) 368 | 369 | def close(self): 370 | '''Closes the handle, if it is still open. 371 | 372 | Usually, you should prefer using the handle as a context manager to 373 | calling :meth:`close` manually.''' 374 | if self.handle is not None: 375 | self.close_handle(self.nsdk, self.handle) 376 | self.handle = None 377 | 378 | def __enter__(self): 379 | return self 380 | 381 | def __exit__(self, *exc_info): 382 | self.close() 383 | 384 | def __bool__(self): 385 | return bool(self.handle) 386 | 387 | __nonzero__ = __bool__ 388 | 389 | class DbInfoHandle(SDKHandleBase): 390 | '''Opaque handle to database information. See 391 | :meth:`oneagent.sdk.SDK.create_database_info`.''' 392 | def close_handle(self, nsdk, handle): 393 | nsdk.databaseinfo_delete(handle) 394 | 395 | class WebapplicationInfoHandle(SDKHandleBase): 396 | '''Opaque handle to web application information. See 397 | :meth:`oneagent.sdk.SDK.create_web_application_info`.''' 398 | def close_handle(self, nsdk, handle): 399 | nsdk.webapplicationinfo_delete(handle) 400 | 401 | class MessagingSystemInfoHandle(SDKHandleBase): 402 | '''Opaque handle for messaging system info object. See 403 | :meth:`oneagent.sdk.SDK.create_messaging_system_info`.''' 404 | def close_handle(self, nsdk, handle): 405 | nsdk.messagingsysteminfo_delete(handle) 406 | 407 | class TraceContextInfo(object): 408 | '''Provides information about a PurePath node using the TraceContext 409 | (Trace-Id, Span-Id) model as defined in https://www.w3.org/TR/trace-context. 410 | 411 | The Span-Id represents the currently active PurePath node (tracer). 412 | This Trace-Id and Span-Id information is not intended for tagging and context-propagation 413 | scenarios and primarily designed for log-enrichment use cases. 414 | 415 | Note that contrary to other info objects, no manual cleanup (delete calls or similar) 416 | are required (or possible) for this class. 417 | ''' 418 | 419 | #: All-zero (invalid) W3C trace ID. 420 | INVALID_TRACE_ID = "00000000000000000000000000000000" 421 | 422 | #: All-zero (invalid) W3C span ID. 423 | INVALID_SPAN_ID = "0000000000000000" 424 | 425 | def __init__(self, is_valid, trace_id, span_id): 426 | #: If true, the trace & span ID are both valid (i.e., non-zero). 427 | #: As an alternative to this property, you can check the truth value of the 428 | #: :class:`TraceContextInfo` instance. 429 | self.is_valid = is_valid # type: bool 430 | 431 | #: The W3C trace ID hex string (never empty or None, but might be all-zero) 432 | self.trace_id = trace_id # type: str 433 | 434 | #: The W3C span ID hex string (never empty or None, but might be all-zero) 435 | self.span_id = span_id # type: str 436 | 437 | def __bool__(self): 438 | return self.is_valid 439 | 440 | __nonzero__ = __bool__ 441 | 442 | def __str__(self): 443 | return self.trace_id + "-" + self.span_id 444 | -------------------------------------------------------------------------------- /src/oneagent/sdk/tracers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''Defines the public SDK tracer types (constructors should be considered 18 | private though). 19 | 20 | Use the factory functions from :class:`oneagent.sdk.SDK` to create tracers.''' 21 | 22 | from oneagent._impl.util import error_from_exc as _error_from_exc 23 | from oneagent._impl import six 24 | 25 | class OutgoingTaggable(object): 26 | '''Mixin base class for tracers that support having other paths linked to 27 | them. 28 | 29 | .. seealso:: Documentation on :ref:`tagging`. 30 | ''' 31 | 32 | # Abstract, for pylint 33 | handle = None 34 | nsdk = None 35 | 36 | @property 37 | def outgoing_dynatrace_string_tag(self): 38 | '''Get a ASCII string tag (as :class:`bytes` on Python 3, :code:`str` 39 | on Python 2) identifying the node of this tracer. Must be called between 40 | starting and ending the tracer (i.e., while it is started). 41 | 42 | .. warning:: This method was originally meant to return a :class:`unicode` object on 43 | Python 2 and a :class:`str` on Python 3 and was documented as such until version 1.4. 44 | However, it has always been returning bytes only. Use :code:`.decode('utf-8')` 45 | (:meth:`bytes.decode`) if you need an actual string. 46 | ''' 47 | return self.nsdk.tracer_get_outgoing_tag(self.handle, False) 48 | 49 | @property 50 | def outgoing_dynatrace_byte_tag(self): 51 | '''Get a :class:`bytes` tag identifying the node of this tracer. Must be 52 | called between starting and ending the tracer (i.e., while it is 53 | started). 54 | ''' 55 | return self.nsdk.tracer_get_outgoing_tag(self.handle, True) 56 | 57 | class Tracer(object): 58 | '''Base class for tracing of operations. 59 | 60 | Note that tracer are not only not thread-safe but thread-affine: They may 61 | only ever be used on the thread that created them. 62 | 63 | If a tracer object evaluates to :data:`False` (i.e., is falsy), tracing has 64 | been rejected for some reason (e.g., because the agent is currently or 65 | permanently inactive). You may then skip adding more information to the 66 | tracer, which might speed up your application. 67 | 68 | .. _tracer-states: 69 | 70 | The usual life-cycle of a tracer is as follows: 71 | 72 | 1. Create a tracer using the appropriate 73 | :code:`oneagent.sdk.SDK.trace_*` method. The tracer is now in the 74 | :dfn:`unstarted` state. 75 | 2. Start the tracer (via :meth:`.start` or by using the tracer as a 76 | context manager, i.e., in a :code:`with`-block). Timing starts here. 77 | The tracer is now :dfn:`started`. 78 | 3. Optionally mark the tracer as failed once (using 79 | :meth:`mark_failed_exc`, :meth:`.mark_failed` or automatically when 80 | an exception leaves the with-block for which this tracer was used as 81 | a context manager). It is still started but also marked as 82 | :dfn:`failed`. 83 | 4. End the tracer (via :meth:`.end` or automatically by using the tracer 84 | as a context manager). Timing stops here. The tracer is now 85 | :dfn:`ended` and no further operations are allowed on it. 86 | 87 | Unless specified otherwise, all operations on tracer are only allowed in the 88 | started state. 89 | 90 | You may short-circuit the life-cycle by calling :meth:`.end` already in the 91 | unstarted state (i.e., before starting the tracer). No nodes or path will be 92 | produced then. However, it is usually better to avoid creating the tracer in 93 | the first place instead of throwing it away unused that way. 94 | 95 | Tracers can be used as context-managers, i.e., in :code:`with` blocks:: 96 | 97 | with tracer: 98 | # code 99 | 100 | This will start the tracer upon entering the :code:`with`-block and end it 101 | upon leaving it. Additionally, if an exception leaves the block, 102 | :meth:`.mark_failed_exc` will be called on the tracer. 103 | ''' 104 | 105 | def __init__(self, nsdk, handle): 106 | self.nsdk = nsdk 107 | self.handle = handle 108 | 109 | def start(self): 110 | '''Start the tracer and timing. 111 | 112 | May only be called in the unstarted state. Transitions the state from 113 | unstarted to started. 114 | 115 | Prefer using the tracer as a context manager (i.e., with a 116 | :code:`with`-block) instead of manually calling this method. 117 | ''' 118 | self.nsdk.tracer_start(self.handle) 119 | 120 | def end(self): 121 | '''Ends the tracer. 122 | 123 | May be called in any state. Transitions the state to ended and releases 124 | any SDK resources owned by this tracer (this includes only internal 125 | resources, things like passed-in 126 | :class:`oneagent.common.DbInfoHandle` need to be released manually). 127 | 128 | Prefer using the tracer as a context manager (i.e., with a 129 | :code:`with`-block) instead of manually calling this method. 130 | ''' 131 | if self.handle is not None: 132 | self.nsdk.tracer_end(self.handle) 133 | self.handle = None 134 | 135 | def mark_failed(self, clsname, msg): 136 | '''Marks the tracer as failed with the given exception class name 137 | :code:`clsname` and message :code:`msg`. 138 | 139 | May only be called in the started state and only if the tracer is not 140 | already marked as failed. Note that this does not end the tracer! Once a 141 | tracer is marked as failed, attempts to do it again are forbidden. 142 | 143 | If possible, using the tracer as a context manager (i.e., with a 144 | :code:`with`-block) or :meth:`.mark_failed_exc` is more convenient than 145 | this method. 146 | 147 | :param str clsname: Fully qualified name of the exception type that 148 | caused the failure. 149 | :param str msg: Exception message that caused the failure. 150 | ''' 151 | self.nsdk.tracer_error(self.handle, clsname, msg) 152 | 153 | def mark_failed_exc(self, e_val=None, e_ty=None): 154 | '''Marks the tracer as failed with the given exception :code:`e_val` of 155 | type :code:`e_ty` (defaults to the current exception). 156 | 157 | May only be called in the started state and only if the tracer is not 158 | already marked as failed. Note that this does not end the tracer! Once a 159 | tracer is marked as failed, attempts to do it again are forbidden. 160 | 161 | If possible, using the tracer as a context manager (i.e., with a 162 | :code:`with`-block) is more convenient than this method. 163 | 164 | If :code:`e_val` and :code:`e_ty` are both none, the current exception 165 | (as retured by :func:`sys.exc_info`) is used. 166 | 167 | :param BaseException e_val: The exception object that caused the 168 | failure. If :code:`None`, the current exception value 169 | (:code:`sys.exc_info()[1]`) is used. 170 | :param type e_ty: The type of the exception that caused the failure. If 171 | :code:`None` the type of :code:`e_val` is used. If that is also 172 | :code:`None`, the current exception type (:code:`sys.exc_info()[0]`) 173 | is used. 174 | ''' 175 | _error_from_exc(self.nsdk, self.handle, e_val, e_ty) 176 | 177 | def __enter__(self): 178 | '''Starts the tracer (as if calling :meth:`.start`) and returns 179 | :code:`self`. For use with :code:`with` blocks.''' 180 | self.start() 181 | return self 182 | 183 | def __exit__(self, e_ty, e_val, e_tb): 184 | '''If any exception leaves the :code:`with`-block, marks the tracer as 185 | failed with that exception (see :meth:`mark_failed_exc`). In any case, 186 | ends the tracer (see :meth:`end`).''' 187 | try: 188 | del e_tb 189 | if e_ty is not None or e_val is not None: 190 | self.mark_failed_exc(e_val, e_ty) 191 | finally: 192 | self.end() 193 | 194 | def __del__(self): 195 | if self.handle is not None: 196 | # Intentionally don't end the tracer: Prefer resource leaks over 197 | # incorrect paths (we might be on another thread anyway). 198 | warn = self.nsdk.agent_get_logging_callback() 199 | if warn: 200 | warn('Un-ended SDK Tracer {}({})'.format( 201 | type(self), self.handle)) 202 | 203 | def __bool__(self): 204 | return bool(self.handle) 205 | 206 | __nonzero__ = __bool__ 207 | 208 | class DatabaseRequestTracer(Tracer): 209 | '''Traces a database request. See 210 | :meth:`oneagent.sdk.SDK.trace_sql_database_request`.''' 211 | 212 | def set_rows_returned(self, rows_returned): 213 | '''Sets the number of retrieved rows for this traced database request. 214 | 215 | :param int rows_returned: The number of rows returned Must not be 216 | negative. 217 | ''' 218 | self.nsdk.databaserequesttracer_set_returned_row_count( 219 | self.handle, rows_returned) 220 | 221 | def set_round_trip_count(self, round_trip_count): 222 | '''Sets the number of round trips to the database server for this traced 223 | database request. 224 | 225 | :param int round_trip_count: The number of round trips. Must not be 226 | negative. 227 | ''' 228 | self.nsdk.databaserequesttracer_set_round_trip_count( 229 | self.handle, round_trip_count) 230 | 231 | 232 | class IncomingRemoteCallTracer(Tracer): 233 | '''Traces an incoming remote call. See 234 | :meth:`oneagent.sdk.SDK.trace_incoming_remote_call`.''' 235 | 236 | class OutgoingRemoteCallTracer(Tracer, OutgoingTaggable): 237 | '''Traces an outgoing remote call. See 238 | :meth:`oneagent.sdk.SDK.trace_outgoing_remote_call`.''' 239 | 240 | def _make_add_kvs_fn(fnname): 241 | add_kv_name = fnname 242 | add_kvs_name = add_kv_name + 's' 243 | 244 | def add_kvs_fn(self, names_or_dict, values=None, count=None): 245 | add_kvs_impl = getattr(self.nsdk, add_kvs_name) 246 | if values is None: 247 | add_kvs_impl( 248 | self.handle, 249 | six.iterkeys(names_or_dict), 250 | six.itervalues(names_or_dict), 251 | len(names_or_dict)) 252 | else: 253 | add_kvs_impl( 254 | self.handle, 255 | names_or_dict, 256 | values, 257 | len(names_or_dict) if count is None else count) 258 | 259 | def add_kv_fn(self, name, value): 260 | add_kv_impl = getattr(self.nsdk, add_kv_name) 261 | add_kv_impl(self.handle, name, value) 262 | 263 | return add_kvs_fn, add_kv_fn 264 | 265 | class IncomingWebRequestTracer(Tracer): 266 | '''Traces an incoming web (HTTP) request. See 267 | :meth:`oneagent.sdk.SDK.trace_incoming_web_request`. 268 | 269 | .. warning:: Regarding HTTP header encoding issues see :ref:`http-encoding-warning`. 270 | 271 | .. method:: add_parameter(name, value) 272 | add_parameters(data) 273 | add_parameters(names, values [, count]) 274 | 275 | Adds the request (POST/form) parameter(s) with the given name(s) and 276 | value(s). 277 | 278 | :meth:`.add_parameter` adds a single parameter: 279 | 280 | :param str name: The name of the parameter. 281 | :param str value: The value of the parameter. 282 | 283 | :meth:`.add_parameters` adds multiple parameters and can be called 284 | either with a single mapping of parameter names to parameter values or 285 | with the names, corresponding values, and an optional count as separate 286 | iterables: 287 | 288 | :param data: A dictionary mapping a parameter name to a 289 | (single) parameter value. 290 | :type data: dict[str, str] 291 | :param names: An iterable (if the count is given) or collection (if not) 292 | of strings with the parameter names. 293 | :type names: ~typing.Iterable[str] or ~typing.Collection[str] 294 | :param values: An iterable (if the count is given) or collection (if 295 | not) of strings with the parameter values. 296 | :type values: ~typing.Iterable[str] or ~typing.Collection[str] 297 | :param int count: An optional integer giving the count of values in 298 | :code:`names` / :code:`values` to use. 299 | 300 | .. method:: add_response_header(name, value) 301 | add_response_headers(data) 302 | add_response_headers(names, values [, count]) 303 | 304 | Adds the HTTP response header(s) with the given name(s) and value(s). 305 | For the parameters, see :meth:`.add_parameter` and 306 | :meth:`.add_parameters`. 307 | 308 | Some headers can appear multiple times in an HTTP response. To capture 309 | all the values, either call :meth:`.add_response_header` multiple times, 310 | or use the signature with names and values as separate values and 311 | provide the name and corresponding values for each, or, if possible for 312 | that particular header, set the value to an appropriately concatenated 313 | string. 314 | ''' 315 | 316 | add_parameters, add_parameter = _make_add_kvs_fn('incomingwebrequesttracer_add_parameter') 317 | 318 | add_response_headers, add_response_header = _make_add_kvs_fn( 319 | 'incomingwebrequesttracer_add_response_header') 320 | 321 | def set_status_code(self, code): 322 | '''Sets the HTTP status code for the response to the traced incoming 323 | request. 324 | 325 | :param int code: The HTTP status code of the HTTP response that is sent 326 | back to the client (e.g., 200 or 404). 327 | ''' 328 | self.nsdk.incomingwebrequesttracer_set_status_code(self.handle, code) 329 | 330 | class OutgoingWebRequestTracer(Tracer, OutgoingTaggable): 331 | '''Traces an outgoing web (HTTP) request. See 332 | :meth:`oneagent.sdk.SDK.trace_outgoing_web_request`. 333 | 334 | .. warning:: Regarding HTTP header encoding issues see :ref:`http-encoding-warning`. 335 | 336 | .. method:: add_response_header(name, value) 337 | add_response_headers(data) 338 | add_response_headers(names, values [, count]) 339 | 340 | Adds the HTTP response header(s) with the given name(s) and value(s). 341 | For the parameters, see :meth:`.add_parameter` and 342 | :meth:`.add_parameters`. 343 | 344 | Some headers can appear multiple times in an HTTP response. To capture 345 | all the values, either call :meth:`.add_response_header` multiple times, 346 | or use the signature with names and values as separate values and 347 | provide the name and corresponding values for each, or, if possible for 348 | that particular header, set the value to an appropriately concatenated 349 | string. 350 | 351 | .. versionadded:: 1.1.0 352 | ''' 353 | 354 | add_response_headers, add_response_header = \ 355 | _make_add_kvs_fn('outgoingwebrequesttracer_add_response_header') 356 | 357 | def set_status_code(self, code): 358 | '''Sets the HTTP status code for the response of the traced outgoing 359 | request. 360 | 361 | :param int code: The HTTP status code of the HTTP response (e.g., 200 or 404). 362 | 363 | .. versionadded:: 1.1.0 364 | ''' 365 | self.nsdk.outgoingwebrequesttracer_set_status_code(self.handle, code) 366 | 367 | class InProcessLinkTracer(Tracer): 368 | '''Traces in-process asynchronous execution. 369 | 370 | See :meth:`oneagent.sdk.SDK.create_in_process_link` and 371 | :meth:`oneagent.sdk.SDK.trace_in_process_link` for more information. 372 | 373 | .. versionadded:: 1.1.0 374 | ''' 375 | 376 | class OutgoingMessageTracer(Tracer, OutgoingTaggable): 377 | '''Tracer for outgoing messages. 378 | 379 | See :meth:`oneagent.sdk.SDK.trace_outgoing_message` for more information. 380 | 381 | .. versionadded:: 1.2.0 382 | ''' 383 | 384 | def set_vendor_message_id(self, message_id): 385 | '''Sets the vendor message ID of an outgoing message. 386 | 387 | :param str message_id: The vendor message ID provided by the messaging system. 388 | 389 | .. note:: This information is often only available after the message was sent. Thus, 390 | calling this function is also supported after starting the tracer. 391 | 392 | .. versionadded:: 1.2.0 393 | ''' 394 | self.nsdk.outgoingmessagetracer_set_vendor_message_id(self.handle, message_id) 395 | 396 | def set_correlation_id(self, correlation_id): 397 | '''Sets the corrrelation ID of an outgoing message. 398 | 399 | :param str correlation_id: The correlation ID for the message, usually application-defined. 400 | 401 | .. note:: This information is often only available after the message was sent. Thus, 402 | calling this function is also supported after starting the tracer. 403 | 404 | .. versionadded:: 1.2.0 405 | ''' 406 | self.nsdk.outgoingmessagetracer_set_correlation_id(self.handle, correlation_id) 407 | 408 | class IncomingMessageReceiveTracer(Tracer): 409 | '''Tracer for receiving messages. 410 | 411 | See :meth:`oneagent.sdk.SDK.trace_incoming_message_receive` for more information. 412 | 413 | .. versionadded:: 1.2.0 414 | ''' 415 | 416 | class IncomingMessageProcessTracer(Tracer): 417 | '''Tracer for processing incoming messages. 418 | 419 | See :meth:`oneagent.sdk.SDK.trace_incoming_message_process` for more information. 420 | 421 | .. versionadded:: 1.2.0 422 | ''' 423 | def set_vendor_message_id(self, message_id): 424 | '''Sets the vendor message ID of an incoming message. 425 | 426 | :param str message_id: The message ID provided by the messaging system. 427 | 428 | .. versionadded:: 1.2.0 429 | ''' 430 | self.nsdk.incomingmessageprocesstracer_set_vendor_message_id(self.handle, message_id) 431 | 432 | def set_correlation_id(self, correlation_id): 433 | '''Sets the corrrelation ID of an incoming message. 434 | 435 | :param str correlation_id: The correlation ID for the message, usually application-defined. 436 | 437 | .. versionadded:: 1.2.0 438 | ''' 439 | self.nsdk.incomingmessageprocesstracer_set_correlation_id(self.handle, correlation_id) 440 | 441 | class CustomServiceTracer(Tracer): 442 | '''Tracer for custom services. 443 | 444 | See :meth:`oneagent.sdk.SDK.trace_custom_service` for more information. 445 | 446 | .. versionadded:: 1.2.0 447 | ''' 448 | -------------------------------------------------------------------------------- /test-util-src/sdkmockiface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2018 Dynatrace LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | '''SDK interface with mock implementation, for testing etc. Uses strict error 18 | handling, i.e. does not support broken paths.''' 19 | 20 | from __future__ import print_function 21 | 22 | import sys 23 | import warnings 24 | from functools import wraps 25 | import threading 26 | import base64 27 | import struct 28 | from itertools import chain 29 | from collections import namedtuple 30 | 31 | 32 | from oneagent._impl.six.moves import _thread, range #pylint:disable=import-error 33 | 34 | from oneagent._impl import six 35 | from oneagent.common import AgentState, AgentForkState, ErrorCode, MessageSeverity 36 | 37 | class SDKLeakWarning(RuntimeWarning): 38 | '''Warning that is emitted when a SDK resource was not properly disposed 39 | of.''' 40 | 41 | class _Handle(object): 42 | def __str__(self): 43 | return '{}@0x{:X}'.format(type(self).__name__, id(self)) 44 | 45 | def __repr__(self): 46 | return '{}{!r}@0x{:X}'.format(type(self).__name__, self.vals, id(self)) 47 | 48 | def __init__(self, *vals): 49 | self.vals = vals 50 | self.is_live = True 51 | 52 | def close(self): 53 | self.is_live = False 54 | 55 | def __del__(self): 56 | if self.is_live: 57 | warnings.warn('Leaked handle {}'.format(self), SDKLeakWarning) 58 | 59 | 60 | 61 | class DbInfoHandle(_Handle): 62 | pass 63 | 64 | class WsAppHandle(_Handle): 65 | pass 66 | 67 | class MessageSystemInfoHandle(_Handle): 68 | pass 69 | 70 | class ThreadBoundObject(object): 71 | def __init__(self): 72 | self.tid = _thread.get_ident() 73 | 74 | def check_thread(self): 75 | if self.tid != _thread.get_ident(): 76 | raise ValueError( 77 | '{} was created on T{}, but T{} attempted an access'.format( 78 | self, self.tid, _thread.get_ident())) 79 | 80 | class TracerHandle(_Handle, ThreadBoundObject): 81 | CREATED = 0 82 | STARTED = 1 83 | ENDED = 2 84 | 85 | LINK_CHILD = 0 86 | LINK_TAG = 1 87 | 88 | _TAG_STRUCT = struct.Struct('>Q') # Big/network-endian unsigned long long 89 | 90 | is_in_taggable = False 91 | has_out_tag = False 92 | is_entrypoint = False 93 | 94 | custom_attribs = [] 95 | 96 | def __init__(self, _nsdk, *vals): 97 | assert isinstance(_nsdk, SDKMockInterface) 98 | _Handle.__init__(self, *vals) 99 | ThreadBoundObject.__init__(self) 100 | self.path = None 101 | self.state = self.CREATED 102 | self.err_info = None 103 | self.children = [] #[(link_kind: int, child: TracerHandle)] 104 | self.linked_parent = None 105 | self.in_tag = None 106 | self.is_in_tag_resolved = False 107 | 108 | def close(self): 109 | self.check_thread() 110 | self.state = self.ENDED 111 | if any(lnk == self.LINK_CHILD and c.state == self.STARTED 112 | for lnk, c in self.children): 113 | raise ValueError( 114 | 'Ending tracer {} that has un-ended children: {}'.format( 115 | self, self.children)) 116 | _Handle.close(self) 117 | 118 | def all_original_children(self): 119 | '''Yields all (direct and indirect) children with LINK_CHILD.''' 120 | return chain.from_iterable( 121 | c.all_nodes_in_subtree() 122 | for lnk, c in self.children 123 | if lnk == self.LINK_CHILD) 124 | 125 | def all_nodes_in_subtree(self): 126 | '''Yields self and all (incl indirect) children with LINK_CHILD.''' 127 | return chain((self,), self.all_original_children()) 128 | 129 | @property 130 | def out_tag(self): 131 | if not self.has_out_tag: 132 | raise ValueError( 133 | '{} tracer is not OutgoingTaggable'.format(type(self))) 134 | if self.state != self.STARTED: 135 | raise ValueError('Can only obtain tag when started!') 136 | return self._TAG_STRUCT.pack(id(self)) 137 | 138 | def set_in_tag(self, tag): 139 | if not self.is_in_taggable: 140 | raise ValueError( 141 | '{} tracer is not IncomingTaggable'.format(type(self))) 142 | if self.state != self.CREATED: 143 | raise ValueError('Cannot set tags after starting.') 144 | 145 | self.in_tag = tag 146 | 147 | @property 148 | def in_tag_as_id(self): 149 | if self.in_tag is None: 150 | return None 151 | (result,) = self._TAG_STRUCT.unpack(self.in_tag) 152 | return result 153 | 154 | def dump(self, indent=''): 155 | result = '{}{}(S={}'.format(indent, str(self), self.state) 156 | intag = self.in_tag_as_id 157 | if intag is not None: 158 | result += ',I={}0x{:x}'.format( 159 | '' if self.is_in_tag_resolved else '!', intag) 160 | result += ')' 161 | valstr = ', '.join(map(repr, self.vals)) 162 | if valstr: 163 | result += '\n{} V=({})'.format(indent, valstr) 164 | for lnk, child in self.children: 165 | result += '\n{} {}\n{}'.format( 166 | indent, lnk, child.dump(indent + ' ')) 167 | return result 168 | 169 | 170 | class RemoteCallHandleBase(TracerHandle): 171 | def __init__(self, *args, **kwargs): 172 | TracerHandle.__init__(self, *args, **kwargs) 173 | self.protocol_name = None 174 | class InRemoteCallHandle(RemoteCallHandleBase): 175 | is_entrypoint = True 176 | is_in_taggable = True 177 | class OutRemoteCallHandle(RemoteCallHandleBase): 178 | has_out_tag = True 179 | class InProcessLinkTracerHandle(TracerHandle): 180 | pass 181 | 182 | class CustomServiceTracerHandle(TracerHandle): 183 | pass 184 | 185 | class OutMsgTracerHandle(TracerHandle): 186 | def __init__(self, *args, **kwargs): 187 | TracerHandle.__init__(self, *args, **kwargs) 188 | self.vendor_message_id = None 189 | self.correlation_id = None 190 | 191 | class InMsgProcessTracerHandle(TracerHandle): 192 | pass 193 | 194 | class DbRequestHandle(TracerHandle): 195 | def __init__(self, *args, **kwargs): 196 | TracerHandle.__init__(self, *args, **kwargs) 197 | self.returned_row_count = None 198 | self.round_trip_count = None 199 | 200 | class InWebReqHandle(TracerHandle): 201 | is_entrypoint = True 202 | is_in_taggable = True 203 | 204 | def __init__(self, *args, **kwargs): 205 | TracerHandle.__init__(self, *args, **kwargs) 206 | self.req_hdrs = [] 207 | self.resp_hdrs = [] 208 | self.params = [] 209 | self.resp_code = None 210 | self.remote_addr = None 211 | 212 | class OutWebReqHandle(TracerHandle): 213 | has_out_tag = True 214 | is_entrypoint = True 215 | 216 | def __init__(self, *args, **kwargs): 217 | TracerHandle.__init__(self, *args, **kwargs) 218 | self.req_hdrs = [] 219 | self.resp_hdrs = [] 220 | self.resp_code = None 221 | 222 | class Path(ThreadBoundObject): 223 | def __init__(self): 224 | ThreadBoundObject.__init__(self) 225 | self.nodestack = [] 226 | 227 | def start(self, tracer): 228 | assert tracer.tid == self.tid 229 | if tracer.state != TracerHandle.CREATED: 230 | raise ValueError( 231 | 'Tracer state {} is != CREATED'.format(tracer.state)) 232 | if self.nodestack: 233 | self.nodestack[-1].children.append( 234 | (TracerHandle.LINK_CHILD, tracer)) 235 | self.nodestack.append(tracer) 236 | tracer.state = TracerHandle.STARTED 237 | 238 | def end(self, tracer): 239 | tracer.close() 240 | if self.nodestack[-1] is not tracer: 241 | raise ValueError('Attempt to end {} while {} was active'.format( 242 | tracer, self.nodestack[-1])) 243 | else: 244 | self.nodestack.pop() 245 | 246 | def add_custom_request_attribute(self, key, value): 247 | self.nodestack[-1].custom_attribs.append((key, value)) 248 | 249 | def _typecheck(val, expected_ty): 250 | if not isinstance(val, expected_ty): 251 | raise TypeError('Expected type {} but got {}({})'.format( 252 | expected_ty, type(val), val)) 253 | if isinstance(val, ThreadBoundObject): 254 | val.check_thread() 255 | 256 | def _livecheck(val, expected_ty, state=None): 257 | _typecheck(val, expected_ty) 258 | if not val.is_live: 259 | raise ValueError('Handle already closed: {}'.format(val)) 260 | if state is not None and val.state != state: 261 | raise ValueError('Handle {} has state {}, but needs {}.'.format( 262 | val, val.state, state)) 263 | 264 | def _checkstate(state, maxstate=None, failure_r=ErrorCode.GENERIC): 265 | def checkstate_impl(func): 266 | @wraps(func) 267 | def state_checked(self, *args, **kwargs): 268 | #pylint:disable=protected-access 269 | if maxstate is not None: 270 | if not state <= self._state <= maxstate: 271 | return failure_r 272 | elif state != self._state: 273 | return failure_r 274 | return func(self, *args, **kwargs) 275 | 276 | state_checked.__wrapped__ = func 277 | return state_checked 278 | 279 | return checkstate_impl 280 | 281 | def _entry_field(func): 282 | 283 | @wraps(func) 284 | def checked(self, tracer_h, *args, **kwargs): 285 | if tracer_h.state != TracerHandle.CREATED: 286 | raise ValueError( 287 | 'Attempt to set entry field too late: ' + func.__name__) 288 | return func(self, tracer_h, *args, **kwargs) 289 | 290 | checked.__wrapped__ = func 291 | return checked 292 | 293 | 294 | def _strcheck(val, optional=False): 295 | if optional and val is None: 296 | return 297 | if not isinstance(val, (six.text_type, six.binary_type)): 298 | raise TypeError('Expected a string type but got {}({})'.format( 299 | type(val), val)) 300 | if not optional and not val.strip(): 301 | raise ValueError('Expected non-empty string, but got {!r}'.format(val)) 302 | 303 | def _mk_add_kvs_fn(adder, web_req_handle_type): 304 | 305 | def add_kvs(self, tracer_h, keys, vals, count): 306 | _livecheck(tracer_h, web_req_handle_type) 307 | _typecheck(count, int) 308 | for _, key, val in zip(range(count), keys, vals): 309 | adder(self, tracer_h, key, val) 310 | return add_kvs 311 | 312 | ProcessTech = namedtuple('ProcessTech', 'type edition version') 313 | 314 | class SDKMockInterface(object): #pylint:disable=too-many-public-methods 315 | def __init__(self): 316 | self._diag_cb = lambda text: self.stub_default_logging_function(text, 0) 317 | self._log_cb = self.stub_default_logging_function 318 | self._state = AgentState.NOT_INITIALIZED 319 | self._log_level = MessageSeverity.FINEST 320 | self._path_tls = threading.local() 321 | self.finished_paths = [] 322 | self.techs = [] 323 | self.finished_paths_lk = threading.RLock() 324 | 325 | def all_finished_nodes(self): 326 | with self.finished_paths_lk: 327 | for node in self.finished_paths: 328 | for subnode in node.all_nodes_in_subtree(): 329 | yield subnode 330 | 331 | 332 | def get_finished_node_by_id(self, id_tag): 333 | with self.finished_paths_lk: 334 | for node in self.all_finished_nodes(): 335 | if id(node) == id_tag: 336 | return node 337 | return None 338 | 339 | 340 | def process_finished_paths_tags(self): 341 | unresolved = [] 342 | with self.finished_paths_lk: 343 | for node in self.all_finished_nodes(): 344 | in_id = node.in_tag_as_id 345 | if in_id is None: 346 | continue 347 | linked = self.get_finished_node_by_id(in_id) 348 | if not linked: 349 | unresolved.append(node) 350 | continue 351 | linked.children.append((TracerHandle.LINK_TAG, node)) 352 | node.is_in_tag_resolved = True 353 | node.linked_parent = linked 354 | return unresolved 355 | 356 | def get_path(self, create=False): 357 | path = getattr(self._path_tls, 'path', None) 358 | if not path and create: 359 | path = Path() 360 | self.set_path(path) 361 | return path 362 | 363 | def set_path(self, path): 364 | self._path_tls.path = path 365 | 366 | #pylint:disable=no-self-use,unused-argument 367 | 368 | def stub_is_sdk_cmdline_arg(self, arg): 369 | return arg.startswith('--dt_') 370 | 371 | def stub_process_cmdline_arg(self, arg, replace): 372 | _strcheck(arg) 373 | _typecheck(replace, bool) 374 | if self._state != AgentState.NOT_INITIALIZED: 375 | return ErrorCode.GENERIC 376 | return ErrorCode.SUCCESS 377 | 378 | def stub_set_variable(self, assignment, replace): 379 | _strcheck(assignment) 380 | _typecheck(replace, bool) 381 | if self._state != AgentState.NOT_INITIALIZED: 382 | return ErrorCode.GENERIC 383 | return ErrorCode.SUCCESS 384 | 385 | def stub_set_logging_level(self, level): 386 | _typecheck(level, int) 387 | if level < MessageSeverity.FINEST or level > MessageSeverity.DEBUG: 388 | warnings.warn('Bad message severity level.', RuntimeWarning) 389 | 390 | def stub_default_logging_function(self, level, msg): 391 | print('[OneSDK:Mock]', level, msg, file=sys.stderr) 392 | 393 | def stub_set_logging_callback(self, sink): 394 | self._log_cb = sink 395 | 396 | 397 | @_checkstate(AgentState.NOT_INITIALIZED) 398 | def stub_free_variables(self): 399 | pass 400 | 401 | def agent_get_version_string(self): 402 | return u'0.000.0.00000000-{}'.format(type(self).__name__) 403 | 404 | def agent_found(self): 405 | return True 406 | 407 | def agent_is_compatible(self): 408 | return True 409 | 410 | @_checkstate(AgentState.NOT_INITIALIZED) 411 | def initialize(self, init_flags=0): 412 | self._state = AgentState.ACTIVE 413 | return ErrorCode.SUCCESS 414 | 415 | @_checkstate(AgentState.ACTIVE, AgentState.TEMPORARILY_INACTIVE) 416 | def shutdown(self): 417 | self._state = AgentState.NOT_INITIALIZED 418 | return ErrorCode.SUCCESS 419 | 420 | def agent_get_current_state(self): 421 | return self._state 422 | 423 | def agent_set_logging_callback(self, callback): 424 | self._diag_cb = callback 425 | 426 | def agent_set_warning_callback(self, callback): 427 | if self._state == AgentState.NOT_INITIALIZED: 428 | return ErrorCode.GENERIC 429 | self._diag_cb = callback 430 | return ErrorCode.SUCCESS 431 | 432 | def agent_set_verbose_callback(self, callback): 433 | if self._state != AgentState.NOT_INITIALIZED: 434 | return ErrorCode.GENERIC 435 | return ErrorCode.SUCCESS 436 | 437 | def agent_get_logging_callback(self): 438 | return self._diag_cb 439 | 440 | def strerror(self, error_code): 441 | if error_code == ErrorCode.SUCCESS: 442 | return u'Success.' 443 | elif error_code == ErrorCode.GENERIC: 444 | return u'Generic error.' 445 | return u'Unknown error #' + str(error_code) 446 | 447 | def agent_get_fork_state(self): 448 | return AgentForkState.ERROR 449 | 450 | def ex_agent_add_process_technology(self, tech_type, tech_edition, tech_version): 451 | _typecheck(tech_type, int) 452 | _strcheck(tech_edition) 453 | _strcheck(tech_version) 454 | self.techs.append(ProcessTech(type=tech_type, edition=tech_edition, version=tech_version)) 455 | 456 | def webapplicationinfo_create(self, vhost, appid, ctxroot): 457 | _strcheck(vhost) 458 | _strcheck(appid) 459 | _strcheck(ctxroot) 460 | return WsAppHandle(vhost, appid, ctxroot) 461 | 462 | def webapplicationinfo_delete(self, wapp_h): 463 | _typecheck(wapp_h, WsAppHandle) 464 | wapp_h.close() 465 | 466 | def incomingwebrequesttracer_create(self, wapp_h, uri, http_method): 467 | _livecheck(wapp_h, WsAppHandle) 468 | _strcheck(uri) 469 | _strcheck(http_method) 470 | return InWebReqHandle(self, wapp_h, uri, http_method) 471 | 472 | #pylint:disable=invalid-name 473 | 474 | 475 | @_entry_field 476 | def incomingwebrequesttracer_add_request_header(self, tracer_h, key, val): 477 | _livecheck(tracer_h, InWebReqHandle) 478 | _strcheck(key) 479 | _strcheck(val) 480 | tracer_h.req_hdrs.append((key, val)) 481 | 482 | incomingwebrequesttracer_add_request_headers = _mk_add_kvs_fn( 483 | incomingwebrequesttracer_add_request_header, InWebReqHandle) 484 | 485 | def incomingwebrequesttracer_add_response_header(self, tracer_h, key, val): 486 | _livecheck(tracer_h, InWebReqHandle) 487 | _strcheck(key) 488 | _strcheck(val) 489 | tracer_h.resp_hdrs.append((key, val)) 490 | 491 | incomingwebrequesttracer_add_response_headers = _mk_add_kvs_fn( 492 | incomingwebrequesttracer_add_response_header, InWebReqHandle) 493 | 494 | def incomingwebrequesttracer_add_parameter(self, tracer_h, key, val): 495 | _livecheck(tracer_h, InWebReqHandle) 496 | _strcheck(key) 497 | _strcheck(val) 498 | tracer_h.params.append((key, val)) 499 | 500 | incomingwebrequesttracer_add_parameters = _mk_add_kvs_fn( 501 | incomingwebrequesttracer_add_parameter, InWebReqHandle) 502 | 503 | @_entry_field 504 | def incomingwebrequesttracer_set_remote_address(self, tracer_h, addr): 505 | _livecheck(tracer_h, InWebReqHandle) 506 | _strcheck(addr, optional=True) 507 | tracer_h.remote_addr = addr 508 | 509 | def incomingwebrequesttracer_set_status_code(self, tracer_h, code): 510 | _livecheck(tracer_h, InWebReqHandle) 511 | _typecheck(code, int) 512 | tracer_h.resp_code = code 513 | 514 | #pylint:enable=invalid-name 515 | 516 | def outgoingwebrequesttracer_create(self, uri, http_method): 517 | _strcheck(uri) 518 | _strcheck(http_method) 519 | return OutWebReqHandle(self, uri, http_method) 520 | 521 | #pylint:disable=invalid-name 522 | 523 | @_entry_field 524 | def outgoingwebrequesttracer_add_request_header(self, tracer_h, key, val): 525 | _livecheck(tracer_h, OutWebReqHandle) 526 | _strcheck(key) 527 | _strcheck(val) 528 | tracer_h.req_hdrs.append((key, val)) 529 | 530 | outgoingwebrequesttracer_add_request_headers = _mk_add_kvs_fn( 531 | outgoingwebrequesttracer_add_request_header, OutWebReqHandle) 532 | 533 | def outgoingwebrequesttracer_add_response_header(self, tracer_h, key, val): 534 | _livecheck(tracer_h, OutWebReqHandle) 535 | _strcheck(key) 536 | _strcheck(val) 537 | tracer_h.resp_hdrs.append((key, val)) 538 | 539 | outgoingwebrequesttracer_add_response_headers = _mk_add_kvs_fn( 540 | outgoingwebrequesttracer_add_response_header, OutWebReqHandle) 541 | 542 | def outgoingwebrequesttracer_set_status_code(self, tracer_h, code): 543 | _livecheck(tracer_h, OutWebReqHandle) 544 | _typecheck(code, int) 545 | tracer_h.resp_code = code 546 | 547 | #pylint:enable=invalid-name 548 | 549 | def databaseinfo_create(self, dbname, dbvendor, chan_ty, chan_ep): 550 | _strcheck(dbname) 551 | _strcheck(dbvendor) 552 | _typecheck(chan_ty, int) 553 | _strcheck(chan_ep, optional=True) 554 | return DbInfoHandle(dbname, dbvendor, chan_ty, chan_ep) 555 | 556 | def databaseinfo_delete(self, dbh): 557 | _typecheck(dbh, DbInfoHandle) 558 | dbh.close() 559 | 560 | #pylint:disable=invalid-name 561 | 562 | def databaserequesttracer_create_sql( 563 | self, dbh, sql): 564 | _livecheck(dbh, DbInfoHandle) 565 | _strcheck(sql) 566 | return DbRequestHandle(self, dbh, sql) 567 | 568 | def databaserequesttracer_set_returned_row_count(self, tracer_h, count): 569 | _livecheck(tracer_h, DbRequestHandle) 570 | _typecheck(count, int) 571 | assert count >= 0, 'Invalid count' 572 | tracer_h.returned_row_count = count 573 | 574 | 575 | def databaserequesttracer_set_round_trip_count(self, tracer_h, count): 576 | _livecheck(tracer_h, DbRequestHandle) 577 | _typecheck(count, int) 578 | assert count >= 0, 'Invalid count' 579 | tracer_h.round_trip_count = count 580 | 581 | #pylint:enable=invalid-name 582 | 583 | def outgoingremotecalltracer_create( #pylint:disable=too-many-arguments 584 | self, svc_method, svc_name, svc_endpoint, chan_ty, chan_ep): 585 | _strcheck(svc_method) 586 | _strcheck(svc_name) 587 | _strcheck(svc_endpoint) 588 | _typecheck(chan_ty, int) 589 | _strcheck(chan_ep, optional=True) 590 | return OutRemoteCallHandle( 591 | self, svc_method, svc_name, svc_endpoint, chan_ty, chan_ep) 592 | 593 | @_entry_field 594 | def outgoingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name 595 | self, tracer_h, protocol_name): 596 | _livecheck(tracer_h, OutRemoteCallHandle, TracerHandle.CREATED) 597 | _strcheck(protocol_name, optional=True) 598 | tracer_h.protocol_name = protocol_name 599 | 600 | def incomingremotecalltracer_create( 601 | self, svc_method, svc_name, svc_endpoint): 602 | _strcheck(svc_method) 603 | _strcheck(svc_name) 604 | _strcheck(svc_endpoint) 605 | return InRemoteCallHandle(self, svc_method, svc_name, svc_endpoint) 606 | 607 | @_entry_field 608 | def incomingremotecalltracer_set_protocol_name( #pylint:disable=invalid-name 609 | self, tracer_h, protocol_name): 610 | _livecheck(tracer_h, InRemoteCallHandle, TracerHandle.CREATED) 611 | _strcheck(protocol_name, optional=True) 612 | tracer_h.protocol_name = protocol_name 613 | 614 | def tracer_start(self, tracer_h): 615 | _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) 616 | path = self.get_path(create=tracer_h.is_entrypoint) 617 | if path: 618 | path.start(tracer_h) 619 | 620 | 621 | def tracer_end(self, tracer_h): 622 | _typecheck(tracer_h, TracerHandle) 623 | path = self.get_path() 624 | if not path: 625 | assert tracer_h.state in (TracerHandle.ENDED, TracerHandle.CREATED) 626 | tracer_h.close() 627 | return 628 | path.end(tracer_h) 629 | if not path.nodestack: 630 | with self.finished_paths_lk: 631 | self.finished_paths.append(tracer_h) # tracer_h is the root node 632 | 633 | def tracer_error(self, tracer_h, error_class, error_message): 634 | _livecheck(tracer_h, TracerHandle, TracerHandle.STARTED) 635 | _strcheck(error_class, optional=True) 636 | _strcheck(error_message, optional=True) 637 | tracer_h.err_info = (error_class, error_message) 638 | 639 | def tracer_get_outgoing_tag(self, tracer_h, use_byte_tag=False): 640 | _livecheck(tracer_h, TracerHandle, TracerHandle.STARTED) 641 | if use_byte_tag: 642 | return tracer_h.out_tag 643 | return base64.b64encode(tracer_h.out_tag) 644 | 645 | @_entry_field 646 | def tracer_set_incoming_string_tag(self, tracer_h, tag): 647 | _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) 648 | if tag is None: 649 | tracer_h.set_in_tag(None) 650 | _strcheck(tag, optional=True) 651 | tracer_h.set_in_tag(base64.b64decode(tag)) 652 | 653 | @_entry_field 654 | def tracer_set_incoming_byte_tag(self, tracer_h, tag): 655 | _livecheck(tracer_h, TracerHandle, TracerHandle.CREATED) 656 | _typecheck(tag, (six.binary_type, type(None))) 657 | tracer_h.set_in_tag(tag) 658 | 659 | def customrequestattribute_add_integer(self, key, value): 660 | _strcheck(key) 661 | _typecheck(value, int) 662 | my_path = self.get_path(True) 663 | my_path.add_custom_request_attribute(key, value) 664 | 665 | def customrequestattribute_add_float(self, key, value): 666 | _strcheck(key) 667 | _typecheck(value, float) 668 | my_path = self.get_path(True) 669 | my_path.add_custom_request_attribute(key, value) 670 | 671 | def customrequestattribute_add_string(self, key, value): 672 | _strcheck(key) 673 | _strcheck(value) 674 | my_path = self.get_path(True) 675 | my_path.add_custom_request_attribute(key, value) 676 | 677 | def customrequestattribute_add_integers(self, keys, values, count): 678 | _typecheck(count, int) 679 | assert count > 0, 'Invalid count' 680 | for _, key, val in zip(range(count), keys, values): 681 | self.customrequestattribute_add_integer(self, key, val) 682 | 683 | def customrequestattribute_add_floats(self, keys, values, count): 684 | _typecheck(count, int) 685 | assert count > 0, 'Invalid count' 686 | for _, key, val in zip(range(count), keys, values): 687 | self.customrequestattribute_add_float(self, key, val) 688 | 689 | def customrequestattribute_add_strings(self, keys, values, count): 690 | _typecheck(count, int) 691 | assert count > 0, 'Invalid count' 692 | for _, key, val in zip(range(count), keys, values): 693 | self.customrequestattribute_add_string(self, key, val) 694 | 695 | def trace_in_process_link(self, link_bytes): 696 | assert link_bytes == b'inproc' 697 | return InProcessLinkTracerHandle(self) 698 | 699 | def create_in_process_link(self): 700 | return b'inproc' 701 | 702 | # Messaging API 703 | 704 | def messagingsysteminfo_create(self, vendor_name, destination_name, destination_type, 705 | channel_type, channel_endpoint): 706 | return MessageSystemInfoHandle(self) 707 | 708 | def messagingsysteminfo_delete(self, handle): 709 | _livecheck(handle, MessageSystemInfoHandle) 710 | handle.close() 711 | 712 | def outgoingmessagetracer_create(self, handle): 713 | _livecheck(handle, MessageSystemInfoHandle) 714 | return OutMsgTracerHandle(self) 715 | 716 | def outgoingmessagetracer_set_vendor_message_id(self, handle, vendor_message_id): 717 | _livecheck(handle, OutMsgTracerHandle) 718 | handle.vendor_message_id = vendor_message_id 719 | 720 | def outgoingmessagetracer_set_correlation_id(self, handle, correlation_id): 721 | _livecheck(handle, OutMsgTracerHandle) 722 | handle.correlation_id = correlation_id 723 | 724 | def incomingmessagereceivetracer_create(self, handle): 725 | _livecheck(handle, MessageSystemInfoHandle) 726 | return TracerHandle(self) 727 | 728 | def incomingmessageprocesstracer_create(self, handle): 729 | _livecheck(handle, MessageSystemInfoHandle) 730 | return InMsgProcessTracerHandle(self) 731 | 732 | def incomingmessageprocesstracer_set_vendor_message_id(self, handle, message_id): 733 | _livecheck(handle, InMsgProcessTracerHandle) 734 | 735 | def incomingmessageprocesstracer_set_correlation_id(self, handle, correlation_id): 736 | _livecheck(handle, InMsgProcessTracerHandle) 737 | 738 | # Custom Service API 739 | 740 | def customservicetracer_create(self, service_method, service_name): 741 | handle = CustomServiceTracerHandle(self) 742 | handle.service_method = service_method 743 | handle.service_name = service_name 744 | return handle 745 | 746 | def tracecontext_get_current(self): 747 | return (ErrorCode.AGENT_NOT_ACTIVE, '00000000000000000000000000000000', '0000000000000000') 748 | --------------------------------------------------------------------------------