Skip to content

Commit e5c9c0b

Browse files
herin049xrmx
andauthored
feat: update auto-instrumentation to re-inject instrumentation path after init (#4469)
* feat: add ability to auto-instrument subprocesses * update CHANGELOG.md * move changelog entry * update auto-instrumentation to re-inject instrumentation path after init * update CHANGELOG.md * add descriptive comment * update formatting * add changelog fragment * add subprocess test case * fix lint errors --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 6ad8336 commit e5c9c0b

3 files changed

Lines changed: 115 additions & 15 deletions

File tree

.changelog/4469.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-instrumentation`: update auto-instrumentation to re-inject instrumentation path after init

opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,7 @@ def run() -> None:
112112
execl(executable, executable, *args.command_args)
113113

114114

115-
def initialize(*, swallow_exceptions: bool = True) -> None:
116-
"""
117-
Setup auto-instrumentation, called by the sitecustomize module
118-
119-
:param swallow_exceptions: Whether or not to propagate instrumentation exceptions to the caller. Exceptions are logged and swallowed by default.
120-
"""
121-
# prevents auto-instrumentation of subprocesses if code execs another python process
122-
if "PYTHONPATH" in environ:
123-
environ["PYTHONPATH"] = _python_path_without_directory(
124-
environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep
125-
)
126-
115+
def _initialize(*, swallow_exceptions: bool = True) -> None:
127116
# handle optional gevent monkey patching. This is done via environment variables so it may be used from the
128117
# opentelemetry operator
129118
gevent_patch: str | None = environ.get(
@@ -157,3 +146,37 @@ def initialize(*, swallow_exceptions: bool = True) -> None:
157146
_logger.exception("Failed to auto initialize OpenTelemetry")
158147
if not swallow_exceptions:
159148
raise exc
149+
150+
151+
def initialize(*, swallow_exceptions: bool = True) -> None:
152+
"""
153+
Setup auto-instrumentation, called by the sitecustomize module
154+
155+
:param swallow_exceptions: Whether or not to propagate instrumentation exceptions to the caller. Exceptions are logged and swallowed by default.
156+
"""
157+
filedir = dirname(abspath(__file__))
158+
159+
python_path = environ.get("PYTHONPATH")
160+
auto_instrumentation_path_was_present = (
161+
python_path is not None and filedir in python_path.split(pathsep)
162+
)
163+
164+
# Remove the auto-instrumentation path during initialization to prevent
165+
# auto-instrumentation from executing in subprocesses spawned during this phase.
166+
# This suppression is performed to avoid creating a recursive loop scenario
167+
# where subprocesses spawned in the initialization phase execute the
168+
# initialization phase again, spawning more subprocesses.
169+
if python_path is not None:
170+
environ["PYTHONPATH"] = _python_path_without_directory(
171+
python_path, filedir, pathsep
172+
)
173+
174+
try:
175+
_initialize(swallow_exceptions=swallow_exceptions)
176+
finally:
177+
if auto_instrumentation_path_was_present:
178+
current = environ.get("PYTHONPATH", "")
179+
if filedir not in current.split(pathsep):
180+
environ["PYTHONPATH"] = (
181+
filedir + pathsep + current if current else filedir
182+
)

opentelemetry-instrumentation/tests/auto_instrumentation/test_initialize.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
# SPDX-License-Identifier: Apache-2.0
33
# type: ignore
44

5+
import subprocess
6+
import sys
57
from os import environ
68
from os.path import abspath, dirname, pathsep
79
from unittest import TestCase
8-
from unittest.mock import patch
10+
from unittest.mock import MagicMock, patch
911

1012
from opentelemetry.instrumentation import auto_instrumentation
1113

@@ -34,11 +36,85 @@ def test_handles_pythonpath_set(self, logger_mock):
3436
{"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"},
3537
)
3638
@patch("opentelemetry.instrumentation.auto_instrumentation._logger")
37-
def test_clears_auto_instrumentation_path(self, logger_mock):
39+
def test_restores_auto_instrumentation_path_after_init(self, logger_mock):
3840
auto_instrumentation.initialize()
39-
self.assertEqual(environ["PYTHONPATH"], "foo")
41+
paths = environ["PYTHONPATH"].split(pathsep)
42+
self.assertIn(self.auto_instrumentation_path, paths)
43+
self.assertIn("foo", paths)
4044
logger_mock.exception.assert_not_called()
4145

46+
@patch.dict(
47+
"os.environ",
48+
{"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"},
49+
)
50+
@patch("opentelemetry.instrumentation.auto_instrumentation._logger")
51+
@patch("opentelemetry.instrumentation.auto_instrumentation._load_distro")
52+
def test_preserves_pythonpath_changes_during_init(
53+
self, load_distro_mock, _logger_mock
54+
):
55+
def modify_pythonpath(*_):
56+
environ["PYTHONPATH"] = (
57+
environ.get("PYTHONPATH", "") + pathsep + "added_during_init"
58+
)
59+
distro = MagicMock()
60+
distro.configure.return_value = None
61+
return distro
62+
63+
load_distro_mock.side_effect = modify_pythonpath
64+
auto_instrumentation.initialize()
65+
paths = environ["PYTHONPATH"].split(pathsep)
66+
self.assertIn(self.auto_instrumentation_path, paths)
67+
self.assertIn("added_during_init", paths)
68+
69+
@patch.dict(
70+
"os.environ",
71+
{"PYTHONPATH": auto_instrumentation_path + pathsep + "foo"},
72+
)
73+
@patch("opentelemetry.instrumentation.auto_instrumentation._logger")
74+
@patch("opentelemetry.instrumentation.auto_instrumentation._load_distro")
75+
def test_subprocess_sees_pythonpath_changes(
76+
self, load_distro_mock, _logger_mock
77+
):
78+
during_init_paths: list[str] | None = None
79+
80+
def capture_pythonpath_in_subprocess(*_):
81+
nonlocal during_init_paths
82+
result = subprocess.run(
83+
[
84+
sys.executable,
85+
"-c",
86+
"import os; print(os.environ.get('PYTHONPATH', ''))",
87+
],
88+
capture_output=True,
89+
text=True,
90+
check=False,
91+
)
92+
raw = result.stdout.strip()
93+
during_init_paths = raw.split(pathsep) if raw else []
94+
distro = MagicMock()
95+
distro.configure.return_value = None
96+
return distro
97+
98+
load_distro_mock.side_effect = capture_pythonpath_in_subprocess
99+
auto_instrumentation.initialize()
100+
101+
self.assertIsNotNone(during_init_paths)
102+
self.assertNotIn(self.auto_instrumentation_path, during_init_paths)
103+
104+
result_after = subprocess.run(
105+
[
106+
sys.executable,
107+
"-c",
108+
"import os; print(os.environ.get('PYTHONPATH', ''))",
109+
],
110+
capture_output=True,
111+
text=True,
112+
check=False,
113+
)
114+
raw_after = result_after.stdout.strip()
115+
after_init_paths = raw_after.split(pathsep) if raw_after else []
116+
self.assertIn(self.auto_instrumentation_path, after_init_paths)
117+
42118
@patch("opentelemetry.instrumentation.auto_instrumentation._logger")
43119
@patch("opentelemetry.instrumentation.auto_instrumentation._load_distro")
44120
def test_handles_exceptions(self, load_distro_mock, logger_mock):

0 commit comments

Comments
 (0)