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 \