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.
- sf_veritas/.gitignore +2 -0
- sf_veritas/__init__.py +4 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +3 -0
- sf_veritas/custom_excepthook.py +285 -0
- sf_veritas/custom_log_handler.py +53 -0
- sf_veritas/custom_output_wrapper.py +107 -0
- sf_veritas/custom_print.py +34 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +83 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +252 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +51 -0
- sf_veritas/patches/network_libraries/aiohttp.py +100 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +93 -0
- sf_veritas/patches/network_libraries/http_client.py +64 -0
- sf_veritas/patches/network_libraries/httpcore.py +152 -0
- sf_veritas/patches/network_libraries/httplib2.py +76 -0
- sf_veritas/patches/network_libraries/httpx.py +123 -0
- sf_veritas/patches/network_libraries/niquests.py +192 -0
- sf_veritas/patches/network_libraries/pycurl.py +71 -0
- sf_veritas/patches/network_libraries/requests.py +187 -0
- sf_veritas/patches/network_libraries/tornado.py +139 -0
- sf_veritas/patches/network_libraries/treq.py +122 -0
- sf_veritas/patches/network_libraries/urllib_request.py +129 -0
- sf_veritas/patches/network_libraries/utils.py +101 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +32 -0
- sf_veritas/patches/web_frameworks/__init__.py +45 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +133 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +132 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +107 -0
- sf_veritas/patches/web_frameworks/bottle.py +142 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +246 -0
- sf_veritas/patches/web_frameworks/django.py +307 -0
- sf_veritas/patches/web_frameworks/eve.py +138 -0
- sf_veritas/patches/web_frameworks/falcon.py +229 -0
- sf_veritas/patches/web_frameworks/fastapi.py +145 -0
- sf_veritas/patches/web_frameworks/flask.py +186 -0
- sf_veritas/patches/web_frameworks/klein.py +40 -0
- sf_veritas/patches/web_frameworks/litestar.py +217 -0
- sf_veritas/patches/web_frameworks/pyramid.py +89 -0
- sf_veritas/patches/web_frameworks/quart.py +155 -0
- sf_veritas/patches/web_frameworks/robyn.py +114 -0
- sf_veritas/patches/web_frameworks/sanic.py +120 -0
- sf_veritas/patches/web_frameworks/starlette.py +144 -0
- sf_veritas/patches/web_frameworks/strawberry.py +269 -0
- sf_veritas/patches/web_frameworks/tornado.py +129 -0
- sf_veritas/patches/web_frameworks/utils.py +55 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +358 -0
- sf_veritas/request_interceptor.py +399 -0
- sf_veritas/request_utils.py +104 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +436 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +58 -0
- sf_veritas/types.py +44 -0
- sf_veritas/unified_interceptor.py +323 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.9.7.dist-info/METADATA +83 -0
- sf_veritas-0.9.7.dist-info/RECORD +86 -0
- sf_veritas-0.9.7.dist-info/WHEEL +4 -0
- 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
|
+
)
|