latch-asgi 0.3.0__py3-none-any.whl → 1.0.0__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.
- latch_asgi/asgi_iface.py +33 -289
- latch_asgi/auth.py +7 -15
- latch_asgi/config.py +2 -0
- latch_asgi/context/common.py +30 -19
- latch_asgi/context/http.py +9 -23
- latch_asgi/context/websocket.py +35 -19
- latch_asgi/datadog_propagator.py +14 -12
- latch_asgi/framework/common.py +33 -0
- latch_asgi/framework/http.py +58 -56
- latch_asgi/framework/websocket.py +98 -93
- latch_asgi/server.py +377 -220
- {latch_asgi-0.3.0.dist-info → latch_asgi-1.0.0.dist-info}/METADATA +1 -2
- latch_asgi-1.0.0.dist-info/RECORD +19 -0
- latch_asgi-0.3.0.dist-info/RECORD +0 -19
- {latch_asgi-0.3.0.dist-info → latch_asgi-1.0.0.dist-info}/WHEEL +0 -0
- {latch_asgi-0.3.0.dist-info → latch_asgi-1.0.0.dist-info}/licenses/COPYING +0 -0
latch_asgi/server.py
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
3
|
-
import traceback
|
|
2
|
+
from collections.abc import Awaitable
|
|
4
3
|
from http import HTTPStatus
|
|
5
|
-
from typing import
|
|
4
|
+
from typing import Self
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
from hypercorn.typing import
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import orjson
|
|
7
|
+
from hypercorn.typing import (
|
|
8
|
+
ASGIReceiveCallable,
|
|
9
|
+
ASGISendCallable,
|
|
10
|
+
HTTPScope,
|
|
11
|
+
LifespanScope,
|
|
12
|
+
LifespanShutdownCompleteEvent,
|
|
13
|
+
LifespanShutdownFailedEvent,
|
|
14
|
+
LifespanStartupCompleteEvent,
|
|
15
|
+
LifespanStartupFailedEvent,
|
|
16
|
+
Scope,
|
|
17
|
+
WebsocketScope,
|
|
18
|
+
)
|
|
19
|
+
from latch_o11y.o11y import log, trace_function_with_span
|
|
20
|
+
from opentelemetry import context
|
|
11
21
|
from opentelemetry.propagate import set_global_textmap
|
|
12
|
-
from opentelemetry.trace import
|
|
22
|
+
from opentelemetry.trace import Span, SpanKind, get_tracer
|
|
23
|
+
from opentelemetry.util.types import AttributeValue
|
|
13
24
|
|
|
14
25
|
from .asgi_iface import (
|
|
15
|
-
HTTPDisconnectEvent,
|
|
16
26
|
HTTPReceiveCallable,
|
|
17
|
-
|
|
18
|
-
HTTPScope,
|
|
27
|
+
HTTPReceiveEvent,
|
|
19
28
|
HTTPSendCallable,
|
|
20
29
|
HTTPSendEvent,
|
|
21
30
|
LifespanReceiveCallable,
|
|
22
|
-
|
|
31
|
+
LifespanReceiveEvent,
|
|
23
32
|
LifespanSendCallable,
|
|
24
33
|
LifespanSendEvent,
|
|
25
|
-
LifespanShutdownCompleteEvent,
|
|
26
|
-
LifespanShutdownEvent,
|
|
27
|
-
LifespanShutdownFailedEvent,
|
|
28
|
-
LifespanStartupCompleteEvent,
|
|
29
|
-
LifespanStartupEvent,
|
|
30
|
-
LifespanStartupFailedEvent,
|
|
31
34
|
WebsocketReceiveCallable,
|
|
32
35
|
WebsocketReceiveEventT,
|
|
33
|
-
WebsocketScope,
|
|
34
36
|
WebsocketSendCallable,
|
|
35
37
|
WebsocketSendEventT,
|
|
36
|
-
type_str,
|
|
37
38
|
)
|
|
38
39
|
from .context import http, websocket
|
|
39
40
|
from .datadog_propagator import DDTraceContextTextMapPropagator
|
|
41
|
+
from .framework.common import otel_header_whitelist
|
|
40
42
|
from .framework.http import (
|
|
41
43
|
HTTPErrorResponse,
|
|
42
44
|
HTTPInternalServerError,
|
|
@@ -49,10 +51,9 @@ from .framework.websocket import (
|
|
|
49
51
|
WebsocketErrorResponse,
|
|
50
52
|
WebsocketInternalServerError,
|
|
51
53
|
WebsocketStatus,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
websocket_request_span_key,
|
|
54
|
+
close_connection,
|
|
55
|
+
current_websocket_session_span,
|
|
56
|
+
websocket_session_span_key,
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
tracer = get_tracer(__name__)
|
|
@@ -61,278 +62,434 @@ tracer = get_tracer(__name__)
|
|
|
61
62
|
# todo(maximsmol): ASGI instrumentation should trace lifespan
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
def get_common_attrs(scope: HTTPScope | WebsocketScope) -> dict[str, AttributeValue]:
|
|
66
|
+
client_addr = scope.get("client")
|
|
67
|
+
if client_addr is None:
|
|
68
|
+
client_addr = (None, None)
|
|
69
|
+
|
|
70
|
+
server_addr = scope.get("server")
|
|
71
|
+
if server_addr is None:
|
|
72
|
+
server_addr = (None, None)
|
|
73
|
+
|
|
74
|
+
attrs: dict[str, AttributeValue] = {
|
|
75
|
+
"client.address": str(client_addr[0]),
|
|
76
|
+
"client.port": str(client_addr[1]),
|
|
77
|
+
"server.address": str(server_addr[0]),
|
|
78
|
+
"server.port": str(server_addr[1]),
|
|
79
|
+
"network.transport": "tcp",
|
|
80
|
+
"network.peer.address": str(client_addr[0]),
|
|
81
|
+
"network.peer.port": str(client_addr[1]),
|
|
82
|
+
"network.local.address": str(server_addr[0]),
|
|
83
|
+
"network.local.port": str(server_addr[1]),
|
|
84
|
+
"network.protocol.name": "http",
|
|
85
|
+
"network.protocol.version": scope["http_version"],
|
|
86
|
+
"url.scheme": scope["scheme"],
|
|
87
|
+
"url.path": scope["path"],
|
|
88
|
+
"url.path_original": scope["raw_path"],
|
|
89
|
+
"url.query": scope["query_string"],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for k, v in scope["headers"]:
|
|
93
|
+
k = k.decode("latin-1")
|
|
94
|
+
k = k.lower()
|
|
95
|
+
|
|
96
|
+
if k == "user-agent":
|
|
97
|
+
attrs["user_agent.original"] = v.decode("latin-1")
|
|
69
98
|
|
|
99
|
+
if k not in otel_header_whitelist:
|
|
100
|
+
v = b"REDACTED"
|
|
101
|
+
|
|
102
|
+
attrs[f"http.request.header.{k}"] = v.decode("latin-1")
|
|
103
|
+
|
|
104
|
+
return attrs
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class LatchASGIServer:
|
|
70
108
|
def __init__(
|
|
71
|
-
self,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
self: Self,
|
|
110
|
+
*,
|
|
111
|
+
http_routes: dict[str, http.Route] | None = None,
|
|
112
|
+
websocket_routes: dict[str, websocket.Route] | None = None,
|
|
113
|
+
startup_tasks: list[Awaitable] | None = None,
|
|
114
|
+
shutdown_tasks: list[Awaitable] | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
if http_routes is None:
|
|
117
|
+
http_routes = {}
|
|
118
|
+
if websocket_routes is None:
|
|
119
|
+
websocket_routes = {}
|
|
120
|
+
|
|
121
|
+
if startup_tasks is None:
|
|
122
|
+
startup_tasks = []
|
|
123
|
+
if shutdown_tasks is None:
|
|
124
|
+
shutdown_tasks = []
|
|
125
|
+
|
|
126
|
+
self.http_routes: dict[str, http.Route] = http_routes
|
|
127
|
+
self.websocket_routes: dict[str, websocket.Route] = websocket_routes
|
|
128
|
+
self.startup_tasks: list[Awaitable] = startup_tasks
|
|
129
|
+
self.shutdown_tasks: list[Awaitable] = shutdown_tasks
|
|
81
130
|
|
|
82
131
|
async def scope_lifespan(
|
|
83
|
-
self,
|
|
132
|
+
self: Self,
|
|
84
133
|
scope: LifespanScope,
|
|
85
134
|
receive: LifespanReceiveCallable,
|
|
86
135
|
send: LifespanSendCallable,
|
|
87
|
-
):
|
|
136
|
+
) -> None:
|
|
137
|
+
asgi_v = scope["asgi"].get("version", "2.0")
|
|
138
|
+
asgi_spec_v = scope["asgi"].get("spec_version", "1.0")
|
|
139
|
+
|
|
88
140
|
await log.info(
|
|
89
|
-
f"Waiting for lifespan events (ASGI v{
|
|
90
|
-
f" v{scope.asgi.spec_version})"
|
|
141
|
+
f"Waiting for lifespan events (ASGI v{asgi_v} @ spec v{asgi_spec_v})"
|
|
91
142
|
)
|
|
143
|
+
|
|
92
144
|
while True:
|
|
93
145
|
message = await receive()
|
|
94
|
-
await log.info(repr(message.type))
|
|
95
146
|
|
|
96
|
-
if
|
|
97
|
-
with tracer.start_as_current_span(
|
|
147
|
+
if message["type"] == "lifespan.startup":
|
|
148
|
+
with tracer.start_as_current_span(
|
|
149
|
+
"ASGI Startup",
|
|
150
|
+
attributes={
|
|
151
|
+
"asgi.event_type": message["type"],
|
|
152
|
+
"asgi.version": asgi_v,
|
|
153
|
+
"asgi.spec_version": asgi_spec_v,
|
|
154
|
+
},
|
|
155
|
+
):
|
|
98
156
|
try:
|
|
99
157
|
await log.info("Executing startup tasks")
|
|
158
|
+
|
|
159
|
+
set_global_textmap(DDTraceContextTextMapPropagator())
|
|
160
|
+
|
|
100
161
|
# todo(maximsmol): debug clock skew on connection reset
|
|
101
162
|
await asyncio.gather(*self.startup_tasks)
|
|
102
163
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"lifespan.startup.complete"
|
|
107
|
-
)
|
|
164
|
+
await send(
|
|
165
|
+
LifespanStartupCompleteEvent(
|
|
166
|
+
type="lifespan.startup.complete"
|
|
108
167
|
)
|
|
168
|
+
)
|
|
109
169
|
except Exception as e:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"lifespan.startup.failed", str(e)
|
|
114
|
-
)
|
|
170
|
+
await send(
|
|
171
|
+
LifespanStartupFailedEvent(
|
|
172
|
+
type="lifespan.startup.failed", message=str(e)
|
|
115
173
|
)
|
|
174
|
+
)
|
|
116
175
|
|
|
117
|
-
raise
|
|
118
|
-
elif
|
|
119
|
-
with tracer.start_as_current_span(
|
|
176
|
+
raise
|
|
177
|
+
elif message["type"] == "lifespan.shutdown":
|
|
178
|
+
with tracer.start_as_current_span(
|
|
179
|
+
"ASGI Shutdown", attributes={"asgi.event_type": message["type"]}
|
|
180
|
+
):
|
|
120
181
|
try:
|
|
121
182
|
await asyncio.gather(*self.shutdown_tasks)
|
|
122
183
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"lifespan.shutdown.complete"
|
|
127
|
-
)
|
|
184
|
+
await send(
|
|
185
|
+
LifespanShutdownCompleteEvent(
|
|
186
|
+
type="lifespan.shutdown.complete"
|
|
128
187
|
)
|
|
188
|
+
)
|
|
129
189
|
except Exception as e:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"lifespan.shutdown.failed", str(e)
|
|
134
|
-
)
|
|
190
|
+
await send(
|
|
191
|
+
LifespanShutdownFailedEvent(
|
|
192
|
+
type="lifespan.shutdown.failed", message=str(e)
|
|
135
193
|
)
|
|
194
|
+
)
|
|
136
195
|
|
|
137
|
-
raise
|
|
196
|
+
raise
|
|
138
197
|
|
|
139
198
|
break
|
|
199
|
+
else:
|
|
200
|
+
raise RuntimeError(f"unknown lifespan event: {message['type']!r}")
|
|
140
201
|
|
|
141
202
|
async def scope_websocket(
|
|
142
|
-
self,
|
|
203
|
+
self: Self,
|
|
143
204
|
scope: WebsocketScope,
|
|
144
205
|
receive: WebsocketReceiveCallable,
|
|
145
206
|
send: WebsocketSendCallable,
|
|
146
|
-
):
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
new_ctx = context.set_value(websocket_request_span_key, get_current_span())
|
|
150
|
-
ctx_reset_token = context.attach(new_ctx)
|
|
207
|
+
) -> None:
|
|
208
|
+
s = current_websocket_session_span()
|
|
151
209
|
|
|
152
|
-
|
|
210
|
+
with tracer.start_as_current_span("find route handler"):
|
|
211
|
+
handler = self.websocket_routes.get(scope["path"])
|
|
153
212
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
if handler is None:
|
|
214
|
+
# todo(maximsmol): better error message
|
|
215
|
+
await log.info("Not found")
|
|
216
|
+
await close_connection(
|
|
217
|
+
send, status=WebsocketStatus.policy_violation, data="Not found"
|
|
218
|
+
)
|
|
219
|
+
return
|
|
157
220
|
|
|
158
|
-
|
|
159
|
-
await close_websocket_connection(
|
|
160
|
-
send, status=WebsocketStatus.policy_violation, data=msg
|
|
161
|
-
)
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
await log.info(f"Websocket {scope.path}")
|
|
221
|
+
s.set_attribute("http.route", scope["path"])
|
|
165
222
|
|
|
223
|
+
try:
|
|
166
224
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if isinstance(res, tuple):
|
|
174
|
-
status, data = res
|
|
175
|
-
else:
|
|
176
|
-
status = WebsocketStatus.normal
|
|
177
|
-
data = res
|
|
178
|
-
|
|
179
|
-
except WebsocketErrorResponse as e:
|
|
180
|
-
raise e
|
|
181
|
-
except Exception as e:
|
|
182
|
-
raise WebsocketInternalServerError(str(e)) from e
|
|
183
|
-
except WebsocketErrorResponse as e:
|
|
184
|
-
await close_websocket_connection(
|
|
185
|
-
send, status=WebsocketStatus.server_error, data=str(e.data)
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
if e.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
189
|
-
traceback.print_exc()
|
|
190
|
-
else:
|
|
191
|
-
await close_websocket_connection(send, status=status, data=data)
|
|
192
|
-
finally:
|
|
193
|
-
if ctx_reset_token is not None:
|
|
194
|
-
context.detach(ctx_reset_token)
|
|
225
|
+
msg = await receive()
|
|
226
|
+
if msg["type"] != "websocket.connect":
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"ASGI protocol violation: missing websocket.connect event"
|
|
229
|
+
)
|
|
195
230
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
231
|
+
ctx = websocket.Context(scope, receive, send)
|
|
232
|
+
data = await handler(ctx)
|
|
233
|
+
except WebsocketErrorResponse:
|
|
234
|
+
raise
|
|
235
|
+
except Exception as e:
|
|
236
|
+
# todo(maximsmol): better error message
|
|
237
|
+
raise WebsocketInternalServerError("Internal Error") from e
|
|
238
|
+
except WebsocketErrorResponse as e:
|
|
239
|
+
await close_connection(
|
|
240
|
+
send, status=e.status, data=orjson.dumps({"error": e.data}).decode()
|
|
241
|
+
)
|
|
203
242
|
|
|
204
|
-
|
|
205
|
-
|
|
243
|
+
if e.status != WebsocketStatus.server_error:
|
|
244
|
+
return
|
|
206
245
|
|
|
207
|
-
|
|
208
|
-
|
|
246
|
+
# await log.exception() # fixme(maximsmol)
|
|
247
|
+
raise
|
|
248
|
+
else:
|
|
249
|
+
await close_connection(send, status=WebsocketStatus.normal, data=data)
|
|
209
250
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
251
|
+
async def scope_http(
|
|
252
|
+
self: Self,
|
|
253
|
+
scope: HTTPScope,
|
|
254
|
+
receive: HTTPReceiveCallable,
|
|
255
|
+
send: HTTPSendCallable,
|
|
256
|
+
) -> None:
|
|
257
|
+
s = current_http_request_span()
|
|
258
|
+
|
|
259
|
+
with tracer.start_as_current_span("find route handler"):
|
|
260
|
+
route = self.http_routes.get(scope["path"])
|
|
261
|
+
|
|
262
|
+
if route is None:
|
|
263
|
+
await send_http_data(send, HTTPStatus.NOT_FOUND, "Not found")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
s.set_attribute("http.route", scope["path"])
|
|
267
|
+
|
|
268
|
+
if not isinstance(route, tuple):
|
|
269
|
+
methods = ["POST"]
|
|
270
|
+
handler = route
|
|
271
|
+
else:
|
|
272
|
+
methods, handler = route
|
|
273
|
+
|
|
274
|
+
if scope["method"] not in methods:
|
|
275
|
+
if len(methods) == 1:
|
|
276
|
+
methods_str = methods[0]
|
|
277
|
+
elif len(methods) == 2:
|
|
278
|
+
methods_str = f"{methods[0]} and {methods[1]}"
|
|
213
279
|
else:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if scope.method not in methods:
|
|
217
|
-
if len(methods) == 1:
|
|
218
|
-
methods_str = methods[0]
|
|
219
|
-
elif len(methods) == 2:
|
|
220
|
-
methods_str = f"{methods[0]} and {methods[1]}"
|
|
221
|
-
else:
|
|
222
|
-
methods_str = ", and ".join([", ".join(methods[:-1]), methods[-1]])
|
|
223
|
-
|
|
224
|
-
await send_http_data(
|
|
225
|
-
send,
|
|
226
|
-
HTTPStatus.METHOD_NOT_ALLOWED,
|
|
227
|
-
f"Only {methods_str} requests are supported",
|
|
228
|
-
)
|
|
229
|
-
return
|
|
280
|
+
methods_str = ", and ".join([", ".join(methods[:-1]), methods[-1]])
|
|
230
281
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
282
|
+
await send_http_data(
|
|
283
|
+
send,
|
|
284
|
+
HTTPStatus.METHOD_NOT_ALLOWED,
|
|
285
|
+
f"Only {methods_str} requests are supported",
|
|
286
|
+
)
|
|
287
|
+
return
|
|
236
288
|
|
|
289
|
+
if handler is None:
|
|
290
|
+
# todo(maximsmol): better error message
|
|
291
|
+
await log.info("Not found")
|
|
292
|
+
await send_http_data(send, HTTPStatus.NOT_FOUND, "Not found")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
try:
|
|
237
296
|
try:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if e.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
254
|
-
# await log.exception() # fixme(maximsmol)
|
|
255
|
-
traceback.print_exc()
|
|
297
|
+
ctx = http.Context(scope, receive, send)
|
|
298
|
+
res = await handler(ctx)
|
|
299
|
+
|
|
300
|
+
if res is not None:
|
|
301
|
+
with tracer.start_as_current_span("send response"):
|
|
302
|
+
await send_auto(send, HTTPStatus.OK, res)
|
|
303
|
+
except HTTPErrorResponse:
|
|
304
|
+
raise
|
|
305
|
+
except Exception as e:
|
|
306
|
+
# todo(maximsmol): better error message
|
|
307
|
+
raise HTTPInternalServerError("Internal error") from e
|
|
308
|
+
except HTTPErrorResponse as e:
|
|
309
|
+
await send_auto(send, e.status, {"error": e.data}, headers=e.headers)
|
|
310
|
+
|
|
311
|
+
if e.status != HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
256
312
|
return
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
313
|
+
|
|
314
|
+
# await log.exception() # fixme(maximsmol)
|
|
315
|
+
raise
|
|
260
316
|
|
|
261
317
|
async def raw_app(
|
|
262
|
-
self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
|
|
263
|
-
):
|
|
318
|
+
self: Self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
|
|
319
|
+
) -> None:
|
|
264
320
|
try:
|
|
265
321
|
if scope["type"] == "lifespan":
|
|
322
|
+
# lifespan is not wrapped in a span because it has a `while(true)`
|
|
266
323
|
|
|
267
|
-
|
|
324
|
+
@trace_function_with_span(tracer)
|
|
325
|
+
async def ls_receive(s: Span) -> LifespanReceiveEvent:
|
|
268
326
|
x = await receive()
|
|
327
|
+
s.set_attribute("event_type", x["type"])
|
|
269
328
|
|
|
270
|
-
if x["type"] ==
|
|
271
|
-
return
|
|
329
|
+
if x["type"] == "lifespan.startup":
|
|
330
|
+
return x
|
|
272
331
|
|
|
273
|
-
if x["type"] ==
|
|
274
|
-
return
|
|
332
|
+
if x["type"] == "lifespan.shutdown":
|
|
333
|
+
return x
|
|
275
334
|
|
|
276
|
-
raise RuntimeError(
|
|
277
|
-
|
|
278
|
-
|
|
335
|
+
raise RuntimeError(f"unknown lifespan event type: {x['type']!r}")
|
|
336
|
+
|
|
337
|
+
@trace_function_with_span(tracer)
|
|
338
|
+
async def ls_send(s: Span, e: LifespanSendEvent) -> None:
|
|
339
|
+
s.set_attribute("type", e["type"])
|
|
279
340
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
await send(data)
|
|
341
|
+
if e["type"] == "lifespan.shutdown.failed":
|
|
342
|
+
s.set_attribute("data.message", e["message"])
|
|
283
343
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
344
|
+
if e["type"] == "lifespan.startup.failed":
|
|
345
|
+
s.set_attribute("data.message", e["message"])
|
|
346
|
+
|
|
347
|
+
await send(e)
|
|
348
|
+
|
|
349
|
+
return await self.scope_lifespan(scope, ls_receive, ls_send)
|
|
287
350
|
|
|
288
351
|
if scope["type"] == "websocket":
|
|
352
|
+
span_name = f"WS {scope['path']}"
|
|
353
|
+
with tracer.start_as_current_span(span_name, kind=SpanKind.SERVER) as s:
|
|
354
|
+
new_ctx = context.set_value(websocket_session_span_key, s)
|
|
355
|
+
ctx_reset_token = context.attach(new_ctx)
|
|
289
356
|
|
|
290
|
-
|
|
291
|
-
|
|
357
|
+
try:
|
|
358
|
+
await log.info(span_name)
|
|
292
359
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
360
|
+
attrs = get_common_attrs(scope)
|
|
361
|
+
for i, x in enumerate(scope["subprotocols"]):
|
|
362
|
+
attrs[f"websocket.subprotocol.{i}"] = x
|
|
363
|
+
s.set_attributes(attrs)
|
|
296
364
|
|
|
297
|
-
|
|
365
|
+
@trace_function_with_span(tracer)
|
|
366
|
+
async def ws_receive(s: Span) -> WebsocketReceiveEventT:
|
|
367
|
+
x = await receive()
|
|
368
|
+
s.set_attribute("type", x["type"])
|
|
298
369
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
)
|
|
370
|
+
if x["type"] == "websocket.connect":
|
|
371
|
+
return x
|
|
302
372
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
373
|
+
if x["type"] == "websocket.disconnect":
|
|
374
|
+
s.set_attribute("data.disconnect_code", x["code"])
|
|
375
|
+
return x
|
|
306
376
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
377
|
+
if x["type"] == "websocket.receive":
|
|
378
|
+
attrs: dict[str, AttributeValue] = {
|
|
379
|
+
"data.binary": x["bytes"] is None
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if x["bytes"] is not None:
|
|
383
|
+
attrs["data.size"] = len(x["bytes"])
|
|
384
|
+
elif x["text"] is not None:
|
|
385
|
+
attrs["data.size"] = len(x["text"])
|
|
386
|
+
|
|
387
|
+
s.set_attributes(attrs)
|
|
388
|
+
return x
|
|
389
|
+
|
|
390
|
+
raise RuntimeError(
|
|
391
|
+
f"unknown websocket event type: {x['type']!r}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
@trace_function_with_span(tracer)
|
|
395
|
+
async def ws_send(s: Span, e: WebsocketSendEventT) -> None:
|
|
396
|
+
s.set_attribute("type", e["type"])
|
|
397
|
+
|
|
398
|
+
if e["type"] == "websocket.accept":
|
|
399
|
+
attrs: dict[str, AttributeValue] = {
|
|
400
|
+
"data.subprotocol": str(e["subprotocol"])
|
|
401
|
+
}
|
|
402
|
+
for i, (k, v) in enumerate(e["headers"]):
|
|
403
|
+
if v not in otel_header_whitelist:
|
|
404
|
+
v = b""
|
|
405
|
+
|
|
406
|
+
attrs[
|
|
407
|
+
f"data.header.{i}.{k.decode('latin-1')}"
|
|
408
|
+
] = v.decode("latin-1")
|
|
409
|
+
|
|
410
|
+
if e["type"] == "websocket.close":
|
|
411
|
+
s.set_attributes(
|
|
412
|
+
{
|
|
413
|
+
"data.code": WebsocketStatus(e["code"]).name,
|
|
414
|
+
"data.reason": str(e["reason"]),
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if e["type"] == "websocket.send":
|
|
419
|
+
attrs: dict[str, AttributeValue] = {
|
|
420
|
+
"data.binary": e["bytes"] is None
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if e["bytes"] is not None:
|
|
424
|
+
attrs["data.size"] = len(e["bytes"])
|
|
425
|
+
elif e["text"] is not None:
|
|
426
|
+
attrs["data.size"] = len(e["text"])
|
|
427
|
+
|
|
428
|
+
s.set_attributes(attrs)
|
|
429
|
+
|
|
430
|
+
await send(e)
|
|
431
|
+
|
|
432
|
+
return await self.scope_websocket(scope, ws_receive, ws_send)
|
|
433
|
+
finally:
|
|
434
|
+
context.detach(ctx_reset_token)
|
|
310
435
|
|
|
311
436
|
if scope["type"] == "http":
|
|
437
|
+
span_name = f"{scope['method']} {scope['path']}"
|
|
438
|
+
with tracer.start_as_current_span(span_name, kind=SpanKind.SERVER) as s:
|
|
439
|
+
new_ctx = context.set_value(http_request_span_key, s)
|
|
440
|
+
ctx_reset_token = context.attach(new_ctx)
|
|
312
441
|
|
|
313
|
-
|
|
314
|
-
|
|
442
|
+
try:
|
|
443
|
+
await log.info(span_name)
|
|
444
|
+
|
|
445
|
+
attrs = get_common_attrs(scope)
|
|
446
|
+
attrs["http.request.method"] = scope["method"]
|
|
447
|
+
s.set_attributes(attrs)
|
|
448
|
+
|
|
449
|
+
@trace_function_with_span(tracer)
|
|
450
|
+
async def http_receive(s: Span) -> HTTPReceiveEvent:
|
|
451
|
+
x = await receive()
|
|
452
|
+
s.set_attribute("event_type", x["type"])
|
|
453
|
+
|
|
454
|
+
if x["type"] == "http.request":
|
|
455
|
+
s.set_attributes(
|
|
456
|
+
{
|
|
457
|
+
"data.size": len(x["body"]),
|
|
458
|
+
"data.more_body": x["more_body"],
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
return x
|
|
315
462
|
|
|
316
|
-
|
|
317
|
-
|
|
463
|
+
if x["type"] == "http.disconnect":
|
|
464
|
+
return x
|
|
318
465
|
|
|
319
|
-
|
|
320
|
-
|
|
466
|
+
raise RuntimeError(
|
|
467
|
+
f"unknown http event type: {x['type']!r}"
|
|
468
|
+
)
|
|
321
469
|
|
|
322
|
-
|
|
470
|
+
@trace_function_with_span(tracer)
|
|
471
|
+
async def http_send(s: Span, e: HTTPSendEvent) -> None:
|
|
472
|
+
s.set_attribute("event_type", e["type"])
|
|
323
473
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
474
|
+
if e["type"] == "http.response.start":
|
|
475
|
+
s.set_attribute("data.status", e["status"])
|
|
476
|
+
current_http_request_span().set_attribute(
|
|
477
|
+
"http.response.status_code", e["status"]
|
|
478
|
+
)
|
|
327
479
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
480
|
+
if e["type"] == "http.response.body":
|
|
481
|
+
s.set_attribute("data.size", len(e["body"]))
|
|
482
|
+
current_http_request_span().set_attribute(
|
|
483
|
+
"http.response.body.size", len(e["body"])
|
|
484
|
+
)
|
|
331
485
|
|
|
332
|
-
|
|
333
|
-
except Exception as e:
|
|
334
|
-
await log.exception("Fallback exception handler:")
|
|
335
|
-
raise e
|
|
486
|
+
await send(e)
|
|
336
487
|
|
|
488
|
+
return await self.scope_http(scope, http_receive, http_send)
|
|
489
|
+
finally:
|
|
490
|
+
context.detach(ctx_reset_token)
|
|
337
491
|
|
|
338
|
-
|
|
492
|
+
raise RuntimeError(f"unsupported protocol: {scope['type']!r}")
|
|
493
|
+
except Exception:
|
|
494
|
+
await log.exception("Fallback exception handler:")
|
|
495
|
+
raise
|