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,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