Skip to content

Commit 7f25ce2

Browse files
authored
Add Monkey Patch for Gevent (#218)
*Issue #, if available:* Some customers are using gevent in their Python application with opentelemetry, and this causes the application to crash due to `maximum recursion depth exceeded` error. This is because `gevent` uses a specific version of the `ssl` module and runs `monkey.patch_all()` to patch the ssl module to the required version. The `monkey` command patches already existing modules to the required version, which may cause problems if there are other applications already using the module before monkey is ran. Because Opentelemetry is also using the `ssl` module , it creates a conflict and causes the application to crash. The recommended solution is to run `moneky.patch_all()` in opentelemetry beforehand so that such conflicts do not occur. *Description of changes:* The issue experienced by customers is specifically due to the `ssl` module, therefore, we can run monkey.patch_ssl() command to only monkey the `ssl` module and reduce the scope. The distro will check if the customer application has the `gevent` module installed, and if so, run the `monkey.patch_ssl()`. *Test* Built opentelemetry docker image with the change, modified the operator to install opentelemetry on sample python application with the image. Checked that the sample application has opentelemetry running and doesn't crash when APIs are called. Test run: https://github.com/aws-observability/aws-otel-python-instrumentation/actions/runs/9813426624 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
2 parents 6177b7e + 3bfe704 commit 7f25ce2

File tree

4 files changed

+112
-2
lines changed

4 files changed

+112
-2
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py

+36
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
4+
import os
45
import sys
56
from logging import Logger, getLogger
67

78
import pkg_resources
89

910
from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches
1011

12+
# Env variable for determining whether we want to monkey patch gevent modules. Possible values are 'all', 'none', and
13+
# comma separated list 'os, thread, time, sys, socket, select, ssl, subprocess, builtins, signal, queue, contextvars'
14+
AWS_GEVENT_PATCH_MODULES = "AWS_GEVENT_PATCH_MODULES"
15+
1116
_logger: Logger = getLogger(__name__)
1217

1318

@@ -20,6 +25,37 @@ def apply_instrumentation_patches() -> None:
2025
2126
Where possible, automated testing should be run to catch upstream changes resulting in broken patches
2227
"""
28+
if _is_installed("gevent"):
29+
try:
30+
gevent_patch_module = os.environ.get(AWS_GEVENT_PATCH_MODULES, "all")
31+
32+
if gevent_patch_module != "none":
33+
# pylint: disable=import-outside-toplevel
34+
# Delay import to only occur if monkey patch is needed (e.g. gevent is used to run application).
35+
from gevent import monkey
36+
37+
if gevent_patch_module == "all":
38+
39+
monkey.patch_all()
40+
else:
41+
module_list = [module.strip() for module in gevent_patch_module.split(",")]
42+
43+
monkey.patch_all(
44+
socket="socket" in module_list,
45+
time="time" in module_list,
46+
select="select" in module_list,
47+
thread="thread" in module_list,
48+
os="os" in module_list,
49+
ssl="ssl" in module_list,
50+
subprocess="subprocess" in module_list,
51+
sys="sys" in module_list,
52+
builtins="builtins" in module_list,
53+
signal="signal" in module_list,
54+
queue="queue" in module_list,
55+
contextvars="contextvars" in module_list,
56+
)
57+
except Exception as exc: # pylint: disable=broad-except
58+
_logger.info("Failed to monkey patch gevent, exception: %s", exc)
2359

2460
if _is_installed("botocore ~= 1.0"):
2561
# pylint: disable=import-outside-toplevel

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py

+74-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3+
import os
34
from typing import Dict
45
from unittest import TestCase
56
from unittest.mock import MagicMock, patch
67

8+
import gevent.monkey
79
import pkg_resources
810

9-
from amazon.opentelemetry.distro.patches._instrumentation_patch import apply_instrumentation_patches
11+
from amazon.opentelemetry.distro.patches._instrumentation_patch import (
12+
AWS_GEVENT_PATCH_MODULES,
13+
apply_instrumentation_patches,
14+
)
1015
from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS
1116
from opentelemetry.semconv.trace import SpanAttributes
1217

@@ -54,15 +59,37 @@ def test_instrumentation_patch(self):
5459
def _run_patch_behaviour_tests(self):
5560
# Test setup
5661
self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "CorrectDistributionObject"
62+
# Test setup to not patch gevent
63+
os.environ[AWS_GEVENT_PATCH_MODULES] = "none"
5764

5865
# Validate unpatched upstream behaviour - important to detect upstream changes that may break instrumentation
5966
self._test_unpatched_botocore_instrumentation()
67+
self._test_unpatched_gevent_instrumentation()
6068

6169
# Apply patches
6270
apply_instrumentation_patches()
6371

6472
# Validate patched upstream behaviour - important to detect downstream changes that may break instrumentation
6573
self._test_patched_botocore_instrumentation()
74+
self._test_unpatched_gevent_instrumentation()
75+
76+
# Test setup to check whether only these two modules get patched by gevent monkey
77+
os.environ[AWS_GEVENT_PATCH_MODULES] = "os, ssl"
78+
79+
# Apply patches
80+
apply_instrumentation_patches()
81+
82+
# Validate that os and ssl gevent monkey patch modules were patched
83+
self._test_patched_gevent_os_ssl_instrumentation()
84+
85+
# Set the value to 'all' so that all the remaining gevent monkey patch modules are patched
86+
os.environ[AWS_GEVENT_PATCH_MODULES] = "all"
87+
88+
# Apply patches again.
89+
apply_instrumentation_patches()
90+
91+
# Validate that remaining gevent monkey patch modules were patched
92+
self._test_patched_gevent_instrumentation()
6693

6794
# Test teardown
6895
self._reset_mocks()
@@ -93,6 +120,20 @@ def _test_unpatched_botocore_instrumentation(self):
93120
self.assertFalse("aws.sqs.queue.url" in attributes)
94121
self.assertFalse("aws.sqs.queue.name" in attributes)
95122

123+
def _test_unpatched_gevent_instrumentation(self):
124+
self.assertFalse(gevent.monkey.is_module_patched("os"), "gevent os module has been patched")
125+
self.assertFalse(gevent.monkey.is_module_patched("thread"), "gevent thread module has been patched")
126+
self.assertFalse(gevent.monkey.is_module_patched("time"), "gevent time module has been patched")
127+
self.assertFalse(gevent.monkey.is_module_patched("sys"), "gevent sys module has been patched")
128+
self.assertFalse(gevent.monkey.is_module_patched("socket"), "gevent socket module has been patched")
129+
self.assertFalse(gevent.monkey.is_module_patched("select"), "gevent select module has been patched")
130+
self.assertFalse(gevent.monkey.is_module_patched("ssl"), "gevent ssl module has been patched")
131+
self.assertFalse(gevent.monkey.is_module_patched("subprocess"), "gevent subprocess module has been patched")
132+
self.assertFalse(gevent.monkey.is_module_patched("builtins"), "gevent builtins module has been patched")
133+
self.assertFalse(gevent.monkey.is_module_patched("signal"), "gevent signal module has been patched")
134+
self.assertFalse(gevent.monkey.is_module_patched("queue"), "gevent queue module has been patched")
135+
self.assertFalse(gevent.monkey.is_module_patched("contextvars"), "gevent contextvars module has been patched")
136+
96137
def _test_patched_botocore_instrumentation(self):
97138
# Kinesis
98139
self.assertTrue("kinesis" in _KNOWN_EXTENSIONS)
@@ -115,6 +156,38 @@ def _test_patched_botocore_instrumentation(self):
115156
self.assertTrue("aws.sqs.queue.name" in sqs_attributes)
116157
self.assertEqual(sqs_attributes["aws.sqs.queue.name"], _QUEUE_NAME)
117158

159+
def _test_patched_gevent_os_ssl_instrumentation(self):
160+
# Only ssl and os module should have been patched since the environment variable was set to 'os, ssl'
161+
self.assertTrue(gevent.monkey.is_module_patched("ssl"), "gevent ssl module has not been patched")
162+
self.assertTrue(gevent.monkey.is_module_patched("os"), "gevent os module has not been patched")
163+
# Rest should still be unpatched
164+
self.assertFalse(gevent.monkey.is_module_patched("thread"), "gevent thread module has been patched")
165+
self.assertFalse(gevent.monkey.is_module_patched("time"), "gevent time module has been patched")
166+
self.assertFalse(gevent.monkey.is_module_patched("sys"), "gevent sys module has been patched")
167+
self.assertFalse(gevent.monkey.is_module_patched("socket"), "gevent socket module has been patched")
168+
self.assertFalse(gevent.monkey.is_module_patched("select"), "gevent select module has been patched")
169+
self.assertFalse(gevent.monkey.is_module_patched("subprocess"), "gevent subprocess module has been patched")
170+
self.assertFalse(gevent.monkey.is_module_patched("builtins"), "gevent builtins module has been patched")
171+
self.assertFalse(gevent.monkey.is_module_patched("signal"), "gevent signal module has been patched")
172+
self.assertFalse(gevent.monkey.is_module_patched("queue"), "gevent queue module has been patched")
173+
self.assertFalse(gevent.monkey.is_module_patched("contextvars"), "gevent contextvars module has been patched")
174+
175+
def _test_patched_gevent_instrumentation(self):
176+
self.assertTrue(gevent.monkey.is_module_patched("os"), "gevent os module has not been patched")
177+
self.assertTrue(gevent.monkey.is_module_patched("time"), "gevent time module has not been patched")
178+
self.assertTrue(gevent.monkey.is_module_patched("socket"), "gevent socket module has not been patched")
179+
self.assertTrue(gevent.monkey.is_module_patched("select"), "gevent select module has not been patched")
180+
self.assertTrue(gevent.monkey.is_module_patched("ssl"), "gevent ssl module has not been patched")
181+
self.assertTrue(gevent.monkey.is_module_patched("subprocess"), "gevent subprocess module has not been patched")
182+
self.assertTrue(gevent.monkey.is_module_patched("signal"), "gevent signal module has not been patched")
183+
self.assertTrue(gevent.monkey.is_module_patched("queue"), "gevent queue module has not been patched")
184+
185+
# Current version of gevent.monkey.patch_all() does not do anything to these modules despite being called
186+
self.assertFalse(gevent.monkey.is_module_patched("thread"), "gevent thread module has been patched")
187+
self.assertFalse(gevent.monkey.is_module_patched("sys"), "gevent sys module has been patched")
188+
self.assertFalse(gevent.monkey.is_module_patched("builtins"), "gevent builtins module not been patched")
189+
self.assertFalse(gevent.monkey.is_module_patched("contextvars"), "gevent contextvars module has been patched")
190+
118191
def _test_botocore_installed_flag(self):
119192
with patch(
120193
"amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"

dev-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ codespell==2.1.0
1414
requests==2.32.0
1515
ruamel.yaml==0.17.21
1616
flaky==3.7.0
17-
botocore==1.34.67
17+
botocore==1.34.67

tox.ini

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ commands_pre =
3030
; Install common packages for all the tests. These are not needed in all the
3131
; cases but it saves a lot of boilerplate in this file.
3232
test: pip install botocore
33+
test: pip install gevent
3334
test: pip install "opentelemetry-api[test] @ {env:CORE_REPO}#egg=opentelemetry-api&subdirectory=opentelemetry-api"
3435
test: pip install "opentelemetry-sdk[test] @ {env:CORE_REPO}#egg=opentelemetry-sdk&subdirectory=opentelemetry-sdk"
3536
test: pip install "opentelemetry-instrumentation[test] @ {env:CONTRIB_REPO}#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation"

0 commit comments

Comments
 (0)