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,307 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
6
|
+
except ImportError:
|
|
7
|
+
MiddlewareMixin = object # fallback for non-Django environments
|
|
8
|
+
|
|
9
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
10
|
+
from ...custom_excepthook import custom_excepthook
|
|
11
|
+
from ...env_vars import PRINT_CONFIGURATION_STATUSES, SF_DEBUG
|
|
12
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
13
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_and_modify_output_wrapper():
|
|
17
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
18
|
+
print("find_and_modify_output_wrapper", log=False)
|
|
19
|
+
try:
|
|
20
|
+
import django
|
|
21
|
+
import django.core.management.base
|
|
22
|
+
|
|
23
|
+
base_path = inspect.getfile(django.core.management.base)
|
|
24
|
+
setup_code = """
|
|
25
|
+
from sf_veritas.custom_output_wrapper import get_custom_output_wrapper_django
|
|
26
|
+
|
|
27
|
+
get_custom_output_wrapper_django()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
with open(base_path, "r+") as f:
|
|
31
|
+
content = f.read()
|
|
32
|
+
if "get_custom_output_wrapper_django()" not in content:
|
|
33
|
+
f.write("\n" + setup_code)
|
|
34
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
35
|
+
print("Custom output wrapper injected into Django.", log=False)
|
|
36
|
+
except ModuleNotFoundError:
|
|
37
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
38
|
+
print(
|
|
39
|
+
"Django not found; skipping output-wrapper injection",
|
|
40
|
+
log=False,
|
|
41
|
+
)
|
|
42
|
+
except PermissionError:
|
|
43
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
44
|
+
print(
|
|
45
|
+
"Permission error injecting output wrapper; skipping",
|
|
46
|
+
log=False,
|
|
47
|
+
)
|
|
48
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
49
|
+
print("find_and_modify_output_wrapper...DONE", log=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SailfishMiddleware(MiddlewareMixin):
|
|
53
|
+
"""
|
|
54
|
+
• process_request – capture inbound SAILFISH_TRACING_HEADER header.
|
|
55
|
+
• process_view – emit one NetworkHop per view (skip Strawberry).
|
|
56
|
+
• __call__ override – last-chance catcher for uncaught exceptions.
|
|
57
|
+
• got_request_exception signal – main hook for 500-level errors.
|
|
58
|
+
• process_exception – fallback for view-raised exceptions.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------ #
|
|
62
|
+
# 0 | Signal registration (called once at server start-up)
|
|
63
|
+
# ------------------------------------------------------------------ #
|
|
64
|
+
def __init__(self, get_response):
|
|
65
|
+
super().__init__(get_response)
|
|
66
|
+
|
|
67
|
+
# Attach to Django's global exception signal so we ALWAYS
|
|
68
|
+
# see real exceptions that become HTTP-500 responses.
|
|
69
|
+
from django.core.signals import got_request_exception
|
|
70
|
+
|
|
71
|
+
got_request_exception.disconnect( # avoid dupes on reload
|
|
72
|
+
self._on_exception_signal, dispatch_uid="sf_veritas_signal"
|
|
73
|
+
)
|
|
74
|
+
got_request_exception.connect(
|
|
75
|
+
self._on_exception_signal,
|
|
76
|
+
weak=False,
|
|
77
|
+
dispatch_uid="sf_veritas_signal",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------ #
|
|
81
|
+
# 1 | Signal handler ← FIXED
|
|
82
|
+
# ------------------------------------------------------------------ #
|
|
83
|
+
def _on_exception_signal(self, sender, request, **kwargs):
|
|
84
|
+
"""
|
|
85
|
+
Handle django.core.signals.got_request_exception.
|
|
86
|
+
|
|
87
|
+
The signal doesn't pass the exception object; per Django's own
|
|
88
|
+
implementation (and Sentry's approach) we fetch it from
|
|
89
|
+
sys.exc_info().
|
|
90
|
+
"""
|
|
91
|
+
import sys
|
|
92
|
+
|
|
93
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
94
|
+
|
|
95
|
+
if SF_DEBUG:
|
|
96
|
+
print(
|
|
97
|
+
f"[[SailfishMiddleware._on_exception_signal]] "
|
|
98
|
+
f"exc_value={exc_value!r}",
|
|
99
|
+
log=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if exc_value:
|
|
103
|
+
custom_excepthook(exc_type, exc_value, exc_tb)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------ #
|
|
106
|
+
# 2 | Last-chance wrapper (rarely triggered in WSGI but free)
|
|
107
|
+
# ------------------------------------------------------------------ #
|
|
108
|
+
def __call__(self, request):
|
|
109
|
+
try:
|
|
110
|
+
return super().__call__(request)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
113
|
+
raise # preserve default Django 500
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------ #
|
|
116
|
+
# 3 | Header capture
|
|
117
|
+
# ------------------------------------------------------------------ #
|
|
118
|
+
def process_request(self, request):
|
|
119
|
+
header_key = f"HTTP_{SAILFISH_TRACING_HEADER.upper().replace('-', '_')}"
|
|
120
|
+
inbound = request.META.get(header_key)
|
|
121
|
+
get_or_set_sf_trace_id(inbound, is_associated_with_inbound_request=True)
|
|
122
|
+
if SF_DEBUG:
|
|
123
|
+
print(
|
|
124
|
+
f"[[SailfishMiddleware.process_request]] "
|
|
125
|
+
f"key={header_key}, inbound={inbound}",
|
|
126
|
+
log=False,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# ------------------------------------------------------------------ #
|
|
130
|
+
# 4 | Network-hop emission (unchanged)
|
|
131
|
+
# ------------------------------------------------------------------ #
|
|
132
|
+
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
133
|
+
module = getattr(view_func, "__module__", "")
|
|
134
|
+
if module.startswith("strawberry"):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
code = getattr(view_func, "__code__", None)
|
|
138
|
+
if not code:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
fname, lno = code.co_filename, code.co_firstlineno
|
|
142
|
+
hop_key = (fname, lno)
|
|
143
|
+
|
|
144
|
+
sent = getattr(request, "_sf_hops_sent", set())
|
|
145
|
+
if hop_key not in sent:
|
|
146
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
147
|
+
NetworkHopsTransmitter().send(
|
|
148
|
+
session_id=session_id,
|
|
149
|
+
line=str(lno),
|
|
150
|
+
column="0",
|
|
151
|
+
name=view_func.__name__,
|
|
152
|
+
entrypoint=fname,
|
|
153
|
+
)
|
|
154
|
+
sent.add(hop_key)
|
|
155
|
+
setattr(request, "_sf_hops_sent", sent)
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------ #
|
|
158
|
+
# 5 | View-level exception hook (unchanged)
|
|
159
|
+
# ------------------------------------------------------------------ #
|
|
160
|
+
def process_exception(self, request, exception):
|
|
161
|
+
print("[[SailfishMiddleware.process_exception]]", log=False)
|
|
162
|
+
custom_excepthook(type(exception), exception, exception.__traceback__)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------------- #
|
|
166
|
+
# Helper – patch django.core.wsgi.get_wsgi_application once
|
|
167
|
+
# --------------------------------------------------------------------------- #
|
|
168
|
+
# --------------------------------------------------------------------------- #
|
|
169
|
+
# Helper – patch django.core.wsgi.get_wsgi_application once
|
|
170
|
+
# --------------------------------------------------------------------------- #
|
|
171
|
+
def _patch_get_wsgi_application() -> None:
|
|
172
|
+
"""
|
|
173
|
+
Replace ``django.core.wsgi.get_wsgi_application`` with a wrapper that:
|
|
174
|
+
|
|
175
|
+
1. Runs ``django.setup()`` (as the original does),
|
|
176
|
+
2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
|
|
177
|
+
*after* settings are configured but *before* the first ``WSGIHandler``
|
|
178
|
+
is built,
|
|
179
|
+
3. Wraps the returned handler in our ``CustomExceptionMiddleware`` so we
|
|
180
|
+
still have a last-chance catcher outside Django's stack.
|
|
181
|
+
|
|
182
|
+
This mirrors the flow used by Sentry's Django integration.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
from django.core import wsgi as _wsgi_mod
|
|
186
|
+
except ImportError: # pragma: no cover
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if getattr(_wsgi_mod, "_sf_patched", False):
|
|
190
|
+
return # idempotent
|
|
191
|
+
|
|
192
|
+
_orig_get_wsgi = _wsgi_mod.get_wsgi_application
|
|
193
|
+
_MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
|
|
194
|
+
|
|
195
|
+
def _sf_get_wsgi_application(*args, **kwargs):
|
|
196
|
+
# --- Step 1: exactly replicate original behaviour -----------------
|
|
197
|
+
import django
|
|
198
|
+
|
|
199
|
+
django.setup(set_prefix=False) # configures settings & apps
|
|
200
|
+
|
|
201
|
+
# --- Step 2: inject middleware *now* (settings are configured) ----
|
|
202
|
+
from django.conf import settings
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
hasattr(settings, "MIDDLEWARE")
|
|
206
|
+
and isinstance(settings.MIDDLEWARE, list)
|
|
207
|
+
and _MW_PATH not in settings.MIDDLEWARE
|
|
208
|
+
):
|
|
209
|
+
settings.MIDDLEWARE.insert(0, _MW_PATH)
|
|
210
|
+
|
|
211
|
+
# --- Step 3: build handler and wrap for last-chance exceptions ----
|
|
212
|
+
from django.core.handlers.wsgi import WSGIHandler
|
|
213
|
+
from sf_veritas.patches.web_frameworks.django import CustomExceptionMiddleware
|
|
214
|
+
|
|
215
|
+
handler = WSGIHandler()
|
|
216
|
+
return CustomExceptionMiddleware(handler)
|
|
217
|
+
|
|
218
|
+
_wsgi_mod.get_wsgi_application = _sf_get_wsgi_application
|
|
219
|
+
_wsgi_mod._sf_patched = True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def patch_django_middleware() -> None:
|
|
223
|
+
"""
|
|
224
|
+
Public entry-point called by ``setup_interceptors``.
|
|
225
|
+
|
|
226
|
+
• Inserts ``SailfishMiddleware`` for *already-configured* settings
|
|
227
|
+
(run-server or ASGI).
|
|
228
|
+
• Patches ``get_wsgi_application`` so *future* WSGI handlers created
|
|
229
|
+
by third-party code inherit the middleware without relying on a
|
|
230
|
+
configured settings object at import time.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
from django.conf import settings
|
|
235
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
236
|
+
except ImportError: # Django not installed
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
_MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
|
|
240
|
+
|
|
241
|
+
# ---------- If settings are *already* configured, patch immediately ---
|
|
242
|
+
try:
|
|
243
|
+
if settings.configured and isinstance(
|
|
244
|
+
getattr(settings, "MIDDLEWARE", None), list
|
|
245
|
+
):
|
|
246
|
+
if _MW_PATH not in settings.MIDDLEWARE:
|
|
247
|
+
settings.MIDDLEWARE.insert(0, _MW_PATH)
|
|
248
|
+
except ImproperlyConfigured:
|
|
249
|
+
# Settings not yet configured – safe to ignore; the WSGI patch below
|
|
250
|
+
# will handle insertion once ``django.setup()`` runs.
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
# ---------- Always patch get_wsgi_application (idempotent) ------------
|
|
254
|
+
_patch_get_wsgi_application()
|
|
255
|
+
|
|
256
|
+
if SF_DEBUG:
|
|
257
|
+
print(
|
|
258
|
+
"[[patch_django_middleware]] Sailfish Django integration ready", log=False
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class CustomExceptionMiddleware:
|
|
263
|
+
"""
|
|
264
|
+
A universal last-chance exception wrapper that works for either
|
|
265
|
+
• ASGI call signature: (scope, receive, send) → coroutine
|
|
266
|
+
• WSGI call signature: (environ, start_response) → iterable
|
|
267
|
+
Every un-handled exception is funneled through ``custom_excepthook`` once.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, app):
|
|
271
|
+
self.app = app
|
|
272
|
+
|
|
273
|
+
# ------------------------------------------------------------------ #
|
|
274
|
+
# Dispatcher – routes ASGI vs WSGI based on arity / argument shape
|
|
275
|
+
# ------------------------------------------------------------------ #
|
|
276
|
+
def __call__(self, *args, **kwargs):
|
|
277
|
+
if len(args) == 3:
|
|
278
|
+
# Heuristic: (scope, receive, send) for ASGI
|
|
279
|
+
return self._asgi_call(*args) # returns coroutine
|
|
280
|
+
# Else assume classic WSGI: (environ, start_response)
|
|
281
|
+
return self._wsgi_call(*args) # returns iterable
|
|
282
|
+
|
|
283
|
+
# ------------------------------------------------------------------ #
|
|
284
|
+
# ASGI branch
|
|
285
|
+
# ------------------------------------------------------------------ #
|
|
286
|
+
async def _asgi_call(self, scope, receive, send):
|
|
287
|
+
try:
|
|
288
|
+
await self.app(scope, receive, send)
|
|
289
|
+
except Exception as exc: # noqa: BLE001
|
|
290
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------ #
|
|
294
|
+
# WSGI branch
|
|
295
|
+
# ------------------------------------------------------------------ #
|
|
296
|
+
def _wsgi_call(self, environ, start_response):
|
|
297
|
+
try:
|
|
298
|
+
return self.app(environ, start_response)
|
|
299
|
+
except Exception as exc: # noqa: BLE001
|
|
300
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
301
|
+
raise
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------ #
|
|
304
|
+
# Delegate attribute access so the wrapped app still behaves normally
|
|
305
|
+
# ------------------------------------------------------------------ #
|
|
306
|
+
def __getattr__(self, attr):
|
|
307
|
+
return getattr(self.app, attr)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Callable, Set, Tuple
|
|
3
|
+
|
|
4
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
5
|
+
from ...env_vars import SF_DEBUG
|
|
6
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
7
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
8
|
+
from .utils import _is_user_code, _unwrap_user_func # shared helpers
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ──────────────────────────────────────────────────────────────
|
|
12
|
+
# Header propagation (still one before_request handler)
|
|
13
|
+
# ──────────────────────────────────────────────────────────────
|
|
14
|
+
def _install_header_middleware(app):
|
|
15
|
+
from flask import request
|
|
16
|
+
|
|
17
|
+
@app.before_request
|
|
18
|
+
def _extract_sf_header():
|
|
19
|
+
rid = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
20
|
+
if rid:
|
|
21
|
+
get_or_set_sf_trace_id(rid, is_associated_with_inbound_request=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ──────────────────────────────────────────────────────────────
|
|
25
|
+
# Per-view hop wrapper
|
|
26
|
+
# ──────────────────────────────────────────────────────────────
|
|
27
|
+
def _hop_wrapper(view_fn: Callable):
|
|
28
|
+
"""
|
|
29
|
+
Return a wrapped callable that fires NetworkHopsTransmitter.send()
|
|
30
|
+
once per request (de-duped via flask.g).
|
|
31
|
+
"""
|
|
32
|
+
from flask import g
|
|
33
|
+
|
|
34
|
+
real_fn = _unwrap_user_func(view_fn)
|
|
35
|
+
|
|
36
|
+
# Skip Strawberry handlers – handled by Strawberry extension
|
|
37
|
+
if real_fn.__module__.startswith("strawberry"):
|
|
38
|
+
return view_fn
|
|
39
|
+
|
|
40
|
+
code = getattr(real_fn, "__code__", None)
|
|
41
|
+
if not code or not _is_user_code(code.co_filename):
|
|
42
|
+
return view_fn
|
|
43
|
+
|
|
44
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
45
|
+
fn_name = real_fn.__name__
|
|
46
|
+
filename = code.co_filename
|
|
47
|
+
line_no = code.co_firstlineno
|
|
48
|
+
|
|
49
|
+
@wraps(view_fn)
|
|
50
|
+
def _wrapped(*args, **kwargs):
|
|
51
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
52
|
+
|
|
53
|
+
sent: Set[Tuple[str, int]] = getattr(g, "_sf_hops_sent", set())
|
|
54
|
+
if hop_key not in sent:
|
|
55
|
+
if SF_DEBUG:
|
|
56
|
+
print(
|
|
57
|
+
f"[[SFTracingEve]] hop → {fn_name} "
|
|
58
|
+
f"({filename}:{line_no}) session={session_id}",
|
|
59
|
+
log=False,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
NetworkHopsTransmitter().send(
|
|
63
|
+
session_id=session_id,
|
|
64
|
+
line=str(line_no),
|
|
65
|
+
column="0",
|
|
66
|
+
name=fn_name,
|
|
67
|
+
entrypoint=filename,
|
|
68
|
+
)
|
|
69
|
+
sent.add(hop_key)
|
|
70
|
+
g._sf_hops_sent = sent
|
|
71
|
+
|
|
72
|
+
return view_fn(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
return _wrapped
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _patch_add_url_rule(cls):
|
|
78
|
+
"""
|
|
79
|
+
Patch add_url_rule on *cls* (cls is Eve or Blueprint) so that the final
|
|
80
|
+
stored endpoint function is wrapped *after* Flask has done its own
|
|
81
|
+
bookkeeping. This catches:
|
|
82
|
+
• Eve resource endpoints created internally via register_resource()
|
|
83
|
+
• Manual @app.route() decorators
|
|
84
|
+
• Blueprints, CBVs, etc.
|
|
85
|
+
"""
|
|
86
|
+
original_add = cls.add_url_rule
|
|
87
|
+
|
|
88
|
+
def patched_add(
|
|
89
|
+
self, rule, endpoint=None, view_func=None, **options
|
|
90
|
+
): # noqa: ANN001
|
|
91
|
+
# let Eve/Flask register the route first
|
|
92
|
+
original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
|
|
93
|
+
|
|
94
|
+
ep = endpoint or (view_func and view_func.__name__)
|
|
95
|
+
if not ep: # defensive
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
target = self.view_functions.get(ep)
|
|
99
|
+
if callable(target):
|
|
100
|
+
self.view_functions[ep] = _hop_wrapper(target)
|
|
101
|
+
|
|
102
|
+
cls.add_url_rule = patched_add
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ──────────────────────────────────────────────────────────────
|
|
106
|
+
# Public entry-point
|
|
107
|
+
# ──────────────────────────────────────────────────────────────
|
|
108
|
+
def patch_eve():
|
|
109
|
+
"""
|
|
110
|
+
• Adds ContextVar propagation middleware
|
|
111
|
+
• Wraps every Eve endpoint (and Blueprint endpoints) to emit one hop
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
import eve
|
|
115
|
+
from flask import Blueprint # Eve relies on Flask blueprints
|
|
116
|
+
except ImportError:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Guard against double-patching
|
|
120
|
+
if getattr(eve.Eve, "__sf_tracing_patched__", False):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# 1. Patch Eve.add_url_rule *and* Blueprint.add_url_rule
|
|
124
|
+
_patch_add_url_rule(eve.Eve)
|
|
125
|
+
_patch_add_url_rule(Blueprint)
|
|
126
|
+
|
|
127
|
+
# 2. Patch Eve.__init__ to install before_request middleware
|
|
128
|
+
original_init = eve.Eve.__init__
|
|
129
|
+
|
|
130
|
+
def patched_init(self, *args, **kwargs):
|
|
131
|
+
original_init(self, *args, **kwargs)
|
|
132
|
+
_install_header_middleware(self)
|
|
133
|
+
|
|
134
|
+
eve.Eve.__init__ = patched_init
|
|
135
|
+
eve.Eve.__sf_tracing_patched__ = True
|
|
136
|
+
|
|
137
|
+
if SF_DEBUG:
|
|
138
|
+
print("[[patch_eve]] header middleware + hop wrapper installed", log=False)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
• SFTracingFalconMiddleware – propagates SAILFISH_TRACING_HEADER → ContextVar.
|
|
3
|
+
• per-responder wrapper – emits ONE NetworkHop per request for
|
|
4
|
+
user-land Falcon responders (sync & async), skipping Strawberry.
|
|
5
|
+
• patch_falcon() – monkey-patches both falcon.App (WSGI) and
|
|
6
|
+
falcon.asgi.App (ASGI) so the above logic is automatic.
|
|
7
|
+
|
|
8
|
+
This patch adds <1 µs overhead per request on CPython 3.11.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import inspect
|
|
15
|
+
from types import MethodType
|
|
16
|
+
from typing import Any, Callable, List, Set, Tuple
|
|
17
|
+
|
|
18
|
+
from ...constants import SAILFISH_TRACING_HEADER
|
|
19
|
+
from ...custom_excepthook import custom_excepthook
|
|
20
|
+
from ...env_vars import SF_DEBUG
|
|
21
|
+
from ...regular_data_transmitter import NetworkHopsTransmitter
|
|
22
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
23
|
+
from .utils import _is_user_code, _unwrap_user_func # shared helpers
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# 1 | Context-propagation middleware
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SFTracingFalconMiddleware:
|
|
31
|
+
"""Works for BOTH WSGI and ASGI flavours of Falcon."""
|
|
32
|
+
|
|
33
|
+
# synchronous apps
|
|
34
|
+
def process_request(self, req, resp): # noqa: D401
|
|
35
|
+
hdr = req.get_header(SAILFISH_TRACING_HEADER)
|
|
36
|
+
if hdr:
|
|
37
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
38
|
+
|
|
39
|
+
# asynchronous apps
|
|
40
|
+
async def process_request_async(self, req, resp): # noqa: D401
|
|
41
|
+
hdr = req.get_header(SAILFISH_TRACING_HEADER)
|
|
42
|
+
if hdr:
|
|
43
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# 2 | Hop-emission helper
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _hop_once(
|
|
52
|
+
req, hop_key: Tuple[str, int], fname: str, lno: int, responder_name: str
|
|
53
|
+
) -> None:
|
|
54
|
+
sent: Set[Tuple[str, int]] = getattr(req.context, "_sf_hops_sent", set())
|
|
55
|
+
if hop_key in sent:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
59
|
+
if SF_DEBUG:
|
|
60
|
+
print(
|
|
61
|
+
f"[[FalconHop]] {responder_name} ({fname}:{lno}) session={session_id}",
|
|
62
|
+
log=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
NetworkHopsTransmitter().send(
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
line=str(lno),
|
|
68
|
+
column="0",
|
|
69
|
+
name=responder_name,
|
|
70
|
+
entrypoint=fname,
|
|
71
|
+
)
|
|
72
|
+
sent.add(hop_key)
|
|
73
|
+
req.context._sf_hops_sent = sent
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_wrapper(base_fn: Callable) -> Callable:
|
|
77
|
+
"""Return a hop-emitting, exception-capturing wrapper around *base_fn*."""
|
|
78
|
+
|
|
79
|
+
real_fn = _unwrap_user_func(base_fn)
|
|
80
|
+
|
|
81
|
+
# Ignore non-user and Strawberry handlers
|
|
82
|
+
if real_fn.__module__.startswith("strawberry") or not _is_user_code(
|
|
83
|
+
real_fn.__code__.co_filename
|
|
84
|
+
):
|
|
85
|
+
return base_fn
|
|
86
|
+
|
|
87
|
+
fname = real_fn.__code__.co_filename
|
|
88
|
+
lno = real_fn.__code__.co_firstlineno
|
|
89
|
+
hop_key = (fname, lno)
|
|
90
|
+
responder_name = real_fn.__name__
|
|
91
|
+
|
|
92
|
+
# ---------------- asynchronous responders ------------------------- #
|
|
93
|
+
if inspect.iscoroutinefunction(base_fn):
|
|
94
|
+
|
|
95
|
+
async def _async_wrapped(self, req, resp, *args, **kwargs): # noqa: D401
|
|
96
|
+
_hop_once(req, hop_key, fname, lno, responder_name)
|
|
97
|
+
try:
|
|
98
|
+
return await base_fn(self, req, resp, *args, **kwargs)
|
|
99
|
+
except Exception as exc: # catches falcon.HTTPError too
|
|
100
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
return _async_wrapped
|
|
104
|
+
|
|
105
|
+
# ---------------- synchronous responders -------------------------- #
|
|
106
|
+
def _sync_wrapped(self, req, resp, *args, **kwargs): # noqa: D401
|
|
107
|
+
_hop_once(req, hop_key, fname, lno, responder_name)
|
|
108
|
+
try:
|
|
109
|
+
return base_fn(self, req, resp, *args, **kwargs)
|
|
110
|
+
except Exception as exc: # catches falcon.HTTPError too
|
|
111
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
return _sync_wrapped
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# 3 | Attach wrapper to every on_<METHOD> responder in a resource
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _wrap_resource(resource: Any) -> None:
|
|
123
|
+
for attr in dir(resource):
|
|
124
|
+
if not attr.startswith("on_"):
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
handler = getattr(resource, attr)
|
|
128
|
+
if not callable(handler) or getattr(handler, "__sf_hop_wrapped__", False):
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
base_fn = handler.__func__ if isinstance(handler, MethodType) else handler
|
|
132
|
+
wrapped_fn = _make_wrapper(base_fn)
|
|
133
|
+
setattr(wrapped_fn, "__sf_hop_wrapped__", True)
|
|
134
|
+
|
|
135
|
+
# Bind to the *instance* so Falcon passes (req, resp, …) correctly
|
|
136
|
+
bound = MethodType(wrapped_fn, resource)
|
|
137
|
+
setattr(resource, attr, bound)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# 4 | Middleware merge utility (unchanged from earlier patch)
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _middleware_pos(cls) -> int:
|
|
146
|
+
sig = inspect.signature(cls.__init__)
|
|
147
|
+
params = [p for p in sig.parameters.values() if p.name != "self"]
|
|
148
|
+
try:
|
|
149
|
+
return [p.name for p in params].index("middleware")
|
|
150
|
+
except ValueError:
|
|
151
|
+
return -1
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _merge_middleware(args, kwargs, mw_pos):
|
|
155
|
+
pos = list(args)
|
|
156
|
+
kw = dict(kwargs)
|
|
157
|
+
existing, used = None, None
|
|
158
|
+
|
|
159
|
+
if "middleware" in kw:
|
|
160
|
+
existing = kw.pop("middleware")
|
|
161
|
+
if existing is None and mw_pos >= 0 and mw_pos < len(pos):
|
|
162
|
+
cand = pos[mw_pos]
|
|
163
|
+
# Not the Response class?
|
|
164
|
+
if not inspect.isclass(cand):
|
|
165
|
+
existing, used = cand, mw_pos
|
|
166
|
+
if existing is None and len(pos) == 1:
|
|
167
|
+
existing, used = pos[0], 0
|
|
168
|
+
|
|
169
|
+
merged: List[Any] = []
|
|
170
|
+
if existing is not None:
|
|
171
|
+
merged = list(existing) if isinstance(existing, (list, tuple)) else [existing]
|
|
172
|
+
merged.insert(0, SFTracingFalconMiddleware())
|
|
173
|
+
|
|
174
|
+
if used is not None:
|
|
175
|
+
pos[used] = merged
|
|
176
|
+
else:
|
|
177
|
+
kw["middleware"] = merged
|
|
178
|
+
|
|
179
|
+
return tuple(pos), kw
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# 5 | Patch helpers
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _patch_app_class(app_cls) -> None:
|
|
188
|
+
mw_pos = _middleware_pos(app_cls)
|
|
189
|
+
orig_init = app_cls.__init__
|
|
190
|
+
orig_add = app_cls.add_route
|
|
191
|
+
|
|
192
|
+
@functools.wraps(orig_init)
|
|
193
|
+
def patched_init(self, *args, **kwargs):
|
|
194
|
+
new_args, new_kwargs = _merge_middleware(args, kwargs, mw_pos)
|
|
195
|
+
orig_init(self, *new_args, **new_kwargs)
|
|
196
|
+
|
|
197
|
+
def patched_add_route(self, uri_template, resource, **kwargs):
|
|
198
|
+
_wrap_resource(resource)
|
|
199
|
+
return orig_add(self, uri_template, resource, **kwargs)
|
|
200
|
+
|
|
201
|
+
app_cls.__init__ = patched_init
|
|
202
|
+
app_cls.add_route = patched_add_route
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
# 6 | Public entry point
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def patch_falcon() -> None:
|
|
211
|
+
"""Activate tracing for both WSGI and ASGI Falcon apps."""
|
|
212
|
+
try:
|
|
213
|
+
import falcon
|
|
214
|
+
except ImportError: # pragma: no cover
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Patch synchronous WSGI app
|
|
218
|
+
_patch_app_class(falcon.App)
|
|
219
|
+
|
|
220
|
+
# Patch asynchronous ASGI app, if available
|
|
221
|
+
try:
|
|
222
|
+
from falcon.asgi import App as ASGIApp # type: ignore
|
|
223
|
+
|
|
224
|
+
_patch_app_class(ASGIApp)
|
|
225
|
+
except ImportError:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
if SF_DEBUG:
|
|
229
|
+
print("[[patch_falcon]] Falcon tracing middleware installed", log=False)
|