/
asgi.py
154 lines (124 loc) · 5.37 KB
/
asgi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""
An ASGI middleware.
Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_.
"""
import functools
import urllib
from sentry_sdk._types import MYPY
from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.utils import ContextVar, event_from_exception, transaction_from_function
from sentry_sdk.tracing import Span
if MYPY:
from typing import Dict
from typing import Any
_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
def _capture_exception(hub, exc):
# type: (Hub, Any) -> None
# Check client here as it might have been unset while streaming response
if hub.client is not None:
event, hint = event_from_exception(
exc,
client_options=hub.client.options,
mechanism={"type": "asgi", "handled": False},
)
hub.capture_event(event, hint=hint)
class SentryAsgiMiddleware:
__slots__ = ("app",)
def __init__(self, app):
self.app = app
def __call__(self, scope, receive=None, send=None):
if receive is None or send is None:
async def run_asgi2(receive, send):
return await self._run_app(
scope, lambda: self.app(scope)(receive, send)
)
return run_asgi2
else:
return self._run_app(scope, lambda: self.app(scope, receive, send))
async def _run_app(self, scope, callback):
if _asgi_middleware_applied.get(False):
return await callback()
_asgi_middleware_applied.set(True)
try:
hub = Hub(Hub.current)
with hub:
with hub.configure_scope() as sentry_scope:
sentry_scope.clear_breadcrumbs()
sentry_scope._name = "asgi"
processor = functools.partial(
self.event_processor, asgi_scope=scope
)
sentry_scope.add_event_processor(processor)
if scope["type"] in ("http", "websocket"):
span = Span.continue_from_headers(dict(scope["headers"]))
span.op = "{}.server".format(scope["type"])
else:
span = Span()
span.op = "asgi.server"
span.set_tag("asgi.type", scope["type"])
span.transaction = "generic ASGI request"
with hub.start_span(span) as span:
try:
return await callback()
except Exception as exc:
_capture_exception(hub, exc)
raise exc from None
finally:
_asgi_middleware_applied.set(False)
def event_processor(self, event, hint, asgi_scope):
request_info = event.setdefault("request", {})
if asgi_scope["type"] in ("http", "websocket"):
request_info["url"] = self.get_url(asgi_scope)
request_info["method"] = asgi_scope["method"]
request_info["headers"] = _filter_headers(self.get_headers(asgi_scope))
request_info["query_string"] = self.get_query(asgi_scope)
if asgi_scope.get("client") and _should_send_default_pii():
request_info["env"] = {"REMOTE_ADDR": asgi_scope["client"][0]}
if asgi_scope.get("endpoint"):
# Webframeworks like Starlette mutate the ASGI env once routing is
# done, which is sometime after the request has started. If we have
# an endpoint, overwrite our path-based transaction name.
event["transaction"] = self.get_transaction(asgi_scope)
return event
def get_url(self, scope):
"""
Extract URL from the ASGI scope, without also including the querystring.
"""
scheme = scope.get("scheme", "http")
server = scope.get("server", None)
path = scope.get("root_path", "") + scope["path"]
for key, value in scope["headers"]:
if key == b"host":
host_header = value.decode("latin-1")
return "%s://%s%s" % (scheme, host_header, path)
if server is not None:
host, port = server
default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
if port != default_port:
return "%s://%s:%s%s" % (scheme, host, port, path)
return "%s://%s%s" % (scheme, host, path)
return path
def get_query(self, scope):
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
return urllib.parse.unquote(scope["query_string"].decode("latin-1"))
def get_headers(self, scope):
"""
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
"""
headers = {} # type: Dict[str, str]
for raw_key, raw_value in scope["headers"]:
key = raw_key.decode("latin-1")
value = raw_value.decode("latin-1")
if key in headers:
headers[key] = headers[key] + ", " + value
else:
headers[key] = value
return headers
def get_transaction(self, scope):
"""
Return a transaction string to identify the routed endpoint.
"""
return transaction_from_function(scope["endpoint"])