sf-veritas 0.9.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

Files changed (86) hide show
  1. sf_veritas/.gitignore +2 -0
  2. sf_veritas/__init__.py +4 -0
  3. sf_veritas/app_config.py +49 -0
  4. sf_veritas/cli.py +336 -0
  5. sf_veritas/constants.py +3 -0
  6. sf_veritas/custom_excepthook.py +285 -0
  7. sf_veritas/custom_log_handler.py +53 -0
  8. sf_veritas/custom_output_wrapper.py +107 -0
  9. sf_veritas/custom_print.py +34 -0
  10. sf_veritas/django_app.py +5 -0
  11. sf_veritas/env_vars.py +83 -0
  12. sf_veritas/exception_handling_middleware.py +18 -0
  13. sf_veritas/exception_metaclass.py +69 -0
  14. sf_veritas/frame_tools.py +112 -0
  15. sf_veritas/import_hook.py +62 -0
  16. sf_veritas/infra_details/__init__.py +3 -0
  17. sf_veritas/infra_details/get_infra_details.py +24 -0
  18. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  19. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  20. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  21. sf_veritas/infra_details/running_on/__init__.py +17 -0
  22. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  23. sf_veritas/interceptors.py +252 -0
  24. sf_veritas/local_env_detect.py +118 -0
  25. sf_veritas/package_metadata.py +6 -0
  26. sf_veritas/patches/__init__.py +0 -0
  27. sf_veritas/patches/concurrent_futures.py +19 -0
  28. sf_veritas/patches/constants.py +1 -0
  29. sf_veritas/patches/exceptions.py +82 -0
  30. sf_veritas/patches/multiprocessing.py +32 -0
  31. sf_veritas/patches/network_libraries/__init__.py +51 -0
  32. sf_veritas/patches/network_libraries/aiohttp.py +100 -0
  33. sf_veritas/patches/network_libraries/curl_cffi.py +93 -0
  34. sf_veritas/patches/network_libraries/http_client.py +64 -0
  35. sf_veritas/patches/network_libraries/httpcore.py +152 -0
  36. sf_veritas/patches/network_libraries/httplib2.py +76 -0
  37. sf_veritas/patches/network_libraries/httpx.py +123 -0
  38. sf_veritas/patches/network_libraries/niquests.py +192 -0
  39. sf_veritas/patches/network_libraries/pycurl.py +71 -0
  40. sf_veritas/patches/network_libraries/requests.py +187 -0
  41. sf_veritas/patches/network_libraries/tornado.py +139 -0
  42. sf_veritas/patches/network_libraries/treq.py +122 -0
  43. sf_veritas/patches/network_libraries/urllib_request.py +129 -0
  44. sf_veritas/patches/network_libraries/utils.py +101 -0
  45. sf_veritas/patches/os.py +17 -0
  46. sf_veritas/patches/threading.py +32 -0
  47. sf_veritas/patches/web_frameworks/__init__.py +45 -0
  48. sf_veritas/patches/web_frameworks/aiohttp.py +133 -0
  49. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +132 -0
  50. sf_veritas/patches/web_frameworks/blacksheep.py +107 -0
  51. sf_veritas/patches/web_frameworks/bottle.py +142 -0
  52. sf_veritas/patches/web_frameworks/cherrypy.py +246 -0
  53. sf_veritas/patches/web_frameworks/django.py +307 -0
  54. sf_veritas/patches/web_frameworks/eve.py +138 -0
  55. sf_veritas/patches/web_frameworks/falcon.py +229 -0
  56. sf_veritas/patches/web_frameworks/fastapi.py +145 -0
  57. sf_veritas/patches/web_frameworks/flask.py +186 -0
  58. sf_veritas/patches/web_frameworks/klein.py +40 -0
  59. sf_veritas/patches/web_frameworks/litestar.py +217 -0
  60. sf_veritas/patches/web_frameworks/pyramid.py +89 -0
  61. sf_veritas/patches/web_frameworks/quart.py +155 -0
  62. sf_veritas/patches/web_frameworks/robyn.py +114 -0
  63. sf_veritas/patches/web_frameworks/sanic.py +120 -0
  64. sf_veritas/patches/web_frameworks/starlette.py +144 -0
  65. sf_veritas/patches/web_frameworks/strawberry.py +269 -0
  66. sf_veritas/patches/web_frameworks/tornado.py +129 -0
  67. sf_veritas/patches/web_frameworks/utils.py +55 -0
  68. sf_veritas/print_override.py +13 -0
  69. sf_veritas/regular_data_transmitter.py +358 -0
  70. sf_veritas/request_interceptor.py +399 -0
  71. sf_veritas/request_utils.py +104 -0
  72. sf_veritas/server_status.py +1 -0
  73. sf_veritas/shutdown_flag.py +11 -0
  74. sf_veritas/subprocess_startup.py +3 -0
  75. sf_veritas/test_cli.py +145 -0
  76. sf_veritas/thread_local.py +436 -0
  77. sf_veritas/timeutil.py +114 -0
  78. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  79. sf_veritas/transmitter.py +58 -0
  80. sf_veritas/types.py +44 -0
  81. sf_veritas/unified_interceptor.py +323 -0
  82. sf_veritas/utils.py +39 -0
  83. sf_veritas-0.9.7.dist-info/METADATA +83 -0
  84. sf_veritas-0.9.7.dist-info/RECORD +86 -0
  85. sf_veritas-0.9.7.dist-info/WHEEL +4 -0
  86. sf_veritas-0.9.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,132 @@
1
+ import inspect
2
+ import sys
3
+ import sysconfig
4
+ from functools import lru_cache, wraps
5
+ from typing import Any, Callable, Optional, Set, Tuple
6
+
7
+ from ...constants import SAILFISH_TRACING_HEADER
8
+ from ...custom_excepthook import custom_excepthook
9
+ from ...env_vars import PRINT_CONFIGURATION_STATUSES, SF_DEBUG
10
+ from ...regular_data_transmitter import NetworkHopsTransmitter
11
+ from ...thread_local import get_or_set_sf_trace_id
12
+ from .utils import _unwrap_user_func
13
+
14
+ # ────────────────────────────────────────────────────
15
+ # User-code predicate: skip stdlib & site-packages
16
+ # ────────────────────────────────────────────────────
17
+ _STDLIB = sysconfig.get_paths()["stdlib"]
18
+ _SITE_TAGS = ("site-packages", "dist-packages")
19
+ _SKIP_PREFIXES = (_STDLIB, "/usr/local/lib/python", "/usr/lib/python")
20
+
21
+
22
+ @lru_cache(maxsize=512)
23
+ def _is_user_code(path: Optional[str] = None) -> bool:
24
+ """True only for your application files."""
25
+ if not path or path.startswith("<"):
26
+ return False
27
+ for p in _SKIP_PREFIXES:
28
+ if path.startswith(p):
29
+ return False
30
+ return not any(tag in path for tag in _SITE_TAGS)
31
+
32
+
33
+ # ────────────────────────────────────────────────────
34
+ # Patch AsyncConsumer.__call__ to hook connect + receive
35
+ # ────────────────────────────────────────────────────
36
+ def patch_async_consumer_call():
37
+ """
38
+ Wraps AsyncConsumer.__call__ so that for each HTTP or WebSocket
39
+ connection:
40
+ 1) SAILFISH_TRACING_HEADER → ContextVar
41
+ 2) Emit a NetworkHop at first user frame in websocket_connect
42
+ 3) Dynamically wrap websocket_receive to emit a hop on first message
43
+ 4) Forward any exception to custom_excepthook
44
+ """
45
+ try:
46
+ from channels.consumer import AsyncConsumer # type: ignore
47
+
48
+ orig_call = AsyncConsumer.__call__
49
+ except:
50
+ if PRINT_CONFIGURATION_STATUSES:
51
+ print("Channels AsyncConsumer not found; skipping patch", log=False)
52
+ return
53
+
54
+ if PRINT_CONFIGURATION_STATUSES:
55
+ print("Patching AsyncConsumer.__call__ for NetworkHops", log=False)
56
+
57
+ @wraps(orig_call)
58
+ async def custom_call(self, scope, receive, send):
59
+ # — Propagate header into ContextVar —
60
+ header_val = None
61
+ if scope["type"] in ("http", "websocket"):
62
+ for name, val in scope.get("headers", []):
63
+ if name.lower() == SAILFISH_TRACING_HEADER.lower().encode():
64
+ header_val = val.decode("utf-8")
65
+ break
66
+ get_or_set_sf_trace_id(header_val, is_associated_with_inbound_request=True)
67
+
68
+ # — One-shot profiler for websocket_connect inside orig_call —
69
+ def tracer(frame, event, _arg):
70
+ if event == "call":
71
+ fn_path = frame.f_code.co_filename
72
+ if _is_user_code(fn_path):
73
+ _, session = get_or_set_sf_trace_id()
74
+ NetworkHopsTransmitter().send(
75
+ session_id=session,
76
+ line=str(frame.f_lineno),
77
+ column="0",
78
+ name=frame.f_code.co_name,
79
+ entrypoint=fn_path,
80
+ )
81
+ sys.setprofile(None)
82
+ return None
83
+ return tracer
84
+
85
+ sys.setprofile(tracer)
86
+
87
+ # — Dynamically wrap this instance's websocket_receive —
88
+ recv = getattr(self, "websocket_receive", None)
89
+ if recv and hasattr(self, "websocket_receive"):
90
+
91
+ @wraps(recv)
92
+ async def wrapped_receive(event):
93
+ # Emit first user-frame hop inside receive
94
+ def recv_tracer(fr, ev, _a):
95
+ if ev == "call":
96
+ path = fr.f_code.co_filename
97
+ if _is_user_code(path):
98
+ _, sess = get_or_set_sf_trace_id()
99
+ NetworkHopsTransmitter().send(
100
+ session_id=sess,
101
+ line=str(fr.f_lineno),
102
+ column="0",
103
+ name=fr.f_code.co_name,
104
+ entrypoint=path,
105
+ )
106
+ sys.setprofile(None)
107
+ return None
108
+ return recv_tracer
109
+
110
+ sys.setprofile(recv_tracer)
111
+ try:
112
+ return await recv(event)
113
+ finally:
114
+ sys.setprofile(None)
115
+
116
+ # override on this instance only
117
+ setattr(self, "websocket_receive", wrapped_receive)
118
+
119
+ # — Call through to original (handles connect, receive, disconnect) —
120
+ try:
121
+ await orig_call(self, scope, receive, send)
122
+ except Exception as exc:
123
+ custom_excepthook(type(exc), exc, exc.__traceback__)
124
+ raise
125
+ finally:
126
+ sys.setprofile(None)
127
+
128
+ # Apply the patch
129
+ AsyncConsumer.__call__ = custom_call
130
+
131
+ if PRINT_CONFIGURATION_STATUSES:
132
+ print("AsyncConsumer.__call__ patched successfully", log=False)
@@ -0,0 +1,107 @@
1
+ """
2
+ Context-var propagation + first-hop NetworkHop emission.
3
+ """
4
+
5
+ # ------------------------------------------------------------------ #
6
+ # Shared helpers (same as Django/FastAPI utils)
7
+ # ------------------------------------------------------------------ #
8
+ import inspect
9
+ import sysconfig
10
+ from functools import lru_cache
11
+ from typing import Any, Callable, Optional, Set, Tuple
12
+
13
+ from ...constants import SAILFISH_TRACING_HEADER
14
+ from ...custom_excepthook import custom_excepthook
15
+ from ...env_vars import SF_DEBUG
16
+ from ...regular_data_transmitter import NetworkHopsTransmitter
17
+ from ...thread_local import get_or_set_sf_trace_id
18
+ from .utils import _is_user_code, _unwrap_user_func
19
+
20
+
21
+ # ------------------------------------------------------------------ #
22
+ # Middleware
23
+ # ------------------------------------------------------------------ #
24
+ async def _sf_tracing_middleware(request, handler):
25
+ """
26
+ BlackSheep function-style middleware that:
27
+ 1. Propagates trace-id from SAILFISH_TRACING_HEADER.
28
+ 2. Emits one NetworkHop for the first user-land handler.
29
+ 3. Captures *any* exception (HTTPException, RuntimeError, etc.),
30
+ passes it to `custom_excepthook`, then re-raises so BlackSheep
31
+ can continue its normal error handling.
32
+ """
33
+
34
+ # 1. Header → ContextVar
35
+ raw_hdr = request.headers.get_first(SAILFISH_TRACING_HEADER.encode())
36
+ if raw_hdr:
37
+ try:
38
+ hdr_val = raw_hdr.decode()
39
+ except UnicodeDecodeError:
40
+ hdr_val = str(raw_hdr)
41
+ get_or_set_sf_trace_id(hdr_val, is_associated_with_inbound_request=True)
42
+
43
+ # 2. Hop capture (once per request)
44
+ if not getattr(request, "_sf_hop_sent", False):
45
+ user_fn = _unwrap_user_func(handler)
46
+ if (
47
+ inspect.isfunction(user_fn)
48
+ and _is_user_code(user_fn.__code__.co_filename)
49
+ and not user_fn.__module__.startswith("strawberry")
50
+ ):
51
+ filename = user_fn.__code__.co_filename
52
+ line_no = user_fn.__code__.co_firstlineno
53
+ fn_name = user_fn.__name__
54
+ _, session_id = get_or_set_sf_trace_id()
55
+
56
+ if SF_DEBUG:
57
+ print(
58
+ f"[[BlackSheepHop]] {fn_name} ({filename}:{line_no}) "
59
+ f"session={session_id}",
60
+ log=False,
61
+ )
62
+
63
+ NetworkHopsTransmitter().send(
64
+ session_id=session_id,
65
+ line=str(line_no),
66
+ column="0",
67
+ name=fn_name,
68
+ entrypoint=filename,
69
+ )
70
+ request._sf_hop_sent = True # mark as done
71
+
72
+ # 3. Continue down the chain and capture exceptions
73
+ try:
74
+ return await handler(request)
75
+ except Exception as exc: # ← includes HTTPException & friends
76
+ custom_excepthook(type(exc), exc, exc.__traceback__)
77
+ raise # Let BlackSheep build the response
78
+
79
+
80
+ # ------------------------------------------------------------------ #
81
+ # Monkey-patch Application.__init__
82
+ # ------------------------------------------------------------------ #
83
+ def patch_blacksheep():
84
+ """
85
+ Injects the tracing middleware into every BlackSheep Application.
86
+ Safe no-op if BlackSheep isn't installed or already patched.
87
+ """
88
+ try:
89
+ from blacksheep import Application
90
+ except ImportError:
91
+ return
92
+
93
+ if getattr(Application, "__sf_tracing_patched__", False):
94
+ return # already patched
95
+
96
+ original_init = Application.__init__
97
+
98
+ def patched_init(self, *args, **kwargs):
99
+ original_init(self, *args, **kwargs)
100
+ # Put our middleware first so we run before user middlewares
101
+ self.middlewares.insert(0, _sf_tracing_middleware)
102
+
103
+ Application.__init__ = patched_init
104
+ Application.__sf_tracing_patched__ = True
105
+
106
+ if SF_DEBUG:
107
+ print("[[patch_blacksheep]] tracing middleware installed", log=False)
@@ -0,0 +1,142 @@
1
+ from ...constants import SAILFISH_TRACING_HEADER
2
+ from ...custom_excepthook import custom_excepthook
3
+ from ...env_vars import SF_DEBUG
4
+ from ...regular_data_transmitter import NetworkHopsTransmitter
5
+ from ...thread_local import get_or_set_sf_trace_id
6
+ from .utils import _is_user_code, _unwrap_user_func # cached helpers
7
+
8
+
9
+ # ------------------------------------------------------------------------------------
10
+ # 1. Hop-capturing plugin ----------------------------------------------------------------
11
+ # ------------------------------------------------------------------------------------
12
+ class _SFTracingPlugin:
13
+ """Bottle plugin (API v2) – wraps each route callback exactly once."""
14
+
15
+ name = "sf_network_hop"
16
+ api = 2
17
+
18
+ def apply(self, callback, route):
19
+ # 1. Resolve real user function
20
+ real_fn = _unwrap_user_func(callback)
21
+ mod = real_fn.__module__
22
+ code = getattr(real_fn, "__code__", None)
23
+
24
+ # 2. Skip library frames and Strawberry GraphQL handlers
25
+ if (
26
+ not code
27
+ or not _is_user_code(code.co_filename)
28
+ or mod.startswith("strawberry")
29
+ ):
30
+ return callback # no wrapping
31
+
32
+ filename, line_no, fn_name = (
33
+ code.co_filename,
34
+ code.co_firstlineno,
35
+ real_fn.__name__,
36
+ )
37
+ hop_key = (filename, line_no)
38
+
39
+ # 3. Wrapper that emits exactly one hop per request
40
+ from bottle import request # local to avoid hard dep
41
+
42
+ def _wrapped(*args, **kwargs): # noqa: ANN001
43
+ sent = request.environ.setdefault("_sf_hops_sent", set())
44
+ if hop_key not in sent:
45
+ _, session_id = get_or_set_sf_trace_id()
46
+
47
+ if SF_DEBUG:
48
+ print(
49
+ f"[[SFTracingBottle]] hop → {fn_name} "
50
+ f"({filename}:{line_no}) session={session_id}",
51
+ log=False,
52
+ )
53
+
54
+ NetworkHopsTransmitter().send(
55
+ session_id=session_id,
56
+ line=str(line_no),
57
+ column="0",
58
+ name=fn_name,
59
+ entrypoint=filename,
60
+ )
61
+ sent.add(hop_key)
62
+
63
+ return callback(*args, **kwargs)
64
+
65
+ return _wrapped
66
+
67
+
68
+ # ------------------------------------------------------------------------------------
69
+ # 2. Context-propagation hook ---------------------------------------------------------
70
+ # ------------------------------------------------------------------------------------
71
+ def _install_before_request(app):
72
+ from bottle import request
73
+
74
+ @app.hook("before_request")
75
+ def _extract_sf_trace_id():
76
+ if hdr := request.headers.get(SAILFISH_TRACING_HEADER):
77
+ get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
78
+
79
+
80
+ # ------------------------------------------------------------------------------------
81
+ # NEW: Global error-handler wrapper for Bottle
82
+ # ------------------------------------------------------------------------------------
83
+ def _install_error_handler(app):
84
+ """
85
+ Replace ``app.default_error_handler`` so *any* exception or HTTPError
86
+ (including those raised via ``abort()`` or ``HTTPError(status=500)``)
87
+ is reported to ``custom_excepthook`` before Bottle builds the response.
88
+
89
+ Bottle always funnels errors through this function, regardless of debug
90
+ mode. See Bottle docs on *Error Handlers*.
91
+ """
92
+ original_handler = app.default_error_handler
93
+
94
+ def _sf_error_handler(error):
95
+ # Forward full traceback (HTTPError keeps it on .__traceback__)
96
+ custom_excepthook(type(error), error, getattr(error, "__traceback__", None))
97
+ return original_handler(error)
98
+
99
+ app.default_error_handler = _sf_error_handler
100
+
101
+
102
+ # ------------------------------------------------------------------------------------
103
+ # 3. Public patch function – call this once at startup
104
+ # ------------------------------------------------------------------------------------
105
+ def patch_bottle():
106
+ """
107
+ • Adds before_request header propagation.
108
+ • Installs NetworkHop plugin (covers all current & future routes).
109
+ • Wraps default_error_handler so exceptions (incl. HTTPError 500) are captured.
110
+ Safe no-op if Bottle is not installed or already patched.
111
+ """
112
+ try:
113
+ import bottle
114
+ except ImportError: # Bottle absent
115
+ return
116
+
117
+ if getattr(bottle.Bottle, "__sf_tracing_patched__", False):
118
+ return
119
+
120
+ # ---- patch Bottle.__init__ ----------------------------------------------------
121
+ original_init = bottle.Bottle.__init__
122
+
123
+ def patched_init(self, *args, **kwargs):
124
+ original_init(self, *args, **kwargs)
125
+
126
+ # ContextVar propagation
127
+ _install_before_request(self)
128
+
129
+ # Install hop plugin (Plugin API v2 ― applies to all routes, past & future)
130
+ self.install(_SFTracingPlugin())
131
+
132
+ # Exception capture (HTTPError 500 or any uncaught Exception)
133
+ _install_error_handler(self)
134
+
135
+ if SF_DEBUG:
136
+ print(
137
+ "[[patch_bottle]] tracing hook + plugin + error handler installed",
138
+ log=False,
139
+ )
140
+
141
+ bottle.Bottle.__init__ = patched_init
142
+ bottle.Bottle.__sf_tracing_patched__ = True
@@ -0,0 +1,246 @@
1
+ """
2
+ • Header propagation via Application.__call__ (unchanged).
3
+ • Global CherryPy Tool (‘before_handler') → 1 NetworkHop (fixed).
4
+ """
5
+
6
+ import inspect
7
+ import types
8
+ from typing import Any, Callable, Iterable, Set
9
+
10
+ from ...constants import SAILFISH_TRACING_HEADER
11
+ from ...custom_excepthook import custom_excepthook # ← NEW
12
+ from ...env_vars import SF_DEBUG
13
+ from ...regular_data_transmitter import NetworkHopsTransmitter
14
+ from ...thread_local import get_or_set_sf_trace_id
15
+ from .utils import _is_user_code
16
+
17
+ # ------------------------------------------------------------------ #
18
+ # Robust un-wrapper (handles LateParamPageHandler, etc.)
19
+ # ------------------------------------------------------------------ #
20
+ _ATTR_CANDIDATES: Iterable[str] = (
21
+ "resolver",
22
+ "func",
23
+ "python_func",
24
+ "_resolver",
25
+ "wrapped_func",
26
+ "__func",
27
+ "callable", # CherryPy handlers
28
+ )
29
+
30
+
31
+ def _unwrap_user_func(fn: Callable[..., Any]) -> Callable[..., Any]:
32
+ """
33
+ Walk through the layers of wrappers/decorators/handler objects around *fn*
34
+ and return the first plain Python *function* object that:
35
+ • lives in user-land code (per _is_user_code)
36
+ • has a real __code__ object.
37
+ The search is breadth-first and robust to cyclic references.
38
+ """
39
+ seen: Set[int] = set()
40
+ queue = [fn]
41
+
42
+ while queue:
43
+ current = queue.pop()
44
+ cid = id(current)
45
+ if cid in seen:
46
+ continue
47
+ seen.add(cid)
48
+
49
+ # ── 1. Bound methods (types.MethodType) ──────────────────────────
50
+ # CherryPy's LateParamPageHandler.callable is usually a bound method.
51
+ if isinstance(current, types.MethodType):
52
+ queue.append(current.__func__)
53
+ continue # don't inspect the MethodType itself any further
54
+
55
+ # ── 2. Plain user function? ─────────────────────────────────────
56
+ if inspect.isfunction(current) and _is_user_code(
57
+ getattr(current.__code__, "co_filename", "")
58
+ ):
59
+ return current
60
+
61
+ # ── 3. CherryPy PageHandler exposes `.callable` ──────────────────
62
+ target = getattr(current, "callable", None)
63
+ if callable(target):
64
+ queue.append(target)
65
+
66
+ # ── 4. functools.wraps chain (`__wrapped__`) ─────────────────────
67
+ wrapped = getattr(current, "__wrapped__", None)
68
+ if callable(wrapped):
69
+ queue.append(wrapped)
70
+
71
+ # ── 5. Other common wrapper attributes ───────────────────────────
72
+ for attr in _ATTR_CANDIDATES:
73
+ val = getattr(current, attr, None)
74
+ if callable(val):
75
+ queue.append(val)
76
+
77
+ # ── 6. Objects with a user-defined __call__ method ───────────────
78
+ call_attr = getattr(current, "__call__", None)
79
+ if (
80
+ callable(call_attr)
81
+ and inspect.isfunction(call_attr)
82
+ and _is_user_code(getattr(call_attr.__code__, "co_filename", ""))
83
+ ):
84
+ queue.append(call_attr)
85
+
86
+ # ── 7. Closure cells inside functions / inner scopes ─────────────
87
+ code_obj = getattr(current, "__code__", None)
88
+ clos = getattr(current, "__closure__", None)
89
+ if code_obj and clos:
90
+ for cell in clos:
91
+ cell_val = cell.cell_contents
92
+ if callable(cell_val):
93
+ queue.append(cell_val)
94
+
95
+ # Fallback: return the original callable (likely framework code)
96
+ return fn
97
+
98
+
99
+ # 2b.  Exception-capture tool (runs *after* an error is detected)
100
+ def _exception_capture_tool():
101
+ """
102
+ CherryPy calls the ‘before_error_response' hook whenever it is about to
103
+ finalise an error page, regardless of whether the error is a framework
104
+ HTTPError/HTTPRedirect or an uncaught Python exception.
105
+ We tap that hook and forward the traceback to Sailfish.
106
+ """
107
+ import sys
108
+
109
+ exc_type, exc_value, exc_tb = sys.exc_info()
110
+ if exc_value:
111
+ if SF_DEBUG:
112
+ print(
113
+ f"[[SFTracingCherryPy]] captured exception: {exc_value!r}",
114
+ log=False,
115
+ )
116
+ custom_excepthook(exc_type, exc_value, exc_tb)
117
+
118
+
119
+ # ------------------------------------------------------------------ #
120
+ # Main patch entry-point
121
+ # ------------------------------------------------------------------ #
122
+ def patch_cherrypy():
123
+ """
124
+ • Propagate SAILFISH_TRACING_HEADER header → ContextVar.
125
+ • Emit one NetworkHop for the first *user* handler frame in each request.
126
+ • Capture **all** CherryPy exceptions (HTTPError, HTTPRedirect, uncaught
127
+ Python errors) and forward them to `custom_excepthook`.
128
+ """
129
+ try:
130
+ import cherrypy # CherryPy may not be installed
131
+ except ImportError:
132
+ return
133
+
134
+ # ──────────────────────────────────────────────────────────────────
135
+ # 1. Header propagation – monkey-patch Application.__call__
136
+ # ──────────────────────────────────────────────────────────────────
137
+ env_key = "HTTP_" + SAILFISH_TRACING_HEADER.upper().replace("-", "_")
138
+ if not getattr(cherrypy.Application, "__sf_hdr_patched__", False):
139
+ orig_call = cherrypy.Application.__call__
140
+
141
+ def patched_call(self, environ, start_response):
142
+ hdr = environ.get(env_key)
143
+ if hdr:
144
+ get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
145
+ return orig_call(self, environ, start_response)
146
+
147
+ cherrypy.Application.__call__ = patched_call
148
+ cherrypy.Application.__sf_hdr_patched__ = True
149
+
150
+ # ──────────────────────────────────────────────────────────────────
151
+ # 2a. Network-hop tool (runs before each handler)
152
+ # ──────────────────────────────────────────────────────────────────
153
+ def _network_hop_tool():
154
+ req = cherrypy.serving.request # thread-local current request
155
+ handler = getattr(req, "handler", None)
156
+ if not callable(handler):
157
+ return
158
+
159
+ real_fn = _unwrap_user_func(handler)
160
+ # Skip GraphQL (Strawberry) or non-user code
161
+ if real_fn.__module__.startswith("strawberry"):
162
+ return
163
+ code = getattr(real_fn, "__code__", None)
164
+ if not code or not _is_user_code(code.co_filename):
165
+ return
166
+
167
+ hop_key = (code.co_filename, code.co_firstlineno)
168
+ sent = getattr(req, "_sf_hops_sent", set())
169
+ if hop_key in sent:
170
+ return
171
+
172
+ _, session_id = get_or_set_sf_trace_id()
173
+ if SF_DEBUG:
174
+ print(
175
+ f"[[SFTracingCherryPy]] hop → {real_fn.__name__} "
176
+ f"({code.co_filename}:{code.co_firstlineno}) "
177
+ f"session={session_id}",
178
+ log=False,
179
+ )
180
+
181
+ NetworkHopsTransmitter().send(
182
+ session_id=session_id,
183
+ line=str(code.co_firstlineno),
184
+ column="0",
185
+ name=real_fn.__name__,
186
+ entrypoint=code.co_filename,
187
+ )
188
+ sent.add(hop_key)
189
+ req._sf_hops_sent = sent
190
+
191
+ if not hasattr(cherrypy.tools, "sf_network_hop"):
192
+ cherrypy.tools.sf_network_hop = cherrypy.Tool(
193
+ "before_handler", _network_hop_tool, priority=5
194
+ )
195
+
196
+ # ──────────────────────────────────────────────────────────────────
197
+ # 2b. Exception-capture tool (runs before error response)
198
+ # ──────────────────────────────────────────────────────────────────
199
+ def _exception_capture_tool():
200
+ import sys
201
+
202
+ exc_type, exc_value, exc_tb = sys.exc_info()
203
+ if exc_value:
204
+ if SF_DEBUG:
205
+ print(
206
+ f"[[SFTracingCherryPy]] captured exception: {exc_value!r}",
207
+ log=False,
208
+ )
209
+ custom_excepthook(exc_type, exc_value, exc_tb)
210
+
211
+ if not hasattr(cherrypy.tools, "sf_exception_capture"):
212
+ cherrypy.tools.sf_exception_capture = cherrypy.Tool(
213
+ "before_error_response", _exception_capture_tool, priority=100
214
+ )
215
+
216
+ # ──────────────────────────────────────────────────────────────────
217
+ # 3. Enable both tools globally
218
+ # ──────────────────────────────────────────────────────────────────
219
+ cherrypy.config.update(
220
+ {
221
+ "tools.sf_network_hop.on": True,
222
+ "tools.sf_exception_capture.on": True,
223
+ }
224
+ )
225
+
226
+ # ──────────────────────────────────────────────────────────────────
227
+ # 4️⃣ Ensure every new Application inherits the tool settings
228
+ # ──────────────────────────────────────────────────────────────────
229
+ if not getattr(cherrypy.Application, "__sf_app_patched__", False):
230
+ orig_app_init = cherrypy.Application.__init__
231
+
232
+ def patched_app_init(self, root, script_name="", config=None):
233
+ config = config or {}
234
+ root_conf = config.setdefault("/", {})
235
+ root_conf.setdefault("tools.sf_network_hop.on", True)
236
+ root_conf.setdefault("tools.sf_exception_capture.on", True)
237
+ orig_app_init(self, root, script_name, config)
238
+
239
+ cherrypy.Application.__init__ = patched_app_init
240
+ cherrypy.Application.__sf_app_patched__ = True
241
+
242
+ if SF_DEBUG:
243
+ print(
244
+ "[[patch_cherrypy]] NetworkHop & Exception tools globally enabled",
245
+ log=False,
246
+ )