Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ uv.lock
# Sandbox
sandbox/
.bob/
.serena/
164 changes: 136 additions & 28 deletions src/instana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import os
import sys
from importlib import util as importlib_util
from typing import Tuple

from instana.collector.helpers.runtime import (
is_autowrapt_instrumented,
is_webhook_instrumented,
Expand Down Expand Up @@ -59,10 +57,7 @@


def load(_: object) -> None:
"""
Method used to activate the Instana sensor via AUTOWRAPT_BOOTSTRAP
environment variable.
"""
"""Activate the Instana Tracer via the AUTOWRAPT_BOOTSTRAP environment variable."""
# Work around https://bugs.python.org/issue32573
if not hasattr(sys, "argv"):
sys.argv = [""]
Expand Down Expand Up @@ -100,16 +95,105 @@ def key_to_bool(k: str) -> bool:
monkey.patch_all()


def get_aws_lambda_handler() -> Tuple[str, str]:
# Guards against boot_agent() being called more than once when monkey_patch()
# is invoked multiple times in the same process (e.g. application code also
# calls it after Instana has already wrapped it).
_eventlet_booted = False


def _defer_boot_until_eventlet_patch() -> None:
"""Defer boot_agent() until after eventlet.monkey_patch() has been called.

Strategy
--------
Two ``wrapt.when_imported`` hooks are always registered — one for the
gunicorn eventlet worker path and one for the bare ``monkey_patch()`` path.
The decision of *which* hook actually boots is made at runtime by checking
``sys.modules``, not by inspecting whether gunicorn is installed:

Hook 1 — ``gunicorn.workers.geventlet`` (gunicorn -k eventlet):
Wraps ``EventletWorker.patch()``, which gunicorn calls only post-fork
inside the worker process (``init_process → self.patch()``).
``when_imported`` is a no-op if the module is never loaded, so
gunicorn ≥26 (which removed this worker) and non-eventlet worker
classes never trigger it.

Hook 2 — ``eventlet.monkey_patch`` (bare application use):
Fires for every other caller (plain scripts, ``socketio.run()``,
gunicorn ≥26, etc.). If Hook 1 already booted the agent — i.e.
``gunicorn.workers.geventlet`` is in ``sys.modules`` — this hook
skips the boot so the arbiter is never initialised when
``--preload`` causes ``monkey_patch()`` to be called there.

Why ``sys.modules`` instead of ``find_spec("gunicorn")``:
``find_spec`` answers "is gunicorn *installed*?", not "is this
process running under gunicorn's eventlet worker?". A gunicorn ≥26
installation, or any non-eventlet worker, would suppress Hook 2
with the old ``find_spec`` branch, leaving no boot path at all.
"""
For instrumenting AWS Lambda, users specify their original lambda handler
in the LAMBDA_HANDLER environment variable. This function searches for and
parses that environment variable or returns the defaults.
import wrapt

def _boot_once() -> None:
global _eventlet_booted
if not _eventlet_booted:
_eventlet_booted = True
if is_truthy(os.environ.get("INSTANA_AUTOPROFILE", None)):
_start_profiler()
boot_agent()

# Hook 1: gunicorn -k eventlet — boots in the worker, after monkey_patch().
# Gunicorn resolves worker_class in Arbiter.setup() before preload_app, so
# geventlet is imported in the arbiter. when_imported fires there and wraps
# EventletWorker.patch; the wrapper itself runs post-fork in the worker.
@wrapt.when_imported("gunicorn.workers.geventlet")
def _on_geventlet_imported(module: object) -> None:
def _after_worker_patch(
wrapped: object,
instance: object,
args: tuple[object, ...],
kwargs: dict[str, object],
) -> object:
result = wrapped(*args, **kwargs)
_boot_once()
return result

wrapt.wrap_function_wrapper(
module,
"EventletWorker.patch",
_after_worker_patch,
)

# Hook 2: bare monkey_patch() call — covers every non-gunicorn scenario and
# gunicorn ≥26 / non-eventlet-worker setups. Skips boot when Hook 1 already
# handled it (geventlet in sys.modules) to prevent arbiter-side boot under
# --preload.
@wrapt.when_imported("eventlet")
def _on_eventlet_imported(module: object) -> None:
def _after_monkey_patch(
wrapped: object,
instance: object,
args: tuple[object, ...],
kwargs: dict[str, object],
) -> object:
result = wrapped(*args, **kwargs)
if "gunicorn.workers.geventlet" not in sys.modules:
_boot_once()
return result

wrapt.wrap_function_wrapper(module, "monkey_patch", _after_monkey_patch)


def get_aws_lambda_handler() -> tuple[str, str]:
"""Return the AWS Lambda handler module and function name.

Users specify their original lambda handler in the LAMBDA_HANDLER
environment variable. This function searches for and parses that
environment variable or returns the defaults.

The default handler value for AWS Lambda is 'lambda_function.lambda_handler'
which equates to the function "lambda_handler in a file named
lambda_function.py" or in Python terms
"from lambda_function import lambda_handler"
which equates to the function ``lambda_handler`` in a file named
``lambda_function.py``, or in Python terms
``from lambda_function import lambda_handler``.
"""
handler_module = "lambda_function"
handler_function = "lambda_handler"
Expand All @@ -126,11 +210,10 @@ def get_aws_lambda_handler() -> Tuple[str, str]:


def lambda_handler(event: str, context: str) -> None:
"""
Entry point for AWS Lambda monitoring.
"""Entry point for AWS Lambda monitoring.

This function will trigger the initialization of Instana monitoring and then call
the original user specified lambda handler function.
Triggers the initialization of Instana monitoring and then calls
the original user-specified lambda handler function.
"""
module_name, function_name = get_aws_lambda_handler()

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


def boot_agent() -> None:
"""Initialize the Instana agent and conditionally load auto-instrumentation."""
"""Initialize the Instana agent and conditionally load auto-instrumentation.

Imports all instrumentation modules unless INSTANA_DISABLE_AUTO_INSTR is set.
"""

import instana.singletons # noqa: F401

Expand All @@ -169,6 +255,7 @@ def boot_agent() -> None:
elasticsearch, # noqa: F401
fastapi, # noqa: F401
flask, # noqa: F401
gevent, # noqa: F401
grpcio, # noqa: F401
httpx, # noqa: F401
logging, # noqa: F401
Expand All @@ -187,7 +274,6 @@ def boot_agent() -> None:
starlette, # noqa: F401
urllib3, # noqa: F401
werkzeug, # noqa: F401
gevent, # noqa: F401
)
from instana.instrumentation.aiohttp import (
client as aiohttp_client, # noqa: F401
Expand Down Expand Up @@ -223,7 +309,10 @@ def boot_agent() -> None:


def _start_profiler() -> None:
"""Start the Instana Auto Profile."""
"""Start the Instana Auto Profile.

Retrieves the profiler singleton and starts it if available.
"""
from instana.singletons import get_profiler

if profiler := get_profiler():
Expand Down Expand Up @@ -251,17 +340,36 @@ def _start_profiler() -> None:
f"Instana: No use in monitoring this process type ({os.path.basename(sys.argv[0])}). Will go sit in a corner quietly."
)
else:
# Automatic gevent monkey patching
# unless auto instrumentation is off, then the customer should do manual gevent monkey patching
if (
(is_autowrapt_instrumented() or is_webhook_instrumented())
and "INSTANA_DISABLE_AUTO_INSTR" not in os.environ
and importlib_util.find_spec("gevent")
):
apply_gevent_monkey_patch()
# Automatic gevent monkey patching
# unless auto instrumentation is off, then the customer should do manual gevent monkey patching
if importlib_util.find_spec("gevent"):
apply_gevent_monkey_patch()

# Eventlet deferred boot: opt-in via INSTANA_EVENTLET_DEFERRED_BOOT=true.
# When set, boot_agent() is deferred until after eventlet.monkey_patch() to
# prevent the ssl.SSLContext RecursionError with gunicorn eventlet workers.
# Without the opt-in we boot immediately, avoiding silent tracing gaps when
# eventlet is installed as a transitive dependency but not actually in use
# (e.g. sync/gthread workers, Celery, plain scripts).
if importlib_util.find_spec("eventlet") and is_truthy(
os.environ.get("INSTANA_EVENTLET_DEFERRED_BOOT", None)
):
# boot_agent() will be called by the wrapper after monkey_patch;
# do not call it here to avoid a double boot.
_defer_boot_until_eventlet_patch()
return_early = True
else:
return_early = False
else:
return_early = False

# AutoProfile
if "INSTANA_AUTOPROFILE" in os.environ:
_start_profiler()
if not return_early:
# AutoProfile
if is_truthy(os.environ.get("INSTANA_AUTOPROFILE", None)):
_start_profiler()

boot_agent()
boot_agent()
Loading
Loading