sf-veritas 0.9.7__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (86) hide show
  1. sf_veritas/.gitignore +2 -0
  2. sf_veritas/__init__.py +4 -0
  3. sf_veritas/app_config.py +49 -0
  4. sf_veritas/cli.py +336 -0
  5. sf_veritas/constants.py +3 -0
  6. sf_veritas/custom_excepthook.py +285 -0
  7. sf_veritas/custom_log_handler.py +53 -0
  8. sf_veritas/custom_output_wrapper.py +107 -0
  9. sf_veritas/custom_print.py +34 -0
  10. sf_veritas/django_app.py +5 -0
  11. sf_veritas/env_vars.py +83 -0
  12. sf_veritas/exception_handling_middleware.py +18 -0
  13. sf_veritas/exception_metaclass.py +69 -0
  14. sf_veritas/frame_tools.py +112 -0
  15. sf_veritas/import_hook.py +62 -0
  16. sf_veritas/infra_details/__init__.py +3 -0
  17. sf_veritas/infra_details/get_infra_details.py +24 -0
  18. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  19. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  20. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  21. sf_veritas/infra_details/running_on/__init__.py +17 -0
  22. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  23. sf_veritas/interceptors.py +252 -0
  24. sf_veritas/local_env_detect.py +118 -0
  25. sf_veritas/package_metadata.py +6 -0
  26. sf_veritas/patches/__init__.py +0 -0
  27. sf_veritas/patches/concurrent_futures.py +19 -0
  28. sf_veritas/patches/constants.py +1 -0
  29. sf_veritas/patches/exceptions.py +82 -0
  30. sf_veritas/patches/multiprocessing.py +32 -0
  31. sf_veritas/patches/network_libraries/__init__.py +51 -0
  32. sf_veritas/patches/network_libraries/aiohttp.py +100 -0
  33. sf_veritas/patches/network_libraries/curl_cffi.py +93 -0
  34. sf_veritas/patches/network_libraries/http_client.py +64 -0
  35. sf_veritas/patches/network_libraries/httpcore.py +152 -0
  36. sf_veritas/patches/network_libraries/httplib2.py +76 -0
  37. sf_veritas/patches/network_libraries/httpx.py +123 -0
  38. sf_veritas/patches/network_libraries/niquests.py +192 -0
  39. sf_veritas/patches/network_libraries/pycurl.py +71 -0
  40. sf_veritas/patches/network_libraries/requests.py +187 -0
  41. sf_veritas/patches/network_libraries/tornado.py +139 -0
  42. sf_veritas/patches/network_libraries/treq.py +122 -0
  43. sf_veritas/patches/network_libraries/urllib_request.py +129 -0
  44. sf_veritas/patches/network_libraries/utils.py +101 -0
  45. sf_veritas/patches/os.py +17 -0
  46. sf_veritas/patches/threading.py +32 -0
  47. sf_veritas/patches/web_frameworks/__init__.py +45 -0
  48. sf_veritas/patches/web_frameworks/aiohttp.py +133 -0
  49. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +132 -0
  50. sf_veritas/patches/web_frameworks/blacksheep.py +107 -0
  51. sf_veritas/patches/web_frameworks/bottle.py +142 -0
  52. sf_veritas/patches/web_frameworks/cherrypy.py +246 -0
  53. sf_veritas/patches/web_frameworks/django.py +307 -0
  54. sf_veritas/patches/web_frameworks/eve.py +138 -0
  55. sf_veritas/patches/web_frameworks/falcon.py +229 -0
  56. sf_veritas/patches/web_frameworks/fastapi.py +145 -0
  57. sf_veritas/patches/web_frameworks/flask.py +186 -0
  58. sf_veritas/patches/web_frameworks/klein.py +40 -0
  59. sf_veritas/patches/web_frameworks/litestar.py +217 -0
  60. sf_veritas/patches/web_frameworks/pyramid.py +89 -0
  61. sf_veritas/patches/web_frameworks/quart.py +155 -0
  62. sf_veritas/patches/web_frameworks/robyn.py +114 -0
  63. sf_veritas/patches/web_frameworks/sanic.py +120 -0
  64. sf_veritas/patches/web_frameworks/starlette.py +144 -0
  65. sf_veritas/patches/web_frameworks/strawberry.py +269 -0
  66. sf_veritas/patches/web_frameworks/tornado.py +129 -0
  67. sf_veritas/patches/web_frameworks/utils.py +55 -0
  68. sf_veritas/print_override.py +13 -0
  69. sf_veritas/regular_data_transmitter.py +358 -0
  70. sf_veritas/request_interceptor.py +399 -0
  71. sf_veritas/request_utils.py +104 -0
  72. sf_veritas/server_status.py +1 -0
  73. sf_veritas/shutdown_flag.py +11 -0
  74. sf_veritas/subprocess_startup.py +3 -0
  75. sf_veritas/test_cli.py +145 -0
  76. sf_veritas/thread_local.py +436 -0
  77. sf_veritas/timeutil.py +114 -0
  78. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  79. sf_veritas/transmitter.py +58 -0
  80. sf_veritas/types.py +44 -0
  81. sf_veritas/unified_interceptor.py +323 -0
  82. sf_veritas/utils.py +39 -0
  83. sf_veritas-0.9.7.dist-info/METADATA +83 -0
  84. sf_veritas-0.9.7.dist-info/RECORD +86 -0
  85. sf_veritas-0.9.7.dist-info/WHEEL +4 -0
  86. sf_veritas-0.9.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,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)