1414import os
1515import sys
1616from importlib import util as importlib_util
17- from typing import Tuple
18-
1917from instana .collector .helpers .runtime import (
2018 is_autowrapt_instrumented ,
2119 is_webhook_instrumented ,
5957
6058
6159def 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
128212def 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
155238def 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
225311def _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