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