Skip to content

Commit 61b030d

Browse files
authored
Merge branch 'main' into release-3.19.2
2 parents c04afb0 + 8899a37 commit 61b030d

3 files changed

Lines changed: 507 additions & 28 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ uv.lock
106106
# Sandbox
107107
sandbox/
108108
.bob/
109+
.serena/

‎src/instana/__init__.py‎

Lines changed: 136 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import os
1515
import sys
1616
from importlib import util as importlib_util
17-
from typing import Tuple
18-
1917
from instana.collector.helpers.runtime import (
2018
is_autowrapt_instrumented,
2119
is_webhook_instrumented,
@@ -59,10 +57,7 @@
5957

6058

6159
def load(_: object) -> None:
62-
"""
63-
Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP
64-
environment variable.
65-
"""
60+
"""Activate the Instana Tracer via the AUTOWRAPT_BOOTSTRAP environment variable."""
6661
# Work around https://bugs.python.org/issue32573
6762
if not hasattr(sys, "argv"):
6863
sys.argv = [""]
@@ -100,16 +95,105 @@ def key_to_bool(k: str) -> bool:
10095
monkey.patch_all()
10196

10297

103-
def get_aws_lambda_handler() -> Tuple[str, str]:
98+
# Guards against boot_agent() being called more than once when monkey_patch()
99+
# is invoked multiple times in the same process (e.g. application code also
100+
# calls it after Instana has already wrapped it).
101+
_eventlet_booted = False
102+
103+
104+
def _defer_boot_until_eventlet_patch() -> None:
105+
"""Defer boot_agent() until after eventlet.monkey_patch() has been called.
106+
107+
Strategy
108+
--------
109+
Two ``wrapt.when_imported`` hooks are always registered — one for the
110+
gunicorn eventlet worker path and one for the bare ``monkey_patch()`` path.
111+
The decision of *which* hook actually boots is made at runtime by checking
112+
``sys.modules``, not by inspecting whether gunicorn is installed:
113+
114+
Hook 1 — ``gunicorn.workers.geventlet`` (gunicorn -k eventlet):
115+
Wraps ``EventletWorker.patch()``, which gunicorn calls only post-fork
116+
inside the worker process (``init_process → self.patch()``).
117+
``when_imported`` is a no-op if the module is never loaded, so
118+
gunicorn ≥26 (which removed this worker) and non-eventlet worker
119+
classes never trigger it.
120+
121+
Hook 2 — ``eventlet.monkey_patch`` (bare application use):
122+
Fires for every other caller (plain scripts, ``socketio.run()``,
123+
gunicorn ≥26, etc.). If Hook 1 already booted the agent — i.e.
124+
``gunicorn.workers.geventlet`` is in ``sys.modules`` — this hook
125+
skips the boot so the arbiter is never initialised when
126+
``--preload`` causes ``monkey_patch()`` to be called there.
127+
128+
Why ``sys.modules`` instead of ``find_spec("gunicorn")``:
129+
``find_spec`` answers "is gunicorn *installed*?", not "is this
130+
process running under gunicorn's eventlet worker?". A gunicorn ≥26
131+
installation, or any non-eventlet worker, would suppress Hook 2
132+
with the old ``find_spec`` branch, leaving no boot path at all.
104133
"""
105-
For instrumenting AWS Lambda, users specify their original lambda handler
106-
in the LAMBDA_HANDLER environment variable. This function searches for and
107-
parses that environment variable or returns the defaults.
134+
import wrapt
135+
136+
def _boot_once() -> None:
137+
global _eventlet_booted
138+
if not _eventlet_booted:
139+
_eventlet_booted = True
140+
if is_truthy(os.environ.get("INSTANA_AUTOPROFILE", None)):
141+
_start_profiler()
142+
boot_agent()
143+
144+
# Hook 1: gunicorn -k eventlet — boots in the worker, after monkey_patch().
145+
# Gunicorn resolves worker_class in Arbiter.setup() before preload_app, so
146+
# geventlet is imported in the arbiter. when_imported fires there and wraps
147+
# EventletWorker.patch; the wrapper itself runs post-fork in the worker.
148+
@wrapt.when_imported("gunicorn.workers.geventlet")
149+
def _on_geventlet_imported(module: object) -> None:
150+
def _after_worker_patch(
151+
wrapped: object,
152+
instance: object,
153+
args: tuple[object, ...],
154+
kwargs: dict[str, object],
155+
) -> object:
156+
result = wrapped(*args, **kwargs)
157+
_boot_once()
158+
return result
159+
160+
wrapt.wrap_function_wrapper(
161+
module,
162+
"EventletWorker.patch",
163+
_after_worker_patch,
164+
)
165+
166+
# Hook 2: bare monkey_patch() call — covers every non-gunicorn scenario and
167+
# gunicorn ≥26 / non-eventlet-worker setups. Skips boot when Hook 1 already
168+
# handled it (geventlet in sys.modules) to prevent arbiter-side boot under
169+
# --preload.
170+
@wrapt.when_imported("eventlet")
171+
def _on_eventlet_imported(module: object) -> None:
172+
def _after_monkey_patch(
173+
wrapped: object,
174+
instance: object,
175+
args: tuple[object, ...],
176+
kwargs: dict[str, object],
177+
) -> object:
178+
result = wrapped(*args, **kwargs)
179+
if "gunicorn.workers.geventlet" not in sys.modules:
180+
_boot_once()
181+
return result
182+
183+
wrapt.wrap_function_wrapper(module, "monkey_patch", _after_monkey_patch)
184+
185+
186+
def get_aws_lambda_handler() -> tuple[str, str]:
187+
"""Return the AWS Lambda handler module and function name.
188+
189+
Users specify their original lambda handler in the LAMBDA_HANDLER
190+
environment variable. This function searches for and parses that
191+
environment variable or returns the defaults.
108192
109193
The default handler value for AWS Lambda is 'lambda_function.lambda_handler'
110-
which equates to the function "lambda_handler in a file named
111-
lambda_function.py" or in Python terms
112-
"from lambda_function import lambda_handler"
194+
which equates to the function ``lambda_handler`` in a file named
195+
``lambda_function.py``, or in Python terms
196+
``from lambda_function import lambda_handler``.
113197
"""
114198
handler_module = "lambda_function"
115199
handler_function = "lambda_handler"
@@ -126,11 +210,10 @@ def get_aws_lambda_handler() -> Tuple[str, str]:
126210

127211

128212
def lambda_handler(event: str, context: str) -> None:
129-
"""
130-
Entry point for AWS Lambda monitoring.
213+
"""Entry point for AWS Lambda monitoring.
131214
132-
This function will trigger the initialization of Instana monitoring and then call
133-
the original user specified lambda handler function.
215+
Triggers the initialization of Instana monitoring and then calls
216+
the original user-specified lambda handler function.
134217
"""
135218
module_name, function_name = get_aws_lambda_handler()
136219

@@ -153,7 +236,10 @@ def lambda_handler(event: str, context: str) -> None:
153236

154237

155238
def boot_agent() -> None:
156-
"""Initialize the Instana agent and conditionally load auto-instrumentation."""
239+
"""Initialize the Instana agent and conditionally load auto-instrumentation.
240+
241+
Imports all instrumentation modules unless INSTANA_DISABLE_AUTO_INSTR is set.
242+
"""
157243

158244
import instana.singletons # noqa: F401
159245

@@ -169,6 +255,7 @@ def boot_agent() -> None:
169255
elasticsearch, # noqa: F401
170256
fastapi, # noqa: F401
171257
flask, # noqa: F401
258+
gevent, # noqa: F401
172259
grpcio, # noqa: F401
173260
httpx, # noqa: F401
174261
logging, # noqa: F401
@@ -187,7 +274,6 @@ def boot_agent() -> None:
187274
starlette, # noqa: F401
188275
urllib3, # noqa: F401
189276
werkzeug, # noqa: F401
190-
gevent, # noqa: F401
191277
)
192278
from instana.instrumentation.aiohttp import (
193279
client as aiohttp_client, # noqa: F401
@@ -223,7 +309,10 @@ def boot_agent() -> None:
223309

224310

225311
def _start_profiler() -> None:
226-
"""Start the Instana Auto Profile."""
312+
"""Start the Instana Auto Profile.
313+
314+
Retrieves the profiler singleton and starts it if available.
315+
"""
227316
from instana.singletons import get_profiler
228317

229318
if profiler := get_profiler():
@@ -251,17 +340,36 @@ def _start_profiler() -> None:
251340
f"Instana: No use in monitoring this process type ({os.path.basename(sys.argv[0])}). Will go sit in a corner quietly."
252341
)
253342
else:
254-
# Automatic gevent monkey patching
255-
# unless auto instrumentation is off, then the customer should do manual gevent monkey patching
256343
if (
257344
(is_autowrapt_instrumented() or is_webhook_instrumented())
258345
and "INSTANA_DISABLE_AUTO_INSTR" not in os.environ
259-
and importlib_util.find_spec("gevent")
260346
):
261-
apply_gevent_monkey_patch()
347+
# Automatic gevent monkey patching
348+
# unless auto instrumentation is off, then the customer should do manual gevent monkey patching
349+
if importlib_util.find_spec("gevent"):
350+
apply_gevent_monkey_patch()
351+
352+
# Eventlet deferred boot: opt-in via INSTANA_EVENTLET_DEFERRED_BOOT=true.
353+
# When set, boot_agent() is deferred until after eventlet.monkey_patch() to
354+
# prevent the ssl.SSLContext RecursionError with gunicorn eventlet workers.
355+
# Without the opt-in we boot immediately, avoiding silent tracing gaps when
356+
# eventlet is installed as a transitive dependency but not actually in use
357+
# (e.g. sync/gthread workers, Celery, plain scripts).
358+
if importlib_util.find_spec("eventlet") and is_truthy(
359+
os.environ.get("INSTANA_EVENTLET_DEFERRED_BOOT", None)
360+
):
361+
# boot_agent() will be called by the wrapper after monkey_patch;
362+
# do not call it here to avoid a double boot.
363+
_defer_boot_until_eventlet_patch()
364+
return_early = True
365+
else:
366+
return_early = False
367+
else:
368+
return_early = False
262369

263-
# AutoProfile
264-
if "INSTANA_AUTOPROFILE" in os.environ:
265-
_start_profiler()
370+
if not return_early:
371+
# AutoProfile
372+
if is_truthy(os.environ.get("INSTANA_AUTOPROFILE", None)):
373+
_start_profiler()
266374

267-
boot_agent()
375+
boot_agent()

0 commit comments

Comments
 (0)