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,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
• SFTracingASGIMiddleware: propagates SAILFISH_TRACING_HEADER inbound → ContextVar.
|
|
3
|
+
• SFTracingRoute: per-endpoint hop capture (user code only).
|
|
4
|
+
• patch_fastapi(): monkey-patch FastAPI.__init__ to inject both.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
8
|
+
from ...custom_excepthook import custom_excepthook
|
|
9
|
+
from ...env_vars import SF_DEBUG
|
|
10
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
11
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
12
|
+
from .utils import _is_user_code, _unwrap_user_func
|
|
13
|
+
|
|
14
|
+
# ---------- FastAPI / Starlette imports (guarded) ----------
|
|
15
|
+
try:
|
|
16
|
+
import fastapi
|
|
17
|
+
from fastapi.requests import Request
|
|
18
|
+
from fastapi.routing import APIRoute
|
|
19
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
20
|
+
except ImportError: # FastAPI not installed – expose no-op
|
|
21
|
+
|
|
22
|
+
def patch_fastapi():
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
else:
|
|
26
|
+
|
|
27
|
+
# ========================================================
|
|
28
|
+
# 1. Context-propagation middleware (already present)
|
|
29
|
+
# ========================================================
|
|
30
|
+
class SFTracingASGIMiddleware:
|
|
31
|
+
"""Fastest possible ASGI middleware (no BaseHTTPMiddleware).
|
|
32
|
+
|
|
33
|
+
• Captures inbound SAILFISH_TRACING_HEADER header → ContextVar
|
|
34
|
+
• Catches all unhandled exceptions and funnels them through
|
|
35
|
+
`custom_excepthook`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, app: ASGIApp):
|
|
39
|
+
self.app = app
|
|
40
|
+
|
|
41
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
42
|
+
# 1) Header capture (HTTP requests only)
|
|
43
|
+
if scope.get("type") == "http":
|
|
44
|
+
hdr = next(
|
|
45
|
+
(
|
|
46
|
+
val.decode()
|
|
47
|
+
for name, val in scope.get("headers", [])
|
|
48
|
+
if name.decode().lower() == SAILFISH_TRACING_HEADER.lower()
|
|
49
|
+
),
|
|
50
|
+
None,
|
|
51
|
+
)
|
|
52
|
+
if hdr:
|
|
53
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
54
|
+
|
|
55
|
+
# 2) Execute downstream app and trap unhandled exceptions
|
|
56
|
+
try:
|
|
57
|
+
await self.app(scope, receive, send)
|
|
58
|
+
except Exception as exc: # noqa: BLE001
|
|
59
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
60
|
+
raise # re-raise so FastAPI still returns a 500 response
|
|
61
|
+
|
|
62
|
+
# ========================================================
|
|
63
|
+
# 2. Hop-capturing APIRoute
|
|
64
|
+
# ========================================================
|
|
65
|
+
class SFTracingRoute(APIRoute):
|
|
66
|
+
"""
|
|
67
|
+
Custom APIRoute that
|
|
68
|
+
|
|
69
|
+
• fires a single NetworkHop when user-code starts (skips Strawberry), and
|
|
70
|
+
• funnels **any** exception – including FastAPI's HTTPException – through
|
|
71
|
+
`custom_excepthook` before letting FastAPI continue its normal handling.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
async def _emit_hop_if_needed(self, request: Request):
|
|
75
|
+
_, session_id = get_or_set_sf_trace_id() # already seeded by middleware
|
|
76
|
+
|
|
77
|
+
endpoint_fn = _unwrap_user_func(self.endpoint)
|
|
78
|
+
filename = getattr(endpoint_fn, "__code__", None).co_filename
|
|
79
|
+
|
|
80
|
+
# Ignore non-user / Strawberry endpoints
|
|
81
|
+
if (
|
|
82
|
+
not filename
|
|
83
|
+
or not _is_user_code(filename)
|
|
84
|
+
or endpoint_fn.__module__.startswith("strawberry")
|
|
85
|
+
):
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
line_no = endpoint_fn.__code__.co_firstlineno
|
|
89
|
+
name = endpoint_fn.__name__
|
|
90
|
+
|
|
91
|
+
if SF_DEBUG:
|
|
92
|
+
print(
|
|
93
|
+
f"[[SFTracingRoute]] hop → {name} ({filename}:{line_no}) "
|
|
94
|
+
f"session={session_id}",
|
|
95
|
+
log=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
NetworkHopsTransmitter().send(
|
|
99
|
+
session_id=session_id,
|
|
100
|
+
line=str(line_no),
|
|
101
|
+
column="0",
|
|
102
|
+
name=name,
|
|
103
|
+
entrypoint=filename,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------ #
|
|
107
|
+
# override FastAPI's handler factory so we can trap *all* exceptions
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
def get_route_handler(self):
|
|
110
|
+
original_route_handler = super().get_route_handler()
|
|
111
|
+
|
|
112
|
+
async def traced_route_handler(request: Request):
|
|
113
|
+
try:
|
|
114
|
+
await self._emit_hop_if_needed(request)
|
|
115
|
+
return await original_route_handler(request)
|
|
116
|
+
except Exception as exc: # <-- catches
|
|
117
|
+
custom_excepthook(type(exc), exc, exc.__traceback__) # all FastAPI
|
|
118
|
+
raise # exceptions
|
|
119
|
+
|
|
120
|
+
return traced_route_handler
|
|
121
|
+
|
|
122
|
+
# ========================================================
|
|
123
|
+
# 3. Monkey-patch FastAPI.__init__
|
|
124
|
+
# ========================================================
|
|
125
|
+
def patch_fastapi():
|
|
126
|
+
"""
|
|
127
|
+
• Inject SFTracingASGIMiddleware at app start-up.
|
|
128
|
+
• Force router.route_class = SFTracingRoute to wrap every endpoint.
|
|
129
|
+
"""
|
|
130
|
+
original_init = fastapi.FastAPI.__init__
|
|
131
|
+
|
|
132
|
+
def patched_init(self, *args, **kwargs):
|
|
133
|
+
# Let FastAPI do its normal work first.
|
|
134
|
+
original_init(self, *args, **kwargs)
|
|
135
|
+
# Insert ASGI middleware at the very top.
|
|
136
|
+
self.add_middleware(SFTracingASGIMiddleware)
|
|
137
|
+
# Ensure all new routes use our tracing route class.
|
|
138
|
+
self.router.route_class = SFTracingRoute
|
|
139
|
+
if SF_DEBUG:
|
|
140
|
+
print(
|
|
141
|
+
"[[patch_fastapi]] Tracing middleware + route class installed",
|
|
142
|
+
log=False,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
fastapi.FastAPI.__init__ = patched_init
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adds:
|
|
3
|
+
• before_request hook → ContextVar propagation (unchanged).
|
|
4
|
+
• global add_url_rule / Blueprint.add_url_rule patch →
|
|
5
|
+
wraps every endpoint in a hop-emitting closure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from types import MethodType
|
|
10
|
+
from typing import Callable, Set, Tuple
|
|
11
|
+
|
|
12
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
13
|
+
from ...custom_excepthook import custom_excepthook
|
|
14
|
+
from ...env_vars import SF_DEBUG
|
|
15
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
16
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
17
|
+
|
|
18
|
+
# ────────────────────────────────────────────────────────────────
|
|
19
|
+
# shared helpers
|
|
20
|
+
# ────────────────────────────────────────────────────────────────
|
|
21
|
+
from .utils import _is_user_code, _unwrap_user_func # cached helpers
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _make_hop_wrapper(
|
|
25
|
+
fn: Callable, hop_key: Tuple[str, int], fn_name: str, filename: str
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Return a wrapper that sends a NetworkHop exactly once per request.
|
|
29
|
+
Uses flask.g to remember which hops have fired.
|
|
30
|
+
"""
|
|
31
|
+
from flask import g
|
|
32
|
+
|
|
33
|
+
@wraps(fn)
|
|
34
|
+
def _wrapped(*args, **kwargs): # noqa: ANN001
|
|
35
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
36
|
+
|
|
37
|
+
sent: Set[Tuple[str, int]] = getattr(g, "_sf_hops_sent", set())
|
|
38
|
+
if hop_key not in sent:
|
|
39
|
+
if SF_DEBUG:
|
|
40
|
+
print(
|
|
41
|
+
f"[[SFTracingFlask]] hop → {fn_name} ({filename}:{hop_key[1]}) "
|
|
42
|
+
f"session={session_id}",
|
|
43
|
+
log=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
NetworkHopsTransmitter().send(
|
|
47
|
+
session_id=session_id,
|
|
48
|
+
line=str(hop_key[1]),
|
|
49
|
+
column="0",
|
|
50
|
+
name=fn_name,
|
|
51
|
+
entrypoint=filename,
|
|
52
|
+
)
|
|
53
|
+
sent.add(hop_key)
|
|
54
|
+
g._sf_hops_sent = sent
|
|
55
|
+
|
|
56
|
+
return fn(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return _wrapped
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _wrap_if_user_view(endpoint_fn: Callable):
|
|
62
|
+
"""
|
|
63
|
+
Decide whether to wrap `endpoint_fn`. Returns the (possibly wrapped)
|
|
64
|
+
callable. Suppress wrapping for library code or Strawberry handlers.
|
|
65
|
+
"""
|
|
66
|
+
real_fn = _unwrap_user_func(endpoint_fn)
|
|
67
|
+
|
|
68
|
+
# Skip Strawberry GraphQL views – Strawberry extension owns them
|
|
69
|
+
if real_fn.__module__.startswith("strawberry"):
|
|
70
|
+
return endpoint_fn
|
|
71
|
+
|
|
72
|
+
code = getattr(real_fn, "__code__", None)
|
|
73
|
+
if not code or not _is_user_code(code.co_filename):
|
|
74
|
+
return endpoint_fn
|
|
75
|
+
|
|
76
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
77
|
+
return _make_hop_wrapper(endpoint_fn, hop_key, real_fn.__name__, code.co_filename)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ────────────────────────────────────────────────────────────────
|
|
81
|
+
# Context-propagation (unchanged)
|
|
82
|
+
# ────────────────────────────────────────────────────────────────
|
|
83
|
+
def _install_before_request(app):
|
|
84
|
+
from flask import request
|
|
85
|
+
|
|
86
|
+
@app.before_request
|
|
87
|
+
def _extract_sf_trace():
|
|
88
|
+
hdr = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
89
|
+
if hdr:
|
|
90
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ────────────────────────────────────────────────────────────────
|
|
94
|
+
# Monkey-patch Flask & Blueprint
|
|
95
|
+
# ────────────────────────────────────────────────────────────────
|
|
96
|
+
try:
|
|
97
|
+
import flask
|
|
98
|
+
from flask import Blueprint
|
|
99
|
+
|
|
100
|
+
def _patch_add_url_rule(cls):
|
|
101
|
+
"""
|
|
102
|
+
Patch *cls*.add_url_rule (cls is Flask or Blueprint) so the final
|
|
103
|
+
stored view function is wrapped after Flask registers it. Works for:
|
|
104
|
+
• view_func positional
|
|
105
|
+
• endpoint string lookup
|
|
106
|
+
• CBV's as_view()
|
|
107
|
+
"""
|
|
108
|
+
original_add = cls.add_url_rule
|
|
109
|
+
|
|
110
|
+
def patched_add(
|
|
111
|
+
self, rule, endpoint=None, view_func=None, **options
|
|
112
|
+
): # noqa: ANN001
|
|
113
|
+
# 1. Let Flask register the route first
|
|
114
|
+
original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
|
|
115
|
+
|
|
116
|
+
# 2. Resolve the canonical endpoint name
|
|
117
|
+
ep_name = endpoint or (view_func and view_func.__name__)
|
|
118
|
+
if not ep_name:
|
|
119
|
+
return # should not happen, but be safe
|
|
120
|
+
|
|
121
|
+
target = self.view_functions.get(ep_name)
|
|
122
|
+
if not callable(target):
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# 3. Wrap if user code
|
|
126
|
+
wrapped = _wrap_if_user_view(target)
|
|
127
|
+
self.view_functions[ep_name] = wrapped
|
|
128
|
+
|
|
129
|
+
cls.add_url_rule = patched_add
|
|
130
|
+
|
|
131
|
+
def patch_flask():
|
|
132
|
+
"""
|
|
133
|
+
• Installs before_request header propagation
|
|
134
|
+
• Wraps every endpoint to emit a single NetworkHop
|
|
135
|
+
• **NEW:** patches Flask.handle_exception + handle_user_exception so ANY
|
|
136
|
+
exception—including flask.abort / HTTPException—triggers custom_excepthook.
|
|
137
|
+
"""
|
|
138
|
+
if getattr(flask.Flask, "__sf_tracing_patched__", False):
|
|
139
|
+
return # idempotent
|
|
140
|
+
|
|
141
|
+
# --- 1. Patch Flask.__init__ to add before_request every time -----------
|
|
142
|
+
original_flask_init = flask.Flask.__init__
|
|
143
|
+
|
|
144
|
+
def patched_init(self, *args, **kwargs):
|
|
145
|
+
original_flask_init(self, *args, **kwargs)
|
|
146
|
+
_install_before_request(self)
|
|
147
|
+
|
|
148
|
+
flask.Flask.__init__ = patched_init
|
|
149
|
+
|
|
150
|
+
# --- 2. Patch add_url_rule for both Flask and Blueprint -----------------
|
|
151
|
+
_patch_add_url_rule(flask.Flask)
|
|
152
|
+
_patch_add_url_rule(Blueprint)
|
|
153
|
+
|
|
154
|
+
# --- 3. Patch exception handlers once on the class ----------------------
|
|
155
|
+
_mw_path = "sf_veritas_exception_patch_applied"
|
|
156
|
+
if not getattr(flask.Flask, _mw_path, False):
|
|
157
|
+
orig_handle_exc = flask.Flask.handle_exception
|
|
158
|
+
orig_handle_user_exc = flask.Flask.handle_user_exception
|
|
159
|
+
|
|
160
|
+
def _patched_handle_exception(self, e):
|
|
161
|
+
custom_excepthook(type(e), e, e.__traceback__)
|
|
162
|
+
return orig_handle_exc(self, e)
|
|
163
|
+
|
|
164
|
+
def _patched_handle_user_exception(self, e):
|
|
165
|
+
custom_excepthook(type(e), e, e.__traceback__)
|
|
166
|
+
return orig_handle_user_exc(self, e)
|
|
167
|
+
|
|
168
|
+
flask.Flask.handle_exception = _patched_handle_exception # 500 errors
|
|
169
|
+
flask.Flask.handle_user_exception = (
|
|
170
|
+
_patched_handle_user_exception # HTTPExc.
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
setattr(flask.Flask, _mw_path, True)
|
|
174
|
+
|
|
175
|
+
flask.Flask.__sf_tracing_patched__ = True
|
|
176
|
+
|
|
177
|
+
if SF_DEBUG:
|
|
178
|
+
print(
|
|
179
|
+
"[[patch_flask]] tracing hooks + exception capture installed", log=False
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except ImportError: # Flask not installed
|
|
183
|
+
|
|
184
|
+
def patch_flask(): # noqa: D401
|
|
185
|
+
"""No-op when Flask is absent."""
|
|
186
|
+
return
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
4
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def patch_klein():
|
|
8
|
+
"""
|
|
9
|
+
Monkey-patch Klein.route so that every @app.route endpoint first
|
|
10
|
+
extracts SAILFISH_TRACING_HEADER and sets our ContextVar, then calls the user handler.
|
|
11
|
+
No-op if Klein isn't installed.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
import klein
|
|
15
|
+
except ImportError:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
original_route = klein.Klein.route
|
|
19
|
+
|
|
20
|
+
def patched_route(self, *args, **kwargs):
|
|
21
|
+
# Grab Klein's decorator for this pattern
|
|
22
|
+
original_decorator = original_route(self, *args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def new_decorator(fn):
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def wrapped(request, *f_args, **f_kwargs):
|
|
27
|
+
header = request.getHeader(SAILFISH_TRACING_HEADER)
|
|
28
|
+
if header:
|
|
29
|
+
get_or_set_sf_trace_id(
|
|
30
|
+
header, is_associated_with_inbound_request=True
|
|
31
|
+
)
|
|
32
|
+
# Now that our ContextVar is set, call the real handler
|
|
33
|
+
return fn(request, *f_args, **f_kwargs)
|
|
34
|
+
|
|
35
|
+
# Register the wrapped handler instead of the raw one
|
|
36
|
+
return original_decorator(wrapped)
|
|
37
|
+
|
|
38
|
+
return new_decorator
|
|
39
|
+
|
|
40
|
+
klein.Klein.route = patched_route
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
import sysconfig
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable, Optional, Set
|
|
6
|
+
|
|
7
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
8
|
+
from ...custom_excepthook import custom_excepthook
|
|
9
|
+
from ...env_vars import SF_DEBUG
|
|
10
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
11
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
12
|
+
from .utils import _is_user_code, _unwrap_user_func
|
|
13
|
+
|
|
14
|
+
_stdlib = sysconfig.get_paths()["stdlib"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=512)
|
|
18
|
+
def _is_user_code(path: Optional[str]) -> bool:
|
|
19
|
+
"""
|
|
20
|
+
True only for “application” files (not stdlib or site-packages).
|
|
21
|
+
"""
|
|
22
|
+
if not path or path.startswith("<"):
|
|
23
|
+
return False
|
|
24
|
+
if path.startswith(_stdlib):
|
|
25
|
+
return False
|
|
26
|
+
if "site-packages" in path or "dist-packages" in path:
|
|
27
|
+
return False
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sf_tracing_factory(app: Callable) -> Callable:
|
|
32
|
+
"""
|
|
33
|
+
ASGI middleware that
|
|
34
|
+
• propagates the inbound SAILFISH_TRACING_HEADER header, and
|
|
35
|
+
• reports any unhandled exception via `custom_excepthook`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
async def _middleware(scope, receive, send):
|
|
39
|
+
# Header propagation
|
|
40
|
+
if scope.get("type") == "http":
|
|
41
|
+
for name, val in scope.get("headers", []):
|
|
42
|
+
if name.decode().lower() == SAILFISH_TRACING_HEADER.lower():
|
|
43
|
+
get_or_set_sf_trace_id(
|
|
44
|
+
val.decode(), is_associated_with_inbound_request=True
|
|
45
|
+
)
|
|
46
|
+
break
|
|
47
|
+
# Exception capture
|
|
48
|
+
try:
|
|
49
|
+
await app(scope, receive, send)
|
|
50
|
+
except Exception as exc: # noqa: BLE001
|
|
51
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
return _middleware
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _sf_profile_factory(app: Callable) -> Callable:
|
|
58
|
+
"""
|
|
59
|
+
ASGI middleware that installs a one-shot profiler tracer to fire on the
|
|
60
|
+
first user-land Python call, then disables itself. Emits exactly one
|
|
61
|
+
NetworkHop per request.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def _make_tracer():
|
|
65
|
+
def tracer(frame, event, _arg):
|
|
66
|
+
try:
|
|
67
|
+
# Skip any C-level events instantly
|
|
68
|
+
if event.startswith("c_"):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# Only care about the first Python call into user code
|
|
72
|
+
if event == "call" and _is_user_code(frame.f_code.co_filename):
|
|
73
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
74
|
+
|
|
75
|
+
func_name = frame.f_code.co_name
|
|
76
|
+
line_no = frame.f_lineno
|
|
77
|
+
filename = frame.f_code.co_filename
|
|
78
|
+
|
|
79
|
+
if SF_DEBUG:
|
|
80
|
+
print(
|
|
81
|
+
f"[[LitestarProfile]] SEND → {func_name} "
|
|
82
|
+
f"({filename}:{line_no}) session={session_id}",
|
|
83
|
+
log=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
NetworkHopsTransmitter().send(
|
|
88
|
+
session_id=session_id,
|
|
89
|
+
line=str(line_no),
|
|
90
|
+
column="0",
|
|
91
|
+
name=func_name,
|
|
92
|
+
entrypoint=filename,
|
|
93
|
+
)
|
|
94
|
+
except Exception as send_err:
|
|
95
|
+
# Log send errors but don't break the request
|
|
96
|
+
print(
|
|
97
|
+
"[[LitestarProfile ERROR send]]", repr(send_err), log=False
|
|
98
|
+
)
|
|
99
|
+
finally:
|
|
100
|
+
# Turn off profiling immediately
|
|
101
|
+
sys.setprofile(None)
|
|
102
|
+
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
return tracer
|
|
106
|
+
except Exception as err:
|
|
107
|
+
# Any tracer error → disable profiling & log it
|
|
108
|
+
sys.setprofile(None)
|
|
109
|
+
print("[[LitestarProfile ERROR tracer]]", repr(err), log=False)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
return tracer
|
|
113
|
+
|
|
114
|
+
class _ProfileMiddleware:
|
|
115
|
+
def __init__(self, app: Callable):
|
|
116
|
+
self.app = app
|
|
117
|
+
|
|
118
|
+
async def __call__(self, scope, receive, send):
|
|
119
|
+
if scope.get("type") == "http":
|
|
120
|
+
# Install the one-shot tracer
|
|
121
|
+
sys.setprofile(_make_tracer())
|
|
122
|
+
try:
|
|
123
|
+
await self.app(scope, receive, send)
|
|
124
|
+
except Exception as exc: # noqa: BLE001
|
|
125
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
126
|
+
raise
|
|
127
|
+
finally:
|
|
128
|
+
sys.setprofile(None)
|
|
129
|
+
else:
|
|
130
|
+
try:
|
|
131
|
+
await self.app(scope, receive, send)
|
|
132
|
+
except Exception as exc: # noqa: BLE001
|
|
133
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
return _ProfileMiddleware(app)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def patch_litestar() -> None:
|
|
140
|
+
"""
|
|
141
|
+
Monkey-patch Litestar.__init__ so every instance auto-wraps with:
|
|
142
|
+
1) _sf_tracing_factory → inbound trace-header propagation
|
|
143
|
+
2) _sf_profile_factory → one-shot tracer for NetworkHops
|
|
144
|
+
Safe no-op if Litestar is not installed.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
import litestar
|
|
148
|
+
from litestar import Litestar
|
|
149
|
+
from litestar.middleware import DefineMiddleware
|
|
150
|
+
except ImportError:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
original_init = Litestar.__init__
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# UPDATED patched_init in patch_litestar.py (entire function shown)
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
def patched_init(self, *args, **kwargs):
|
|
159
|
+
"""
|
|
160
|
+
Injects Sailfish into every Litestar app instance by
|
|
161
|
+
|
|
162
|
+
1. Pre-pending two ASGI middlewares
|
|
163
|
+
• _sf_tracing_factory – header propagation + last-chance catcher
|
|
164
|
+
• _sf_profile_factory – one-shot hop emitter
|
|
165
|
+
2. Adding a **generic exception handler** so *any* exception—
|
|
166
|
+
including `HTTPException` and framework-level errors—triggers
|
|
167
|
+
`custom_excepthook` exactly once before Litestar builds the
|
|
168
|
+
response.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------- #
|
|
172
|
+
# 1 | Middleware injection (existing behaviour)
|
|
173
|
+
# ---------------------------------------------------------- #
|
|
174
|
+
mw = list(kwargs.get("middleware", []))
|
|
175
|
+
from litestar.middleware import DefineMiddleware
|
|
176
|
+
|
|
177
|
+
mw.insert(0, DefineMiddleware(_sf_tracing_factory))
|
|
178
|
+
mw.insert(1, DefineMiddleware(_sf_profile_factory))
|
|
179
|
+
kwargs["middleware"] = mw
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------- #
|
|
182
|
+
# 2 | Universal exception handler
|
|
183
|
+
# ---------------------------------------------------------- #
|
|
184
|
+
def _sf_exception_handler(request, exc): # type: ignore[valid-type]
|
|
185
|
+
"""
|
|
186
|
+
Litestar calls this for **any** Exception once routing / dep-
|
|
187
|
+
resolution is done. We just forward to `custom_excepthook`
|
|
188
|
+
and re-raise so the builtin handler still produces a Response.
|
|
189
|
+
"""
|
|
190
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
191
|
+
raise exc # let Litestar fall back to its default logic
|
|
192
|
+
|
|
193
|
+
# Merge with user-supplied handlers (if any)
|
|
194
|
+
existing_handlers = kwargs.get("exception_handlers", {})
|
|
195
|
+
if isinstance(existing_handlers, dict):
|
|
196
|
+
existing_handlers.setdefault(Exception, _sf_exception_handler)
|
|
197
|
+
else: # Litestar also accepts list[tuple[Exception, Handler]]
|
|
198
|
+
existing_handlers = list(existing_handlers) # type: ignore[arg-type]
|
|
199
|
+
existing_handlers.append((Exception, _sf_exception_handler))
|
|
200
|
+
kwargs["exception_handlers"] = existing_handlers
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------- #
|
|
203
|
+
# 3 | Debug log
|
|
204
|
+
# ---------------------------------------------------------- #
|
|
205
|
+
if SF_DEBUG:
|
|
206
|
+
print(
|
|
207
|
+
"[[patch_litestar]] installed header+profile middleware AND "
|
|
208
|
+
"global exception handler",
|
|
209
|
+
log=False,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------- #
|
|
213
|
+
# 4 | Delegate to original __init__
|
|
214
|
+
# ---------------------------------------------------------- #
|
|
215
|
+
return original_init(self, *args, **kwargs)
|
|
216
|
+
|
|
217
|
+
Litestar.__init__ = patched_init
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
import sysconfig
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable, Set, Tuple
|
|
6
|
+
|
|
7
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
8
|
+
from ...custom_excepthook import custom_excepthook
|
|
9
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
10
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
11
|
+
from .utils import _is_user_code # cached helpers
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ------------------------------------------------------------------ #
|
|
15
|
+
# 1.2 Tween factory: header + one-shot profile tracer + exceptions #
|
|
16
|
+
# ------------------------------------------------------------------ #
|
|
17
|
+
def _sf_tracing_tween_factory(handler, registry):
|
|
18
|
+
"""
|
|
19
|
+
Pyramid tween that:
|
|
20
|
+
• Reads SAILFISH_TRACING_HEADER header → ContextVar.
|
|
21
|
+
• Sets a one-shot profiler to emit the first user-land NetworkHop.
|
|
22
|
+
• Funnels *all* exceptions (including HTTPException) through
|
|
23
|
+
`custom_excepthook` before letting Pyramid continue normal handling.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def _tween(request):
|
|
27
|
+
# ── 1) Propagate incoming trace header ──────────────────────────
|
|
28
|
+
hdr = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
29
|
+
if hdr:
|
|
30
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
31
|
+
|
|
32
|
+
# ── 2) One-shot tracer to detect first user-code frame ──────────
|
|
33
|
+
def tracer(frame, event, _arg):
|
|
34
|
+
if event != "call": # only Python calls
|
|
35
|
+
return tracer
|
|
36
|
+
fn_path = frame.f_code.co_filename
|
|
37
|
+
if _is_user_code(fn_path):
|
|
38
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
39
|
+
func_name = frame.f_code.co_name
|
|
40
|
+
line_no = frame.f_lineno
|
|
41
|
+
NetworkHopsTransmitter().send(
|
|
42
|
+
session_id=session_id,
|
|
43
|
+
line=str(line_no),
|
|
44
|
+
column="0",
|
|
45
|
+
name=func_name,
|
|
46
|
+
entrypoint=fn_path,
|
|
47
|
+
)
|
|
48
|
+
sys.setprofile(None) # disable after first hop
|
|
49
|
+
return None
|
|
50
|
+
return tracer
|
|
51
|
+
|
|
52
|
+
sys.setprofile(tracer)
|
|
53
|
+
|
|
54
|
+
# ── 3) Call downstream handler & capture **all** exceptions ─────
|
|
55
|
+
try:
|
|
56
|
+
return handler(request)
|
|
57
|
+
except Exception as exc: # HTTPException included
|
|
58
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
59
|
+
raise # re-raise for Pyramid
|
|
60
|
+
finally:
|
|
61
|
+
sys.setprofile(None) # safety-net cleanup
|
|
62
|
+
|
|
63
|
+
return _tween
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------ #
|
|
67
|
+
# 1.3 Monkey-patch Configurator to auto-add our tween #
|
|
68
|
+
# ------------------------------------------------------------------ #
|
|
69
|
+
def patch_pyramid():
|
|
70
|
+
"""
|
|
71
|
+
Ensure every Pyramid Configurator implicitly registers our tween
|
|
72
|
+
at the INVOCATION stage (just above MAIN).
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
import pyramid.config
|
|
76
|
+
import pyramid.tweens
|
|
77
|
+
except ImportError:
|
|
78
|
+
return # Pyramid not installed
|
|
79
|
+
|
|
80
|
+
original_init = pyramid.config.Configurator.__init__
|
|
81
|
+
|
|
82
|
+
def patched_init(self, *args, **kwargs):
|
|
83
|
+
original_init(self, *args, **kwargs)
|
|
84
|
+
# Use a dotted name—implicit ordering places it just above MAIN
|
|
85
|
+
dotted = f"{_sf_tracing_tween_factory.__module__}._sf_tracing_tween_factory"
|
|
86
|
+
# 'over=pyramid.tweens.MAIN' ensures our tween runs *before* the main handler
|
|
87
|
+
self.add_tween(dotted, over=pyramid.tweens.MAIN)
|
|
88
|
+
|
|
89
|
+
pyramid.config.Configurator.__init__ = patched_init
|