diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a8bfc4e1..7825faa09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: fe47e98afbecfa7a10310e1e64ef1198f916ccbb + CORE_REPO_SHA: 025246b8eaa332aa089dff65f35bc868cabf5387 jobs: build: @@ -23,7 +23,7 @@ jobs: fail-fast: false # ensures the entire test matrix is run, even if one permutation fails matrix: python-version: [ py36, py37, py38, py39, pypy3 ] - package: ["instrumentation", "exporter", "sdkextension", "propagator"] + package: ["instrumentation", "distro", "exporter", "sdkextension", "propagator"] os: [ ubuntu-20.04 ] steps: - name: Checkout Contrib Repo @ SHA - ${{ github.sha }} diff --git a/docs-requirements.txt b/docs-requirements.txt index 242200a43..efee060b4 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -6,9 +6,9 @@ sphinx-autodoc-typehints # doesn't work for pkg_resources. -e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-api&subdirectory=opentelemetry-api" -e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions" --e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" -e "git+https://github.com/open-telemetry/opentelemetry-python.git#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk" -e "git+https://github.com/open-telemetry/opentelemetry-python-contrib.git#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http" +./opentelemetry-instrumentation # Required by opentelemetry-instrumentation fastapi>=0.65.2 diff --git a/eachdist.ini b/eachdist.ini index e3656febe..58c35bb5d 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -5,6 +5,7 @@ ignore= _template sortfirst= + opentelemetry-instrumentation util/opentelemetry-util-http instrumentation/opentelemetry-instrumentation-wsgi instrumentation/opentelemetry-instrumentation-dbapi diff --git a/opentelemetry-distro/MANIFEST.in b/opentelemetry-distro/MANIFEST.in new file mode 100644 index 000000000..191b7d195 --- /dev/null +++ b/opentelemetry-distro/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-distro/README.rst b/opentelemetry-distro/README.rst new file mode 100644 index 000000000..809528391 --- /dev/null +++ b/opentelemetry-distro/README.rst @@ -0,0 +1,23 @@ +OpenTelemetry Distro +==================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-distro.svg + :target: https://pypi.org/project/opentelemetry-distro/ + +Installation +------------ + +:: + + pip install opentelemetry-distro + + +This package provides entrypoints to configure OpenTelemetry. + +References +---------- + +* `OpenTelemetry Project `_ +* `Example using opentelemetry-distro `_ diff --git a/opentelemetry-distro/setup.cfg b/opentelemetry-distro/setup.cfg new file mode 100644 index 000000000..bf41c9010 --- /dev/null +++ b/opentelemetry-distro/setup.cfg @@ -0,0 +1,60 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-distro +description = OpenTelemetry Python Distro +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-distro +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Typing :: Typed + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + opentelemetry-api ~= 1.3 + opentelemetry-instrumentation == 0.25b0 + opentelemetry-sdk == 1.6.0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_distro = + distro = opentelemetry.distro:OpenTelemetryDistro +opentelemetry_configurator = + configurator = opentelemetry.distro:OpenTelemetryConfigurator + +[options.extras_require] +test = +otlp = + opentelemetry-exporter-otlp == 1.6.0 diff --git a/opentelemetry-distro/setup.py b/opentelemetry-distro/setup.py new file mode 100644 index 000000000..ddf6de91a --- /dev/null +++ b/opentelemetry-distro/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "distro", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME, encoding="utf-8") as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-distro/src/opentelemetry/distro/__init__.py b/opentelemetry-distro/src/opentelemetry/distro/__init__.py new file mode 100644 index 000000000..97e3e2fcc --- /dev/null +++ b/opentelemetry-distro/src/opentelemetry/distro/__init__.py @@ -0,0 +1,34 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER +from opentelemetry.instrumentation.distro import BaseDistro +from opentelemetry.sdk._configuration import _OTelSDKConfigurator + + +class OpenTelemetryConfigurator(_OTelSDKConfigurator): + pass + + +class OpenTelemetryDistro(BaseDistro): + """ + The OpenTelemetry provided Distro configures a default set of + configuration out of the box. + """ + + # pylint: disable=no-self-use + def _configure(self, **kwargs): + os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp_proto_grpc_span") diff --git a/opentelemetry-distro/src/opentelemetry/distro/py.typed b/opentelemetry-distro/src/opentelemetry/distro/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/opentelemetry-distro/src/opentelemetry/distro/version.py b/opentelemetry-distro/src/opentelemetry/distro/version.py new file mode 100644 index 000000000..2a05c9b36 --- /dev/null +++ b/opentelemetry-distro/src/opentelemetry/distro/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.25b0" diff --git a/opentelemetry-distro/tests/__init__.py b/opentelemetry-distro/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py new file mode 100644 index 000000000..2e42ed904 --- /dev/null +++ b/opentelemetry-distro/tests/test_distro.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +import os +from unittest import TestCase + +from pkg_resources import DistributionNotFound, require + +from opentelemetry.distro import OpenTelemetryDistro +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER + + +class TestDistribution(TestCase): + def test_package_available(self): + try: + require(["opentelemetry-distro"]) + except DistributionNotFound: + self.fail("opentelemetry-distro not installed") + + def test_default_configuration(self): + distro = OpenTelemetryDistro() + self.assertIsNone(os.environ.get(OTEL_TRACES_EXPORTER)) + distro.configure() + self.assertEqual( + "otlp_proto_grpc_span", os.environ.get(OTEL_TRACES_EXPORTER) + ) diff --git a/opentelemetry-instrumentation/LICENSE b/opentelemetry-instrumentation/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/opentelemetry-instrumentation/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/opentelemetry-instrumentation/MANIFEST.in b/opentelemetry-instrumentation/MANIFEST.in new file mode 100644 index 000000000..faee27714 --- /dev/null +++ b/opentelemetry-instrumentation/MANIFEST.in @@ -0,0 +1,8 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst +include LICENSE diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst new file mode 100644 index 000000000..cae4e3ab5 --- /dev/null +++ b/opentelemetry-instrumentation/README.rst @@ -0,0 +1,123 @@ +OpenTelemetry Instrumentation +============================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation.svg + :target: https://pypi.org/project/opentelemetry-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation + + +This package provides a couple of commands that help automatically instruments a program: + +.. note:: + You need to install a distro package to get auto instrumentation working. The ``opentelemetry-distro`` + package contains the default distro and automatically configures some of the common options for users. + For more info about ``opentelemetry-distro`` check `here `__ + :: + + pip install opentelemetry-distro[otlp] + + +opentelemetry-bootstrap +----------------------- + +:: + + opentelemetry-bootstrap --action=install|requirements + +This commands inspects the active Python site-packages and figures out which +instrumentation packages the user might want to install. By default it prints out +a list of the suggested instrumentation packages which can be added to a requirements.txt +file. It also supports installing the suggested packages when run with :code:`--action=install` +flag. + + +opentelemetry-instrument +------------------------ + +:: + + opentelemetry-instrument python program.py + +The instrument command will try to automatically detect packages used by your python program +and when possible, apply automatic tracing instrumentation on them. This means your program +will get automatic distributed tracing for free without having to make any code changes +at all. This will also configure a global tracer and tracing exporter without you having to +make any code changes. By default, the instrument command will use the OTLP exporter but +this can be overriden when needed. + +The command supports the following configuration options as CLI arguments and environment vars: + + +* ``--trace-exporter`` or ``OTEL_TRACES_EXPORTER`` + +Used to specify which trace exporter to use. Can be set to one or more of the well-known exporter +names (see below). + + - Defaults to `otlp`. + - Can be set to `none` to disable automatic tracer initialization. + +You can pass multiple values to configure multiple exporters e.g, ``zipkin,prometheus`` + +Well known trace exporter names: + + - jaeger_proto + - jaeger_thrift + - opencensus + - otlp + - otlp_proto_grpc_span + - otlp_proto_http_span + - zipkin_json + - zipkin_proto + +``otlp`` is an alias for ``otlp_proto_grpc_span``. + +* ``--id-generator`` or ``OTEL_PYTHON_ID_GENERATOR`` + +Used to specify which IDs Generator to use for the global Tracer Provider. By default, it +will use the random IDs generator. + +The code in ``program.py`` needs to use one of the packages for which there is +an OpenTelemetry integration. For a list of the available integrations please +check `here `_ + +* ``OTEL_PYTHON_DISABLED_INSTRUMENTATIONS`` + +If set by the user, opentelemetry-instrument will read this environment variable to disable specific instrumentations. +e.g OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "requests,django" + + +Examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:: + + opentelemetry-instrument --trace-exporter otlp flask run --port=3000 + +The above command will pass ``--trace-exporter otlp`` to the instrument command and ``--port=3000`` to ``flask run``. + +:: + + opentelemetry-instrument --trace-exporter zipkin_json,otlp celery -A tasks worker --loglevel=info + +The above command will configure global trace provider, attach zipkin and otlp exporters to it and then +start celery with the rest of the arguments. + +:: + + opentelemetry-instrument --ids-generator random flask run --port=3000 + +The above command will configure the global trace provider to use the Random IDs Generator, and then +pass ``--port=3000`` to ``flask run``. + +References +---------- + +* `OpenTelemetry Project `_ diff --git a/opentelemetry-instrumentation/setup.cfg b/opentelemetry-instrumentation/setup.cfg new file mode 100644 index 000000000..042f0edb1 --- /dev/null +++ b/opentelemetry-instrumentation/setup.cfg @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +[metadata] +name = opentelemetry-instrumentation +description = Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-instrumentation +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = + opentelemetry-api ~= 1.4 + wrapt >= 1.0.0, < 2.0.0 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + opentelemetry-instrument = opentelemetry.instrumentation.auto_instrumentation:run + opentelemetry-bootstrap = opentelemetry.instrumentation.bootstrap:run +opentelemetry_environment_variables = + instrumentation = opentelemetry.instrumentation.environment_variables + +[options.extras_require] +test = diff --git a/opentelemetry-instrumentation/setup.py b/opentelemetry-instrumentation/setup.py new file mode 100644 index 000000000..b5cafbe66 --- /dev/null +++ b/opentelemetry-instrumentation/setup.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "instrumentation", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME, encoding="utf-8") as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py new file mode 100644 index 000000000..214789429 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from argparse import REMAINDER, ArgumentParser +from logging import getLogger +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from re import sub +from shutil import which + +from pkg_resources import iter_entry_points + +_logger = getLogger(__file__) + + +def run() -> None: + + parser = ArgumentParser( + description=""" + opentelemetry-instrument automatically instruments a Python + program and its dependencies and then runs the program. + """, + epilog=""" + Optional arguments (except for --help) for opentelemetry-instrument + directly correspond with OpenTelemetry environment variables. The + corresponding optional argument is formed by removing the OTEL_ or + OTEL_PYTHON_ prefix from the environment variable and lower casing the + rest. For example, the optional argument --attribute_value_length_limit + corresponds with the environment variable + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. + + These optional arguments will override the current value of the + corresponding environment variable during the execution of the command. + """, + ) + + argument_otel_environment_variable = {} + + for entry_point in iter_entry_points( + "opentelemetry_environment_variables" + ): + environment_variable_module = entry_point.load() + + for attribute in dir(environment_variable_module): + + if attribute.startswith("OTEL_"): + + argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower() + + parser.add_argument( + f"--{argument}", required=False, + ) + argument_otel_environment_variable[argument] = attribute + + parser.add_argument("command", help="Your Python application.") + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=REMAINDER, + ) + + args = parser.parse_args() + + for argument, otel_environment_variable in ( + argument_otel_environment_variable + ).items(): + value = getattr(args, argument) + if value is not None: + + environ[otel_environment_variable] = value + + python_path = environ.get("PYTHONPATH") + + if not python_path: + python_path = [] + + else: + python_path = python_path.split(pathsep) + + cwd_path = getcwd() + + # This is being added to support applications that are being run from their + # own executable, like Django. + # FIXME investigate if there is another way to achieve this + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + filedir_path = dirname(abspath(__file__)) + + python_path = [path for path in python_path if path != filedir_path] + + python_path.insert(0, filedir_path) + + environ["PYTHONPATH"] = pathsep.join(python_path) + + executable = which(args.command) + execl(executable, executable, *args.command_args) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py new file mode 100644 index 000000000..f7a6412ff --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -0,0 +1,141 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from logging import getLogger +from os import environ, path +from os.path import abspath, dirname, pathsep +from re import sub + +from pkg_resources import iter_entry_points + +from opentelemetry.instrumentation.dependencies import ( + get_dist_dependency_conflicts, +) +from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro +from opentelemetry.instrumentation.environment_variables import ( + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, +) + +logger = getLogger(__file__) + + +def _load_distros() -> BaseDistro: + for entry_point in iter_entry_points("opentelemetry_distro"): + try: + distro = entry_point.load()() + if not isinstance(distro, BaseDistro): + logger.debug( + "%s is not an OpenTelemetry Distro. Skipping", + entry_point.name, + ) + continue + logger.debug( + "Distribution %s will be configured", entry_point.name + ) + return distro + except Exception as exc: # pylint: disable=broad-except + logger.exception( + "Distribution %s configuration failed", entry_point.name + ) + raise exc + return DefaultDistro() + + +def _load_instrumentors(distro): + package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) + if isinstance(package_to_exclude, str): + package_to_exclude = package_to_exclude.split(",") + # to handle users entering "requests , flask" or "requests, flask" with spaces + package_to_exclude = [x.strip() for x in package_to_exclude] + + for entry_point in iter_entry_points("opentelemetry_pre_instrument"): + entry_point.load()() + + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + if entry_point.name in package_to_exclude: + logger.debug( + "Instrumentation skipped for library %s", entry_point.name + ) + continue + + try: + conflict = get_dist_dependency_conflicts(entry_point.dist) + if conflict: + logger.debug( + "Skipping instrumentation %s: %s", + entry_point.name, + conflict, + ) + continue + + # tell instrumentation to not run dep checks again as we already did it above + distro.load_instrumentor(entry_point, skip_dep_check=True) + logger.debug("Instrumented %s", entry_point.name) + except Exception as exc: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + raise exc + + for entry_point in iter_entry_points("opentelemetry_post_instrument"): + entry_point.load()() + + +def _load_configurators(): + configured = None + for entry_point in iter_entry_points("opentelemetry_configurator"): + if configured is not None: + logger.warning( + "Configuration of %s not loaded, %s already loaded", + entry_point.name, + configured, + ) + continue + try: + entry_point.load()().configure() # type: ignore + configured = entry_point.name + except Exception as exc: # pylint: disable=broad-except + logger.exception("Configuration of %s failed", entry_point.name) + raise exc + + +def initialize(): + try: + distro = _load_distros() + distro.configure() + _load_configurators() + _load_instrumentors(distro) + except Exception: # pylint: disable=broad-except + logger.exception("Failed to auto initialize opentelemetry") + finally: + environ["PYTHONPATH"] = sub( + fr"{dirname(abspath(__file__))}{pathsep}?", + "", + environ["PYTHONPATH"], + ) + + +if ( + hasattr(sys, "argv") + and sys.argv[0].split(path.sep)[-1] == "celery" + and "worker" in sys.argv[1:] +): + from celery.signals import worker_process_init # pylint:disable=E0401 + + @worker_process_init.connect(weak=False) + def init_celery(*args, **kwargs): + initialize() + + +else: + initialize() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py new file mode 100644 index 000000000..f1c8181ba --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import logging +import subprocess +import sys + +import pkg_resources + +from opentelemetry.instrumentation.bootstrap_gen import ( + default_instrumentations, + libraries, +) + +logger = logging.getLogger(__file__) + + +def _syscall(func): + def wrapper(package=None): + try: + if package: + return func(package) + return func() + except subprocess.SubprocessError as exp: + cmd = getattr(exp, "cmd", None) + if cmd: + msg = f'Error calling system command "{" ".join(cmd)}"' + if package: + msg = f'{msg} for package "{package}"' + raise RuntimeError(msg) + + return wrapper + + +@_syscall +def _sys_pip_install(package): + # explicit upgrade strategy to override potential pip config + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-U", + "--upgrade-strategy", + "only-if-needed", + package, + ] + ) + + +def _pip_check(): + """Ensures none of the instrumentations have dependency conflicts. + Clean check reported as: + 'No broken requirements found.' + Dependency conflicts are reported as: + 'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.' + To not be too restrictive, we'll only check for relevant packages. + """ + with subprocess.Popen( + [sys.executable, "-m", "pip", "check"], stdout=subprocess.PIPE + ) as check_pipe: + pip_check = check_pipe.communicate()[0].decode() + pip_check_lower = pip_check.lower() + for package_tup in libraries.values(): + for package in package_tup: + if package.lower() in pip_check_lower: + raise RuntimeError(f"Dependency conflict found: {pip_check}") + + +def _is_installed(req): + if req in sys.modules: + return True + + try: + pkg_resources.get_distribution(req) + except pkg_resources.DistributionNotFound: + return False + except pkg_resources.VersionConflict as exc: + logger.warning( + "instrumentation for package %s is available but version %s is installed. Skipping.", + exc.req, + exc.dist.as_requirement(), # pylint: disable=no-member + ) + return False + return True + + +def _find_installed_libraries(): + libs = default_instrumentations[:] + libs.extend( + [ + v["instrumentation"] + for _, v in libraries.items() + if _is_installed(v["library"]) + ] + ) + return libs + + +def _run_requirements(): + logger.setLevel(logging.ERROR) + print("\n".join(_find_installed_libraries()), end="") + + +def _run_install(): + for lib in _find_installed_libraries(): + _sys_pip_install(lib) + _pip_check() + + +def run() -> None: + action_install = "install" + action_requirements = "requirements" + + parser = argparse.ArgumentParser( + description=""" + opentelemetry-bootstrap detects installed libraries and automatically + installs the relevant instrumentation packages for them. + """ + ) + parser.add_argument( + "-a", + "--action", + choices=[action_install, action_requirements], + default=action_requirements, + help=""" + install - uses pip to install the new requirements using to the + currently active site-package. + requirements - prints out the new requirements to stdout. Action can + be piped and appended to a requirements.txt file. + """, + ) + args = parser.parse_args() + + cmd = { + action_install: _run_install, + action_requirements: _run_requirements, + }[args.action] + cmd() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py new file mode 100644 index 000000000..282d4491c --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -0,0 +1,142 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES. +# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE. + +libraries = { + "aiohttp": { + "library": "aiohttp ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.25b0", + }, + "aiopg": { + "library": "aiopg >= 0.13.0, < 1.3.0", + "instrumentation": "opentelemetry-instrumentation-aiopg==0.25b0", + }, + "asgiref": { + "library": "asgiref ~= 3.0", + "instrumentation": "opentelemetry-instrumentation-asgi==0.25b0", + }, + "asyncpg": { + "library": "asyncpg >= 0.12.0", + "instrumentation": "opentelemetry-instrumentation-asyncpg==0.25b0", + }, + "boto": { + "library": "boto~=2.0", + "instrumentation": "opentelemetry-instrumentation-boto==0.25b0", + }, + "botocore": { + "library": "botocore ~= 1.0", + "instrumentation": "opentelemetry-instrumentation-botocore==0.25b0", + }, + "celery": { + "library": "celery >= 4.0, < 6.0", + "instrumentation": "opentelemetry-instrumentation-celery==0.25b0", + }, + "django": { + "library": "django >= 1.10", + "instrumentation": "opentelemetry-instrumentation-django==0.25b0", + }, + "elasticsearch": { + "library": "elasticsearch >= 2.0", + "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.25b0", + }, + "falcon": { + "library": "falcon >= 2.0.0, < 4.0.0", + "instrumentation": "opentelemetry-instrumentation-falcon==0.25b0", + }, + "fastapi": { + "library": "fastapi ~= 0.58", + "instrumentation": "opentelemetry-instrumentation-fastapi==0.25b0", + }, + "flask": { + "library": "flask >= 1.0, < 3.0", + "instrumentation": "opentelemetry-instrumentation-flask==0.25b0", + }, + "grpcio": { + "library": "grpcio ~= 1.27", + "instrumentation": "opentelemetry-instrumentation-grpc==0.25b0", + }, + "httpx": { + "library": "httpx >= 0.18.0, < 0.19.0", + "instrumentation": "opentelemetry-instrumentation-httpx==0.25b0", + }, + "jinja2": { + "library": "jinja2 >= 2.7, < 4.0", + "instrumentation": "opentelemetry-instrumentation-jinja2==0.25b0", + }, + "mysql-connector-python": { + "library": "mysql-connector-python ~= 8.0", + "instrumentation": "opentelemetry-instrumentation-mysql==0.25b0", + }, + "pika": { + "library": "pika >= 1.1.0", + "instrumentation": "opentelemetry-instrumentation-pika==0.25b0", + }, + "psycopg2": { + "library": "psycopg2 >= 2.7.3.1", + "instrumentation": "opentelemetry-instrumentation-psycopg2==0.25b0", + }, + "pymemcache": { + "library": "pymemcache ~= 1.3", + "instrumentation": "opentelemetry-instrumentation-pymemcache==0.25b0", + }, + "pymongo": { + "library": "pymongo ~= 3.1", + "instrumentation": "opentelemetry-instrumentation-pymongo==0.25b0", + }, + "PyMySQL": { + "library": "PyMySQL ~= 0.10.1", + "instrumentation": "opentelemetry-instrumentation-pymysql==0.25b0", + }, + "pyramid": { + "library": "pyramid >= 1.7", + "instrumentation": "opentelemetry-instrumentation-pyramid==0.25b0", + }, + "redis": { + "library": "redis >= 2.6", + "instrumentation": "opentelemetry-instrumentation-redis==0.25b0", + }, + "requests": { + "library": "requests ~= 2.0", + "instrumentation": "opentelemetry-instrumentation-requests==0.25b0", + }, + "scikit-learn": { + "library": "scikit-learn ~= 0.24.0", + "instrumentation": "opentelemetry-instrumentation-sklearn==0.25b0", + }, + "sqlalchemy": { + "library": "sqlalchemy", + "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.25b0", + }, + "starlette": { + "library": "starlette ~= 0.13.0", + "instrumentation": "opentelemetry-instrumentation-starlette==0.25b0", + }, + "tornado": { + "library": "tornado >= 6.0", + "instrumentation": "opentelemetry-instrumentation-tornado==0.25b0", + }, + "urllib3": { + "library": "urllib3 >= 1.0.0, < 2.0.0", + "instrumentation": "opentelemetry-instrumentation-urllib3==0.25b0", + }, +} +default_instrumentations = [ + "opentelemetry-instrumentation-dbapi==0.25b0", + "opentelemetry-instrumentation-logging==0.25b0", + "opentelemetry-instrumentation-sqlite3==0.25b0", + "opentelemetry-instrumentation-urllib==0.25b0", + "opentelemetry-instrumentation-wsgi==0.25b0", +] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py new file mode 100644 index 000000000..6c65d6677 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py @@ -0,0 +1,62 @@ +from logging import getLogger +from typing import Collection, Optional + +from pkg_resources import ( + Distribution, + DistributionNotFound, + RequirementParseError, + VersionConflict, + get_distribution, +) + +logger = getLogger(__file__) + + +class DependencyConflict: + required: str = None + found: Optional[str] = None + + def __init__(self, required, found=None): + self.required = required + self.found = found + + def __str__(self): + return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"' + + +def get_dist_dependency_conflicts( + dist: Distribution, +) -> Optional[DependencyConflict]: + main_deps = dist.requires() + instrumentation_deps = [] + for dep in dist.requires(("instruments",)): + if dep not in main_deps: + # we set marker to none so string representation of the dependency looks like + # requests ~= 1.0 + # instead of + # requests ~= 1.0; extra = "instruments" + # which does not work with `get_distribution()` + dep.marker = None + instrumentation_deps.append(str(dep)) + + return get_dependency_conflicts(instrumentation_deps) + + +def get_dependency_conflicts( + deps: Collection[str], +) -> Optional[DependencyConflict]: + for dep in deps: + try: + get_distribution(dep) + except VersionConflict as exc: + return DependencyConflict(dep, exc.dist) + except DistributionNotFound: + return DependencyConflict(dep) + except RequirementParseError as exc: + logger.warning( + 'error parsing dependency, reporting as a conflict: "%s" - %s', + dep, + exc, + ) + return DependencyConflict(dep) + return None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py new file mode 100644 index 000000000..cc1c99c1e --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -0,0 +1,71 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Distribution (Distro) +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + +_LOG = getLogger(__name__) + + +class BaseDistro(ABC): + """An ABC for distro""" + + _instance = None + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @abstractmethod + def _configure(self, **kwargs): + """Configure the distribution""" + + def configure(self, **kwargs): + """Configure the distribution""" + self._configure(**kwargs) + + def load_instrumentor( # pylint: disable=no-self-use + self, entry_point: EntryPoint, **kwargs + ): + """Takes a collection of instrumentation entry points + and activates them by instantiating and calling instrument() + on each one. + + Distros can override this method to customize the behavior by + inspecting each entry point and configuring them in special ways, + passing additional arguments, load a replacement/fork instead, + skip loading entirely, etc. + """ + instrumentor: BaseInstrumentor = entry_point.load() + instrumentor().instrument(**kwargs) + + +class DefaultDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +__all__ = ["BaseDistro", "DefaultDistro"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py new file mode 100644 index 000000000..ad28f0685 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/environment_variables.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PYTHON_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS" +""" +.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS +""" diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py new file mode 100644 index 000000000..74ebe8674 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -0,0 +1,132 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +""" +OpenTelemetry Base Instrumentor +""" + +from abc import ABC, abstractmethod +from logging import getLogger +from typing import Collection, Optional + +from opentelemetry.instrumentation.dependencies import ( + DependencyConflict, + get_dependency_conflicts, +) + +_LOG = getLogger(__name__) + + +class BaseInstrumentor(ABC): + """An ABC for instrumentors + + Child classes of this ABC should instrument specific third + party libraries or frameworks either by using the + ``opentelemetry-instrument`` command or by calling their methods + directly. + + Since every third party library or framework is different and has different + instrumentation needs, more methods can be added to the child classes as + needed to provide practical instrumentation to the end user. + """ + + _instance = None + _is_instrumented_by_opentelemetry = False + + def __new__(cls, *args, **kwargs): + + if cls._instance is None: + cls._instance = object.__new__(cls, *args, **kwargs) + + return cls._instance + + @property + def is_instrumented_by_opentelemetry(self): + return self._is_instrumented_by_opentelemetry + + @abstractmethod + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of python packages with versions that the will be instrumented. + + The format should be the same as used in requirements.txt or setup.py. + + For example, if an instrumentation instruments requests 1.x, this method should look + like: + + def instrumentation_dependencies(self) -> Collection[str]: + return ['requests ~= 1.0'] + + This will ensure that the instrumentation will only be used when the specified library + is present in the environment. + """ + + def _instrument(self, **kwargs): + """Instrument the library""" + + @abstractmethod + def _uninstrument(self, **kwargs): + """Uninstrument the library""" + + def _check_dependency_conflicts(self) -> Optional[DependencyConflict]: + dependencies = self.instrumentation_dependencies() + return get_dependency_conflicts(dependencies) + + def instrument(self, **kwargs): + """Instrument the library + + This method will be called without any optional arguments by the + ``opentelemetry-instrument`` command. + + This means that calling this method directly without passing any + optional values should do the very same thing that the + ``opentelemetry-instrument`` command does. + """ + + if self._is_instrumented_by_opentelemetry: + _LOG.warning("Attempting to instrument while already instrumented") + return None + + # check if instrumentor has any missing or conflicting dependencies + skip_dep_check = kwargs.pop("skip_dep_check", False) + if not skip_dep_check: + conflict = self._check_dependency_conflicts() + if conflict: + _LOG.error(conflict) + return None + + result = self._instrument( # pylint: disable=assignment-from-no-return + **kwargs + ) + self._is_instrumented_by_opentelemetry = True + return result + + def uninstrument(self, **kwargs): + """Uninstrument the library + + See ``BaseInstrumentor.instrument`` for more information regarding the + usage of ``kwargs``. + """ + + if self._is_instrumented_by_opentelemetry: + result = self._uninstrument(**kwargs) + self._is_instrumented_by_opentelemetry = False + return result + + _LOG.warning("Attempting to uninstrument while already uninstrumented") + + return None + + +__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py new file mode 100644 index 000000000..168a7f788 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -0,0 +1,122 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements experimental propagators to inject trace context +into response carriers. This is useful for server side frameworks that start traces +when server requests and want to share the trace context with the client so the +client can add its spans to the same trace. + +This is part of an upcoming W3C spec and will eventually make it to the Otel spec. + +https://w3c.github.io/trace-context/#trace-context-http-response-headers-format +""" + +import typing +from abc import ABC, abstractmethod + +from opentelemetry import trace +from opentelemetry.context.context import Context +from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id + +_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +_RESPONSE_PROPAGATOR = None + + +def get_global_response_propagator(): + return _RESPONSE_PROPAGATOR + + +def set_global_response_propagator(propagator): + global _RESPONSE_PROPAGATOR # pylint:disable=global-statement + _RESPONSE_PROPAGATOR = propagator + + +class Setter(ABC): + @abstractmethod + def set(self, carrier, key, value): + """Inject the provided key value pair in carrier.""" + + +class DictHeaderSetter(Setter): + def set(self, carrier, key, value): # pylint: disable=no-self-use + old_value = carrier.get(key, "") + if old_value: + value = f"{old_value}, {value}" + carrier[key] = value + + +class FuncSetter(Setter): + """FuncSetter coverts a function into a valid Setter. Any function that can + set values in a carrier can be converted into a Setter by using FuncSetter. + This is useful when injecting trace context into non-dict objects such + HTTP Response objects for different framework. + + For example, it can be used to create a setter for Falcon response object as: + + setter = FuncSetter(falcon.api.Response.append_header) + + and then used with the propagator as: + + propagator.inject(falcon_response, setter=setter) + + This would essentially make the propagator call `falcon_response.append_header(key, value)` + """ + + def __init__(self, func): + self._func = func + + def set(self, carrier, key, value): + self._func(carrier, key, value) + + +default_setter = DictHeaderSetter() + + +class ResponsePropagator(ABC): + @abstractmethod + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + + +class TraceResponsePropagator(ResponsePropagator): + """Experimental propagator that injects tracecontext into HTTP responses.""" + + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + span = trace.get_current_span(context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + header_name = "traceresponse" + setter.set( + carrier, + header_name, + f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}", + ) + setter.set( + carrier, _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, header_name, + ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py new file mode 100644 index 000000000..5f63b4af5 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -0,0 +1,64 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Sequence + +from wrapt import ObjectProxy + +# pylint: disable=unused-import +from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY # noqa: F401 +from opentelemetry.trace import StatusCode + + +def extract_attributes_from_object( + obj: any, attributes: Sequence[str], existing: Dict[str, str] = None +) -> Dict[str, str]: + extracted = {} + if existing: + extracted.update(existing) + for attr in attributes: + value = getattr(obj, attr, None) + if value is not None: + extracted[attr] = str(value) + return extracted + + +def http_status_to_status_code( + status: int, allow_redirect: bool = True +) -> StatusCode: + """Converts an HTTP status code to an OpenTelemetry canonical status code + + Args: + status (int): HTTP status code + """ + # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status + if status < 100: + return StatusCode.ERROR + if status <= 299: + return StatusCode.UNSET + if status <= 399 and allow_redirect: + return StatusCode.UNSET + return StatusCode.ERROR + + +def unwrap(obj, attr: str): + """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it + + Args: + obj: Object that holds a reference to the wrapped function + attr (str): Name of the wrapped function + """ + func = getattr(obj, attr, None) + if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"): + setattr(obj, attr, func.__wrapped__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py new file mode 100644 index 000000000..2a05c9b36 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/version.py @@ -0,0 +1,15 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.25b0" diff --git a/opentelemetry-instrumentation/tests/__init__.py b/opentelemetry-instrumentation/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opentelemetry-instrumentation/tests/test_bootstrap.py b/opentelemetry-instrumentation/tests/test_bootstrap.py new file mode 100644 index 000000000..d1052de28 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_bootstrap.py @@ -0,0 +1,86 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from io import StringIO +from random import sample +from unittest import TestCase +from unittest.mock import call, patch + +from opentelemetry.instrumentation import bootstrap +from opentelemetry.instrumentation.bootstrap_gen import libraries + + +def sample_packages(packages, rate): + return sample(list(packages), int(len(packages) * rate),) + + +class TestBootstrap(TestCase): + + installed_libraries = {} + installed_instrumentations = {} + + @classmethod + def setUpClass(cls): + cls.installed_libraries = sample_packages( + [lib["instrumentation"] for lib in libraries.values()], 0.6 + ) + + # treat 50% of sampled packages as pre-installed + cls.installed_instrumentations = sample_packages( + cls.installed_libraries, 0.5 + ) + + cls.pkg_patcher = patch( + "opentelemetry.instrumentation.bootstrap._find_installed_libraries", + return_value=cls.installed_libraries, + ) + + cls.pip_install_patcher = patch( + "opentelemetry.instrumentation.bootstrap._sys_pip_install", + ) + cls.pip_check_patcher = patch( + "opentelemetry.instrumentation.bootstrap._pip_check", + ) + + cls.pkg_patcher.start() + cls.mock_pip_install = cls.pip_install_patcher.start() + cls.mock_pip_check = cls.pip_check_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.pip_check_patcher.start() + cls.pip_install_patcher.start() + cls.pkg_patcher.stop() + + @patch("sys.argv", ["bootstrap", "-a", "pipenv"]) + def test_run_unknown_cmd(self): + with self.assertRaises(SystemExit): + bootstrap.run() + + @patch("sys.argv", ["bootstrap", "-a", "requirements"]) + def test_run_cmd_print(self): + with patch("sys.stdout", new=StringIO()) as fake_out: + bootstrap.run() + self.assertEqual( + fake_out.getvalue(), "\n".join(self.installed_libraries), + ) + + @patch("sys.argv", ["bootstrap", "-a", "install"]) + def test_run_cmd_install(self): + bootstrap.run() + self.mock_pip_install.assert_has_calls( + [call(i) for i in self.installed_libraries], any_order=True, + ) + self.assertEqual(self.mock_pip_check.call_count, 1) diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py new file mode 100644 index 000000000..a8acac62f --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_dependencies.py @@ -0,0 +1,77 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +import pkg_resources +import pytest + +from opentelemetry.instrumentation.dependencies import ( + DependencyConflict, + get_dependency_conflicts, + get_dist_dependency_conflicts, +) +from opentelemetry.test.test_base import TestBase + + +class TestDependencyConflicts(TestBase): + def setUp(self): + pass + + def test_get_dependency_conflicts_empty(self): + self.assertIsNone(get_dependency_conflicts([])) + + def test_get_dependency_conflicts_no_conflict(self): + self.assertIsNone(get_dependency_conflicts(["pytest"])) + + def test_get_dependency_conflicts_not_installed(self): + conflict = get_dependency_conflicts(["this-package-does-not-exist"]) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "this-package-does-not-exist" but found: "None"', + ) + + def test_get_dependency_conflicts_mismatched_version(self): + conflict = get_dependency_conflicts(["pytest == 5000"]) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {pytest.__version__}"', + ) + + def test_get_dist_dependency_conflicts(self): + def mock_requires(extras=()): + if "instruments" in extras: + return [ + pkg_resources.Requirement( + 'test-pkg ~= 1.0; extra == "instruments"' + ) + ] + return [] + + dist = pkg_resources.Distribution( + project_name="test-instrumentation", version="1.0" + ) + dist.requires = mock_requires + + conflict = get_dist_dependency_conflicts(dist) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "test-pkg~=1.0" but found: "None"', + ) diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py new file mode 100644 index 000000000..399b3f8a6 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_distro.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from unittest import TestCase + +from pkg_resources import EntryPoint + +from opentelemetry.instrumentation.distro import BaseDistro +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class MockInstrumetor(BaseInstrumentor): + def instrumentation_dependencies(self): + return [] + + def _instrument(self, **kwargs): + pass + + def _uninstrument(self, **kwargs): + pass + + +class MockEntryPoint(EntryPoint): + def __init__(self, obj): # pylint: disable=super-init-not-called + self._obj = obj + + def load(self, *args, **kwargs): # pylint: disable=signature-differs + return self._obj + + +class MockDistro(BaseDistro): + def _configure(self, **kwargs): + pass + + +class TestDistro(TestCase): + def test_load_instrumentor(self): + # pylint: disable=protected-access + distro = MockDistro() + + instrumentor = MockInstrumetor() + entry_point = MockEntryPoint(MockInstrumetor) + + self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) + distro.load_instrumentor(entry_point) + self.assertTrue(instrumentor._is_instrumented_by_opentelemetry) diff --git a/opentelemetry-instrumentation/tests/test_instrumentor.py b/opentelemetry-instrumentation/tests/test_instrumentor.py new file mode 100644 index 000000000..dee32c34e --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_instrumentor.py @@ -0,0 +1,50 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from logging import WARNING +from unittest import TestCase + +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor + + +class TestInstrumentor(TestCase): + class Instrumentor(BaseInstrumentor): + def _instrument(self, **kwargs): + return "instrumented" + + def _uninstrument(self, **kwargs): + return "uninstrumented" + + def instrumentation_dependencies(self): + return [] + + def test_protect(self): + instrumentor = self.Instrumentor() + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + self.assertEqual(instrumentor.instrument(), "instrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.instrument(), None) + + self.assertEqual(instrumentor.uninstrument(), "uninstrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + def test_singleton(self): + self.assertIs(self.Instrumentor(), self.Instrumentor()) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py new file mode 100644 index 000000000..62461aafa --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_propagators.py @@ -0,0 +1,80 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +from opentelemetry import trace +from opentelemetry.instrumentation import propagators +from opentelemetry.instrumentation.propagators import ( + DictHeaderSetter, + TraceResponsePropagator, + get_global_response_propagator, + set_global_response_propagator, +) +from opentelemetry.test.test_base import TestBase + + +class TestGlobals(TestBase): + def test_get_set(self): + original = propagators._RESPONSE_PROPAGATOR + + propagators._RESPONSE_PROPAGATOR = None + self.assertIsNone(get_global_response_propagator()) + + prop = TraceResponsePropagator() + set_global_response_propagator(prop) + self.assertIs(prop, get_global_response_propagator()) + + propagators._RESPONSE_PROPAGATOR = original + + +class TestDictHeaderSetter(TestBase): + def test_simple(self): + setter = DictHeaderSetter() + carrier = {} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "vv") + + def test_append(self): + setter = DictHeaderSetter() + carrier = {"kk": "old"} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "old, vv") + + +class TestTraceResponsePropagator(TestBase): + def test_inject(self): + span = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ), + ) + + ctx = trace.set_span_in_context(span) + prop = TraceResponsePropagator() + carrier = {} + prop.inject(carrier, ctx) + self.assertEqual( + carrier["Access-Control-Expose-Headers"], "traceresponse" + ) + self.assertEqual( + carrier["traceresponse"], + "00-00000000000000000000000000000001-0000000000000002-00", + ) diff --git a/opentelemetry-instrumentation/tests/test_run.py b/opentelemetry-instrumentation/tests/test_run.py new file mode 100644 index 000000000..9fd3a2171 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_run.py @@ -0,0 +1,118 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# type: ignore + +from os import environ, getcwd +from os.path import abspath, dirname, pathsep +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER +from opentelemetry.instrumentation import auto_instrumentation + + +class TestRun(TestCase): + auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) + + @classmethod + def setUpClass(cls): + cls.execl_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.execl" + ) + cls.which_patcher = patch( + "opentelemetry.instrumentation.auto_instrumentation.which" + ) + + cls.execl_patcher.start() + cls.which_patcher.start() + + @classmethod + def tearDownClass(cls): + cls.execl_patcher.stop() + cls.which_patcher.stop() + + @patch("sys.argv", ["instrument", ""]) + @patch.dict("os.environ", {"PYTHONPATH": ""}) + def test_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd()]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict("os.environ", {"PYTHONPATH": "abc"}) + def test_non_empty(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict( + "os.environ", + {"PYTHONPATH": pathsep.join(["abc", auto_instrumentation_path])}, + ) + def test_after_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + @patch("sys.argv", ["instrument", ""]) + @patch.dict( + "os.environ", + { + "PYTHONPATH": pathsep.join( + [auto_instrumentation_path, "abc", auto_instrumentation_path] + ) + }, + ) + def test_single_path(self): + auto_instrumentation.run() + self.assertEqual( + environ["PYTHONPATH"], + pathsep.join([self.auto_instrumentation_path, getcwd(), "abc"]), + ) + + +class TestExecl(TestCase): + @patch("sys.argv", ["1", "2", "3"]) + @patch("opentelemetry.instrumentation.auto_instrumentation.which") + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_execl( + self, mock_execl, mock_which + ): # pylint: disable=no-self-use + mock_which.configure_mock(**{"return_value": "python"}) + + auto_instrumentation.run() + + mock_execl.assert_called_with("python", "python", "3") + + +class TestArgs(TestCase): + @patch("opentelemetry.instrumentation.auto_instrumentation.execl") + def test_exporter(self, _): # pylint: disable=no-self-use + with patch("sys.argv", ["instrument", "2"]): + auto_instrumentation.run() + self.assertIsNone(environ.get(OTEL_TRACES_EXPORTER)) + + with patch( + "sys.argv", + ["instrument", "--traces_exporter", "jaeger", "1", "2"], + ): + auto_instrumentation.run() + self.assertEqual(environ.get(OTEL_TRACES_EXPORTER), "jaeger") diff --git a/opentelemetry-instrumentation/tests/test_utils.py b/opentelemetry-instrumentation/tests/test_utils.py new file mode 100644 index 000000000..273c6f085 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_utils.py @@ -0,0 +1,45 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from http import HTTPStatus + +from opentelemetry.instrumentation.utils import http_status_to_status_code +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import StatusCode + + +class TestUtils(TestBase): + # See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status + def test_http_status_to_status_code(self): + for status_code, expected in ( + (HTTPStatus.OK, StatusCode.UNSET), + (HTTPStatus.ACCEPTED, StatusCode.UNSET), + (HTTPStatus.IM_USED, StatusCode.UNSET), + (HTTPStatus.MULTIPLE_CHOICES, StatusCode.UNSET), + (HTTPStatus.BAD_REQUEST, StatusCode.ERROR), + (HTTPStatus.UNAUTHORIZED, StatusCode.ERROR), + (HTTPStatus.FORBIDDEN, StatusCode.ERROR), + (HTTPStatus.NOT_FOUND, StatusCode.ERROR), + (HTTPStatus.UNPROCESSABLE_ENTITY, StatusCode.ERROR,), + (HTTPStatus.TOO_MANY_REQUESTS, StatusCode.ERROR,), + (HTTPStatus.NOT_IMPLEMENTED, StatusCode.ERROR), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCode.ERROR), + (HTTPStatus.GATEWAY_TIMEOUT, StatusCode.ERROR,), + (HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, StatusCode.ERROR,), + (600, StatusCode.ERROR), + (99, StatusCode.ERROR), + ): + with self.subTest(status_code=status_code): + actual = http_status_to_status_code(int(status_code)) + self.assertEqual(actual, expected, status_code) diff --git a/scripts/build.sh b/scripts/build.sh index 5eab42ba9..5bdacb1e8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf $DISTDIR/* - for d in exporter/*/ instrumentation/*/ propagator/*/ sdk-extension/*/ util/*/ ; do + for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-distro/ instrumentation/*/ propagator/*/ sdk-extension/*/ util/*/ ; do ( echo "building $d" cd "$d" diff --git a/scripts/generate_instrumentation_bootstrap.py b/scripts/generate_instrumentation_bootstrap.py index c5832d0eb..631879557 100755 --- a/scripts/generate_instrumentation_bootstrap.py +++ b/scripts/generate_instrumentation_bootstrap.py @@ -15,17 +15,18 @@ # limitations under the License. import ast -import filecmp import logging import os import subprocess import sys -import tempfile import astor import pkg_resources -import requests -from otel_packaging import get_instrumentation_packages, scripts_path +from otel_packaging import ( + get_instrumentation_packages, + root_path, + scripts_path, +) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("instrumentation_list_generator") @@ -49,16 +50,14 @@ libraries = {} default_instrumentations = [] """ -tmpdir = tempfile.TemporaryDirectory() # pylint: disable=R1732 -gen_path = os.path.join(tmpdir.name, "new.py",) - -current_path = os.path.join(tmpdir.name, "current.py",) - -core_repo = os.getenv("CORE_REPO_SHA", "main") -url = f"https://raw.githubusercontent.com/open-telemetry/opentelemetry-python/{core_repo}/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py" -r = requests.get(url, allow_redirects=True) -with open(current_path, "wb") as output: - output.write(r.content) +gen_path = os.path.join( + root_path, + "opentelemetry-instrumentation", + "src", + "opentelemetry", + "instrumentation", + "bootstrap_gen.py", +) def main(): @@ -98,7 +97,7 @@ def main(): "scripts/eachdist.py", "format", "--path", - tmpdir.name, + "opentelemetry-instrumentation/src", ], check=True, ) @@ -106,15 +105,5 @@ def main(): logger.info("generated %s", gen_path) -def compare(): - if not filecmp.cmp(current_path, gen_path): - logger.info( - 'Generated code is out of date, please run "tox -e generate" and commit bootstrap_gen.py to core repo.' - ) - os.replace(gen_path, "bootstrap_gen.py") - sys.exit(1) - - if __name__ == "__main__": main() - compare() diff --git a/tox.ini b/tox.ini index 20ac03b53..9533aeb7e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,14 @@ envlist = py3{6,7,8,9}-test-sdkextension-aws pypy3-test-sdkextension-aws + ; opentelemetry-distro + py3{6,7,8,9}-test-distro + pypy3-test-distro + + ; opentelemetry-instrumentation + py3{6,7,8,9}-test-opentelemetry-instrumentation + pypy3-test-opentelemetry-instrumentation + ; opentelemetry-instrumentation-aiohttp-client py3{6,7,8,9}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client @@ -199,6 +207,8 @@ setenv = CORE_REPO="git+https://github.com/open-telemetry/opentelemetry-python.git@{env:CORE_REPO_SHA}" changedir = + test-distro: opentelemetry-distro/tests + test-opentelemetry-instrumentation: opentelemetry-instrumentation/tests test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests @@ -246,9 +256,11 @@ commands_pre = ; cases but it saves a lot of boilerplate in this file. test: pip install "opentelemetry-api[test] @ {env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api" test: pip install "opentelemetry-semantic-conventions[test] @ {env:CORE_REPO}#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions" - test: pip install "opentelemetry-instrumentation[test] @ {env:CORE_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" test: pip install "opentelemetry-sdk[test] @ {env:CORE_REPO}#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk" test: pip install "opentelemetry-test[test] @ {env:CORE_REPO}#egg=opentelemetry-test&subdirectory=tests/util" + test: pip install {toxinidir}/opentelemetry-instrumentation + + distro: pip install {toxinidir}/opentelemetry-distro celery: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] @@ -317,7 +329,7 @@ commands_pre = sqlalchemy{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] - elasticsearch{2,5,6}: pip install "{env:CORE_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] + elasticsearch{2,5,6}: pip install {toxinidir}/opentelemetry-instrumentation[test] {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] httpx: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test] @@ -345,8 +357,8 @@ deps = commands_pre = python -m pip install "{env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api" python -m pip install "{env:CORE_REPO}#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions" - python -m pip install "{env:CORE_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" python -m pip install "{env:CORE_REPO}#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk" + python -m pip install {toxinidir}/opentelemetry-instrumentation python -m pip install {toxinidir}/util/opentelemetry-util-http changedir = docs @@ -370,10 +382,10 @@ deps = commands_pre = python -m pip install "{env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api" python -m pip install "{env:CORE_REPO}#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions" - python -m pip install "{env:CORE_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" python -m pip install "{env:CORE_REPO}#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk" python -m pip install "{env:CORE_REPO}#egg=opentelemetry-test&subdirectory=tests/util" python -m pip install -e {toxinidir}/util/opentelemetry-util-http[test] + python -m pip install -e {toxinidir}/opentelemetry-instrumentation[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] @@ -412,6 +424,7 @@ commands_pre = python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test] python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] + python -m pip install -e {toxinidir}/opentelemetry-distro[test] commands = python scripts/eachdist.py lint --check-only @@ -441,9 +454,9 @@ changedir = commands_pre = pip install "{env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api" \ "{env:CORE_REPO}#egg=opentelemetry-semantic-conventions&subdirectory=opentelemetry-semantic-conventions" \ - "{env:CORE_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation" \ "{env:CORE_REPO}#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk" \ "{env:CORE_REPO}#egg=opentelemetry-test&subdirectory=tests/util" \ + -e {toxinidir}/opentelemetry-instrumentation \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery \ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pika \