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,155 @@
1
+ """
2
+ • SFTracingQuartASGIMiddleware: pulls SAILFISH_TRACING_HEADER into your ContextVar.
3
+ • patch_quart(): wraps Quart.__init__, installs middleware and
4
+ redefines .route so that each user-land view emits one NetworkHop.
5
+ """
6
+
7
+ import asyncio
8
+ import inspect
9
+ import sysconfig
10
+ from functools import lru_cache, wraps
11
+ from typing import Any, Callable, Set, Tuple
12
+
13
+ from ...constants import SAILFISH_TRACING_HEADER
14
+ from ...custom_excepthook import custom_excepthook
15
+ from ...regular_data_transmitter import NetworkHopsTransmitter
16
+ from ...thread_local import get_or_set_sf_trace_id
17
+ from .utils import _is_user_code, _unwrap_user_func # your cached helpers
18
+
19
+ try:
20
+ import quart
21
+ from quart.app import Quart
22
+ from quart.wrappers import Response
23
+ except ImportError:
24
+ # Quart not installed → no-op
25
+ def patch_quart():
26
+ return
27
+
28
+ else:
29
+ # ──────────────────────────────────────────────────────────
30
+ # 1) ASGI middleware to preserve SAILFISH_TRACING_HEADER in ContextVar
31
+ # ──────────────────────────────────────────────────────────
32
+ class SFTracingQuartASGIMiddleware:
33
+ """Wraps the ASGI app so inbound SAILFISH_TRACING_HEADER → ContextVar."""
34
+
35
+ def __init__(self, app):
36
+ self.app = app
37
+
38
+ async def __call__(self, scope, receive, send):
39
+ if scope.get("type") == "http":
40
+ for name, val in scope.get("headers", []):
41
+ if name.decode("utf-8").lower() == SAILFISH_TRACING_HEADER.lower():
42
+ get_or_set_sf_trace_id(
43
+ val.decode("utf-8"), is_associated_with_inbound_request=True
44
+ )
45
+ break
46
+ await self.app(scope, receive, send)
47
+
48
+ # ──────────────────────────────────────────────────────────
49
+ # 2) Monkey-patch Quart to install our middleware + route wrapper
50
+ # ──────────────────────────────────────────────────────────
51
+ def patch_quart():
52
+ """
53
+ Patches Quart.__init__ so that:
54
+ 1. the internal ASGI app is wrapped by SFTracingQuartASGIMiddleware
55
+ (captures inbound SAILFISH_TRACING_HEADER);
56
+ 2. every @app.route handler is wrapped to emit one NetworkHop *and*
57
+ funnel **all** exceptions—*including* quart.exceptions.HTTPException—
58
+ through custom_excepthook, then re-raise so Quart still returns the
59
+ correct HTTP response.
60
+ """
61
+
62
+ original_init = Quart.__init__
63
+
64
+ def patched_init(self, *args, **kwargs):
65
+ # 1) call original ctor
66
+ original_init(self, *args, **kwargs)
67
+
68
+ # 2) wrap ASGI app for header propagation
69
+ self.asgi_app = SFTracingQuartASGIMiddleware(self.asgi_app)
70
+
71
+ # 3) patch .route decorator
72
+ original_route = self.route
73
+
74
+ def tracing_route(self, rule: str, **options):
75
+ """
76
+ Replacement for @app.route(...) → decorator(fn).
77
+ """
78
+ decorator = original_route(rule, **options)
79
+
80
+ def wrapper(fn: Callable[..., Any]) -> Callable[..., Any]:
81
+ # unwrap any decorators/closures to find real user fn
82
+ user_fn = _unwrap_user_func(fn)
83
+
84
+ code = getattr(user_fn, "__code__", None)
85
+ if not code or not _is_user_code(code.co_filename):
86
+ return decorator(fn) # non-user code → no hop/exception capture
87
+
88
+ if getattr(user_fn, "__module__", "").startswith("strawberry"):
89
+ return decorator(fn) # skip Strawberry views
90
+
91
+ filename = code.co_filename
92
+ line_no = code.co_firstlineno
93
+ name = user_fn.__name__
94
+ sent: Set[Tuple[str, int]] = set() # one-time hop flag
95
+
96
+ # choose async vs sync wrapper based on endpoint type
97
+ if asyncio.iscoroutinefunction(fn):
98
+
99
+ @wraps(fn)
100
+ async def wrapped(*a, **k):
101
+ key = (filename, line_no)
102
+ if key not in sent:
103
+ _, session_id = get_or_set_sf_trace_id()
104
+ NetworkHopsTransmitter().send(
105
+ session_id=session_id,
106
+ line=str(line_no),
107
+ column="0",
108
+ name=name,
109
+ entrypoint=filename,
110
+ )
111
+ sent.add(key)
112
+ try:
113
+ return await fn(*a, **k)
114
+ except (
115
+ Exception
116
+ ) as exc: # capture ALL exceptions, incl. HTTPException
117
+ custom_excepthook(type(exc), exc, exc.__traceback__)
118
+ raise
119
+
120
+ wrapped_fn = wrapped
121
+ else:
122
+
123
+ @wraps(fn)
124
+ def wrapped(*a, **k):
125
+ key = (filename, line_no)
126
+ if key not in sent:
127
+ _, session_id = get_or_set_sf_trace_id()
128
+ NetworkHopsTransmitter().send(
129
+ session_id=session_id,
130
+ line=str(line_no),
131
+ column="0",
132
+ name=name,
133
+ entrypoint=filename,
134
+ )
135
+ sent.add(key)
136
+ try:
137
+ return fn(*a, **k)
138
+ except Exception as exc:
139
+ custom_excepthook(type(exc), exc, exc.__traceback__)
140
+ raise
141
+
142
+ wrapped_fn = wrapped
143
+
144
+ return decorator(wrapped_fn)
145
+
146
+ return wrapper
147
+
148
+ # rebind the instance's route method
149
+ self.route = tracing_route.__get__(self, Quart)
150
+
151
+ # apply the patch once
152
+ Quart.__init__ = patched_init
153
+
154
+ # expose the patch function
155
+ __all__ = ["patch_quart"]
@@ -0,0 +1,114 @@
1
+ import functools
2
+ import inspect
3
+ import sys
4
+ import sysconfig
5
+ from functools import lru_cache
6
+ from typing import Any, Callable, Optional, Set
7
+
8
+ from ...constants import SAILFISH_TRACING_HEADER
9
+ from ...custom_excepthook import custom_excepthook
10
+ from ...env_vars import SF_DEBUG
11
+ from ...regular_data_transmitter import NetworkHopsTransmitter
12
+ from ...thread_local import get_or_set_sf_trace_id
13
+ from ..constants import supported_network_verbs as HTTP_METHODS
14
+ from .utils import _is_user_code, _unwrap_user_func
15
+
16
+ _stdlib = sysconfig.get_paths()["stdlib"]
17
+
18
+
19
+ @lru_cache(maxsize=512)
20
+ def _is_user_code(path: Optional[str]) -> bool:
21
+ """
22
+ True only for “application” files (not stdlib or site-packages).
23
+ """
24
+ if not path or path.startswith("<"):
25
+ return False
26
+ if path.startswith(_stdlib):
27
+ return False
28
+ if "site-packages" in path or "dist-packages" in path:
29
+ return False
30
+ return True
31
+
32
+
33
+ def patch_robyn():
34
+ """
35
+ Monkey-patch robyn.Robyn so that every Robyn() instance
36
+ auto-wraps route handlers with header propagation and network‐hop emission.
37
+ Safe no-op if Robyn isn't installed.
38
+ """
39
+ try:
40
+ import robyn
41
+ except ImportError:
42
+ return
43
+
44
+ for method_name in HTTP_METHODS:
45
+ if not hasattr(robyn.Robyn, method_name):
46
+ continue
47
+
48
+ original_factory = getattr(robyn.Robyn, method_name)
49
+
50
+ def make_patched(factory):
51
+ @functools.wraps(factory)
52
+ def patched(self, path: str, *args, **kwargs):
53
+ # original decorator returned by Robyn
54
+ decorator = factory(self, path, *args, **kwargs)
55
+
56
+ def custom_decorator(fn):
57
+ real_fn = _unwrap_user_func(fn)
58
+
59
+ @functools.wraps(fn)
60
+ async def wrapped_handler(request, *a, **kw):
61
+ # ──────────────────────────────────────────────────────────
62
+ # 1) Capture inbound trace header
63
+ # ──────────────────────────────────────────────────────────
64
+ hdr = getattr(request, "headers", {}).get(
65
+ SAILFISH_TRACING_HEADER
66
+ )
67
+ if hdr:
68
+ get_or_set_sf_trace_id(
69
+ hdr, is_associated_with_inbound_request=True
70
+ )
71
+
72
+ # ──────────────────────────────────────────────────────────
73
+ # 2) Emit a single NetworkHop (user code only)
74
+ # ──────────────────────────────────────────────────────────
75
+ filename = real_fn.__code__.co_filename
76
+ if _is_user_code(filename):
77
+ line_no = real_fn.__code__.co_firstlineno
78
+ _, session_id = get_or_set_sf_trace_id()
79
+
80
+ if SF_DEBUG:
81
+ print(
82
+ f"[[RobynHop]] {real_fn.__name__} @ {filename}:{line_no} "
83
+ f"session={session_id}",
84
+ log=False,
85
+ )
86
+
87
+ NetworkHopsTransmitter().send(
88
+ session_id=session_id,
89
+ line=str(line_no),
90
+ column="0",
91
+ name=real_fn.__name__,
92
+ entrypoint=filename,
93
+ )
94
+
95
+ # ──────────────────────────────────────────────────────────
96
+ # 3) Run user handler and funnel ANY exception (including
97
+ # robyn.HTTPException) through custom_excepthook
98
+ # ──────────────────────────────────────────────────────────
99
+ try:
100
+ return await real_fn(request, *a, **kw)
101
+ except Exception as exc: # noqa: BLE001
102
+ # Fast path: let custom_excepthook decide deduplication
103
+ custom_excepthook(type(exc), exc, exc.__traceback__)
104
+ raise # Re-raise so Robyn still returns proper error
105
+
106
+ # Register wrapped_handler in place of the original
107
+ return decorator(wrapped_handler)
108
+
109
+ return custom_decorator
110
+
111
+ return patched
112
+
113
+ # bind a fresh patched_factory for each HTTP method
114
+ setattr(robyn.Robyn, method_name, make_patched(original_factory))
@@ -0,0 +1,120 @@
1
+ import inspect
2
+ import sysconfig
3
+ from functools import lru_cache
4
+
5
+ from ...constants import SAILFISH_TRACING_HEADER
6
+ from ...custom_excepthook import custom_excepthook
7
+ from ...env_vars import SF_DEBUG
8
+ from ...regular_data_transmitter import NetworkHopsTransmitter
9
+ from ...thread_local import get_or_set_sf_trace_id
10
+ from .utils import _is_user_code, _unwrap_user_func
11
+
12
+
13
+ def patch_sanic():
14
+ """
15
+ If Sanic is installed, monkey-patch Sanic.__init__ so every app:
16
+ 1) request middleware: pulls SAILFISH_TRACING_HEADER → ContextVar
17
+ 2) response middleware: emits exactly one NetworkHop from the
18
+ first user-land frame of the handler that just ran.
19
+ Safe no-op if Sanic isn't present.
20
+ """
21
+ print("[[patch_sanic]] patching sanic...", log=False)
22
+ try:
23
+ from sanic import Sanic
24
+ except ImportError:
25
+ return
26
+ print("[[patch_sanic]] patching sanic...about to start", log=False)
27
+
28
+ # ----------------------------------------------------------------- #
29
+ # Patch __init__: install two middleware
30
+ # ----------------------------------------------------------------- #
31
+ orig_init = Sanic.__init__
32
+
33
+ def patched_init(self, *args, **kwargs):
34
+ """
35
+ After the original Sanic app is created we attach:
36
+ • request-middleware – capture `SAILFISH_TRACING_HEADER`
37
+ • response-middleware – emit one NetworkHop
38
+ • *NEW* universal exception handler – funnels every Exception
39
+ (including Sanic HTTP errors) through `custom_excepthook`
40
+ and then delegates to Sanic's default error handling chain.
41
+ """
42
+ # ---------------------------------------------------------------- #
43
+ # Let Sanic build the app normally
44
+ # ---------------------------------------------------------------- #
45
+ orig_init(self, *args, **kwargs)
46
+
47
+ # ─────────────────── 1) Inbound header capture ─────────────────── #
48
+ async def _push_trace_id(request):
49
+ hdr = request.headers.get(SAILFISH_TRACING_HEADER)
50
+ if hdr:
51
+ get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
52
+
53
+ try:
54
+ self.register_middleware(_push_trace_id, attach_to="request")
55
+ except TypeError: # Sanic<22 compatibility
56
+ self.register_middleware(_push_trace_id, "request")
57
+
58
+ # ─────────────────── 2) NetworkHop emission ────────────────────── #
59
+ async def _emit_hop(request, response):
60
+ handler = getattr(request, "route", None)
61
+ if not handler:
62
+ return
63
+ fn = getattr(handler, "handler", None)
64
+ if not fn:
65
+ return
66
+
67
+ real_fn = _unwrap_user_func(fn)
68
+ code = getattr(real_fn, "__code__", None)
69
+ path = getattr(code, "co_filename", None)
70
+ if not _is_user_code(path):
71
+ return
72
+
73
+ line_no = code.co_firstlineno
74
+ name = real_fn.__name__
75
+ _, session_id = get_or_set_sf_trace_id() # already seeded
76
+
77
+ if SF_DEBUG:
78
+ print(
79
+ f"[[Sanic-hop]] {name} ({path}:{line_no}) session={session_id}",
80
+ log=False,
81
+ )
82
+
83
+ NetworkHopsTransmitter().send(
84
+ session_id=session_id,
85
+ line=str(line_no),
86
+ column="0",
87
+ name=name,
88
+ entrypoint=path,
89
+ )
90
+
91
+ try:
92
+ self.register_middleware(_emit_hop, attach_to="response")
93
+ except TypeError:
94
+ self.register_middleware(_emit_hop, "response")
95
+
96
+ # ─────────────────── 3) Universal exception hook  NEW ──────────── #
97
+ async def _capture_exception(request, exception):
98
+ """
99
+ Called for *any* exception – user errors, `abort|HTTPException`,
100
+ or Sanic-specific errors. We forward to `custom_excepthook`
101
+ and then fall back to Sanic's default error handler so
102
+ behaviour is unchanged.
103
+ """
104
+ custom_excepthook(type(exception), exception, exception.__traceback__)
105
+ # Delegate to default handler to keep standard 4xx/5xx payload
106
+ response = request.app.error_handler.default(request, exception)
107
+ if inspect.isawaitable(response):
108
+ response = await response
109
+ return response
110
+
111
+ # Register for the base `Exception` class to catch everything
112
+ self.error_handler.add(Exception, _capture_exception)
113
+
114
+ if SF_DEBUG:
115
+ print(
116
+ "[[patch_sanic]] tracing middlewares + exception handler added",
117
+ log=False,
118
+ )
119
+
120
+ Sanic.__init__ = patched_init
@@ -0,0 +1,144 @@
1
+ """
2
+ • NetworkHopMiddleware: pure ASGI middleware that
3
+ - pulls SAILFISH_TRACING_HEADER → ContextVar
4
+ - installs a one-shot sys.setprofile tracer to catch the first user frame
5
+ • patch_starlette(): idempotent—monkey-patches Starlette.__init__
6
+ to insert NetworkHopMiddleware on every app, before any routes fire.
7
+ """
8
+
9
+ import inspect
10
+ import sys
11
+ import sysconfig
12
+ from typing import Optional
13
+
14
+ from ...constants import SAILFISH_TRACING_HEADER
15
+ from ...custom_excepthook import custom_excepthook
16
+ from ...env_vars import SF_DEBUG
17
+ from ...regular_data_transmitter import NetworkHopsTransmitter
18
+ from ...thread_local import get_or_set_sf_trace_id
19
+
20
+ # Guard so we only patch once
21
+ _starlette_patched = False
22
+
23
+ try:
24
+ from starlette.applications import Starlette
25
+ from starlette.types import ASGIApp, Receive, Scope, Send
26
+ except ImportError:
27
+
28
+ def patch_starlette():
29
+ return
30
+
31
+ else:
32
+ # Pre-compute stdlib path for user-code checks
33
+ _STDLIB_PATH = sysconfig.get_paths()["stdlib"]
34
+
35
+ def _is_user_code(path: Optional[str] = None) -> bool:
36
+ """Return True if filename is in user code (not stdlib or site-packages)."""
37
+ if not path or path.startswith("<"):
38
+ return False
39
+ if path.startswith(_STDLIB_PATH):
40
+ return False
41
+ if "site-packages" in path or "dist-packages" in path:
42
+ return False
43
+ return True
44
+
45
+ def patch_starlette():
46
+ global _starlette_patched
47
+ if _starlette_patched:
48
+ return
49
+ _starlette_patched = True
50
+
51
+ # ----------------- profiler tracer factory -----------------
52
+ def _make_tracer(info_scope: Scope):
53
+ def tracer(frame, event, arg):
54
+ if event == "call" and _is_user_code(frame.f_code.co_filename):
55
+ # First user frame: emit NetworkHop
56
+ _, session_id = get_or_set_sf_trace_id()
57
+ NetworkHopsTransmitter().send(
58
+ session_id=session_id,
59
+ line=str(frame.f_lineno),
60
+ column="0",
61
+ name=frame.f_code.co_name,
62
+ entrypoint=frame.f_code.co_filename,
63
+ )
64
+ # Stop profiling this request
65
+ sys.setprofile(None)
66
+ return tracer
67
+
68
+ return tracer
69
+
70
+ # ----------------- ASGI middleware -----------------
71
+ class NetworkHopMiddleware:
72
+ """
73
+ ASGI middleware that
74
+
75
+ 1) Pulls the inbound SAILFISH_TRACING_HEADER → ContextVar.
76
+ 2) Installs a one-shot ``sys.setprofile`` tracer to record the first
77
+ *user-land* frame in the request (sends a NetworkHop).
78
+ 3) Funnels **all** exceptions - including Starlette/HTTPException - through
79
+ ``custom_excepthook`` before letting Starlette continue its normal
80
+ error handling.
81
+ """
82
+
83
+ def __init__(self, app: ASGIApp):
84
+ self.app = app
85
+
86
+ async def __call__(self, scope: Scope, receive: Receive, send: Send):
87
+ profiler_installed = False
88
+
89
+ if scope.get("type") == "http":
90
+ # --- 1) header capture ---------------------------------------
91
+ headers = {
92
+ k.decode().lower(): v.decode()
93
+ for k, v in scope.get("headers", [])
94
+ }
95
+ hdr = headers.get(SAILFISH_TRACING_HEADER.lower())
96
+ if hdr:
97
+ get_or_set_sf_trace_id(
98
+ hdr, is_associated_with_inbound_request=True
99
+ )
100
+
101
+ # --- 2) install one-shot profiler ---------------------------
102
+ import sys
103
+
104
+ def _tracer(frame, event, arg):
105
+ if event == "call" and _is_user_code(frame.f_code.co_filename):
106
+ _, session_id = get_or_set_sf_trace_id()
107
+ NetworkHopsTransmitter().send(
108
+ session_id=session_id,
109
+ line=str(frame.f_lineno),
110
+ column="0",
111
+ name=frame.f_code.co_name,
112
+ entrypoint=frame.f_code.co_filename,
113
+ )
114
+ sys.setprofile(None) # disable after first hop
115
+ return _tracer
116
+
117
+ sys.setprofile(_tracer)
118
+ profiler_installed = True
119
+
120
+ # --- 3) run downstream app and trap *all* exceptions ------------
121
+ try:
122
+ await self.app(scope, receive, send)
123
+ except Exception as exc: # ← catches HTTPException too
124
+ custom_excepthook(type(exc), exc, exc.__traceback__)
125
+ raise # Let Starlette build the 4xx/5xx response
126
+ finally:
127
+ if profiler_installed:
128
+ sys.setprofile(None) # safety-net
129
+ sys.setprofile(None)
130
+
131
+ # ----------------- patch Starlette init -----------------
132
+ original_init = Starlette.__init__
133
+
134
+ def patched_init(self, *args, **kwargs):
135
+ # 1) Run the original constructor
136
+ original_init(self, *args, **kwargs)
137
+
138
+ # 2) Insert our ASGI middleware at the top
139
+ self.add_middleware(NetworkHopMiddleware)
140
+
141
+ if SF_DEBUG:
142
+ print("[[patch_starlette]] Installed NetworkHopMiddleware", log=False)
143
+
144
+ Starlette.__init__ = patched_init