diff --git a/CHANGELOG.md b/CHANGELOG.md index be5cd01123..f9d97a5986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3266)) - `opentelemetry-instrumentation-botocore` Add support for GenAI choice events ([#3275](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3275)) +- `opentelemetry-instrumentation` make it simpler to initialize auto-instrumentation programmatically + ([#3273](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3273)) ### Fixed diff --git a/opentelemetry-instrumentation/README.rst b/opentelemetry-instrumentation/README.rst index 3ed88c213f..ce17dbbe0c 100644 --- a/opentelemetry-instrumentation/README.rst +++ b/opentelemetry-instrumentation/README.rst @@ -130,6 +130,19 @@ start celery with the rest of the arguments. The above command will configure the global trace provider to use the Random IDs Generator, and then pass ``--port=3000`` to ``flask run``. +Programmatic Auto-instrumentation +-------------------- + +:: + + from opentelemetry.instrumentation import auto_instrumentation + auto_instrumentation.initialize() + + +If you are in an environment where you cannot use opentelemetry-instrument to inject auto-instrumentation you can do so programmatically with +the code above. Please note that some instrumentations may require the ``initialize()`` method to be called before the library they +instrument is imported. + References ---------- diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 963b3a6956..69af0b4cea 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -19,6 +19,12 @@ from re import sub from shutil import which +from opentelemetry.instrumentation.auto_instrumentation._load import ( + _load_configurators, + _load_distro, + _load_instrumentors, +) +from opentelemetry.instrumentation.utils import _python_path_without_directory from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import entry_points @@ -110,3 +116,20 @@ def run() -> None: executable = which(args.command) execl(executable, executable, *args.command_args) + + +def initialize(): + """Setup auto-instrumentation, called by the sitecustomize module""" + # prevents auto-instrumentation of subprocesses if code execs another python process + if "PYTHONPATH" in environ: + environ["PYTHONPATH"] = _python_path_without_directory( + environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep + ) + + try: + distro = _load_distro() + distro.configure() + _load_configurators() + _load_instrumentors(distro) + except Exception: # pylint: disable=broad-except + _logger.exception("Failed to auto initialize OpenTelemetry") diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py index 912675f1b7..c126b87372 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py @@ -12,33 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from logging import getLogger -from os import environ -from os.path import abspath, dirname, pathsep - -from opentelemetry.instrumentation.auto_instrumentation._load import ( - _load_configurators, - _load_distro, - _load_instrumentors, -) -from opentelemetry.instrumentation.utils import _python_path_without_directory - -logger = getLogger(__name__) - - -def initialize(): - # prevents auto-instrumentation of subprocesses if code execs another python process - environ["PYTHONPATH"] = _python_path_without_directory( - environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep - ) - - try: - distro = _load_distro() - distro.configure() - _load_configurators() - _load_instrumentors(distro) - except Exception: # pylint: disable=broad-except - logger.exception("Failed to auto initialize opentelemetry") - +from opentelemetry.instrumentation.auto_instrumentation import initialize initialize() diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_initialize.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_initialize.py new file mode 100644 index 0000000000..6d05a69c8e --- /dev/null +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_initialize.py @@ -0,0 +1,61 @@ +# 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 +from os.path import abspath, dirname, pathsep +from unittest import TestCase +from unittest.mock import patch + +from opentelemetry.instrumentation import auto_instrumentation + +# TODO: convert to assertNoLogs instead of mocking logger when 3.10 is baseline + + +class TestInitialize(TestCase): + auto_instrumentation_path = dirname(abspath(auto_instrumentation.__file__)) + + @patch.dict("os.environ", {}, clear=True) + @patch("opentelemetry.instrumentation.auto_instrumentation._logger") + def test_handles_pythonpath_not_set(self, logger_mock): + auto_instrumentation.initialize() + self.assertNotIn("PYTHONPATH", environ) + logger_mock.exception.assert_not_called() + + @patch.dict("os.environ", {"PYTHONPATH": "."}) + @patch("opentelemetry.instrumentation.auto_instrumentation._logger") + def test_handles_pythonpath_set(self, logger_mock): + auto_instrumentation.initialize() + self.assertEqual(environ["PYTHONPATH"], ".") + logger_mock.exception.assert_not_called() + + @patch.dict( + "os.environ", + {"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"}, + ) + @patch("opentelemetry.instrumentation.auto_instrumentation._logger") + def test_clears_auto_instrumentation_path(self, logger_mock): + auto_instrumentation.initialize() + self.assertEqual(environ["PYTHONPATH"], "foo") + logger_mock.exception.assert_not_called() + + @patch("opentelemetry.instrumentation.auto_instrumentation._logger") + @patch("opentelemetry.instrumentation.auto_instrumentation._load_distro") + def test_handles_exceptions(self, load_distro_mock, logger_mock): + # pylint:disable=no-self-use + load_distro_mock.side_effect = ValueError + auto_instrumentation.initialize() + logger_mock.exception.assert_called_once_with( + "Failed to auto initialize OpenTelemetry" + ) diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_sitecustomize.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_sitecustomize.py new file mode 100644 index 0000000000..97b5133c38 --- /dev/null +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_sitecustomize.py @@ -0,0 +1,28 @@ +# 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 unittest.mock import patch + + +class TestSiteCustomize(TestCase): + # pylint:disable=import-outside-toplevel,unused-import,no-self-use + @patch("opentelemetry.instrumentation.auto_instrumentation.initialize") + def test_sitecustomize_side_effects(self, initialize_mock): + initialize_mock.assert_not_called() + + import opentelemetry.instrumentation.auto_instrumentation.sitecustomize # NOQA + + initialize_mock.assert_called_once()