latch-asgi 0.2.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 +60 -66
- latch_asgi/framework/websocket.py +100 -128
- latch_asgi/server.py +377 -221
- latch_asgi-1.0.0.dist-info/METADATA +18 -0
- latch_asgi-1.0.0.dist-info/RECORD +19 -0
- {latch_asgi-0.2.0.dist-info → latch_asgi-1.0.0.dist-info}/WHEEL +1 -1
- latch_asgi-1.0.0.dist-info/licenses/COPYING +121 -0
- latch_asgi-0.2.0.dist-info/METADATA +0 -25
- latch_asgi-0.2.0.dist-info/RECORD +0 -18
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,11 +51,9 @@ from .framework.websocket import (
|
|
|
49
51
|
WebsocketErrorResponse,
|
|
50
52
|
WebsocketInternalServerError,
|
|
51
53
|
WebsocketStatus,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
send_websocket_data,
|
|
56
|
-
websocket_request_span_key,
|
|
54
|
+
close_connection,
|
|
55
|
+
current_websocket_session_span,
|
|
56
|
+
websocket_session_span_key,
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
tracer = get_tracer(__name__)
|
|
@@ -62,278 +62,434 @@ tracer = get_tracer(__name__)
|
|
|
62
62
|
# todo(maximsmol): ASGI instrumentation should trace lifespan
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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")
|
|
70
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:
|
|
71
108
|
def __init__(
|
|
72
|
-
self,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
82
130
|
|
|
83
131
|
async def scope_lifespan(
|
|
84
|
-
self,
|
|
132
|
+
self: Self,
|
|
85
133
|
scope: LifespanScope,
|
|
86
134
|
receive: LifespanReceiveCallable,
|
|
87
135
|
send: LifespanSendCallable,
|
|
88
|
-
):
|
|
136
|
+
) -> None:
|
|
137
|
+
asgi_v = scope["asgi"].get("version", "2.0")
|
|
138
|
+
asgi_spec_v = scope["asgi"].get("spec_version", "1.0")
|
|
139
|
+
|
|
89
140
|
await log.info(
|
|
90
|
-
f"Waiting for lifespan events (ASGI v{
|
|
91
|
-
f" v{scope.asgi.spec_version})"
|
|
141
|
+
f"Waiting for lifespan events (ASGI v{asgi_v} @ spec v{asgi_spec_v})"
|
|
92
142
|
)
|
|
143
|
+
|
|
93
144
|
while True:
|
|
94
145
|
message = await receive()
|
|
95
|
-
await log.info(repr(message.type))
|
|
96
146
|
|
|
97
|
-
if
|
|
98
|
-
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
|
+
):
|
|
99
156
|
try:
|
|
100
157
|
await log.info("Executing startup tasks")
|
|
158
|
+
|
|
159
|
+
set_global_textmap(DDTraceContextTextMapPropagator())
|
|
160
|
+
|
|
101
161
|
# todo(maximsmol): debug clock skew on connection reset
|
|
102
162
|
await asyncio.gather(*self.startup_tasks)
|
|
103
163
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"lifespan.startup.complete"
|
|
108
|
-
)
|
|
164
|
+
await send(
|
|
165
|
+
LifespanStartupCompleteEvent(
|
|
166
|
+
type="lifespan.startup.complete"
|
|
109
167
|
)
|
|
168
|
+
)
|
|
110
169
|
except Exception as e:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"lifespan.startup.failed", str(e)
|
|
115
|
-
)
|
|
170
|
+
await send(
|
|
171
|
+
LifespanStartupFailedEvent(
|
|
172
|
+
type="lifespan.startup.failed", message=str(e)
|
|
116
173
|
)
|
|
174
|
+
)
|
|
117
175
|
|
|
118
|
-
raise
|
|
119
|
-
elif
|
|
120
|
-
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
|
+
):
|
|
121
181
|
try:
|
|
122
182
|
await asyncio.gather(*self.shutdown_tasks)
|
|
123
183
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
"lifespan.shutdown.complete"
|
|
128
|
-
)
|
|
184
|
+
await send(
|
|
185
|
+
LifespanShutdownCompleteEvent(
|
|
186
|
+
type="lifespan.shutdown.complete"
|
|
129
187
|
)
|
|
188
|
+
)
|
|
130
189
|
except Exception as e:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"lifespan.shutdown.failed", str(e)
|
|
135
|
-
)
|
|
190
|
+
await send(
|
|
191
|
+
LifespanShutdownFailedEvent(
|
|
192
|
+
type="lifespan.shutdown.failed", message=str(e)
|
|
136
193
|
)
|
|
194
|
+
)
|
|
137
195
|
|
|
138
|
-
raise
|
|
196
|
+
raise
|
|
139
197
|
|
|
140
198
|
break
|
|
199
|
+
else:
|
|
200
|
+
raise RuntimeError(f"unknown lifespan event: {message['type']!r}")
|
|
141
201
|
|
|
142
202
|
async def scope_websocket(
|
|
143
|
-
self,
|
|
203
|
+
self: Self,
|
|
144
204
|
scope: WebsocketScope,
|
|
145
205
|
receive: WebsocketReceiveCallable,
|
|
146
206
|
send: WebsocketSendCallable,
|
|
147
|
-
):
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
new_ctx = context.set_value(websocket_request_span_key, get_current_span())
|
|
151
|
-
ctx_reset_token = context.attach(new_ctx)
|
|
207
|
+
) -> None:
|
|
208
|
+
s = current_websocket_session_span()
|
|
152
209
|
|
|
153
|
-
|
|
210
|
+
with tracer.start_as_current_span("find route handler"):
|
|
211
|
+
handler = self.websocket_routes.get(scope["path"])
|
|
154
212
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
220
|
|
|
159
|
-
|
|
160
|
-
await close_websocket_connection(
|
|
161
|
-
send, status=WebsocketStatus.policy_violation, data=msg
|
|
162
|
-
)
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
await log.info(f"Websocket {scope.path}")
|
|
221
|
+
s.set_attribute("http.route", scope["path"])
|
|
166
222
|
|
|
223
|
+
try:
|
|
167
224
|
try:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if isinstance(res, tuple):
|
|
175
|
-
status, data = res
|
|
176
|
-
else:
|
|
177
|
-
status = WebsocketStatus.normal
|
|
178
|
-
data = res
|
|
179
|
-
|
|
180
|
-
except WebsocketErrorResponse as e:
|
|
181
|
-
raise e
|
|
182
|
-
except Exception as e:
|
|
183
|
-
raise WebsocketInternalServerError(str(e)) from e
|
|
184
|
-
except WebsocketErrorResponse as e:
|
|
185
|
-
await close_websocket_connection(
|
|
186
|
-
send, status=WebsocketStatus.server_error, data=str(e.data)
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
if e.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
190
|
-
traceback.print_exc()
|
|
191
|
-
else:
|
|
192
|
-
await close_websocket_connection(send, status=status, data=data)
|
|
193
|
-
finally:
|
|
194
|
-
if ctx_reset_token is not None:
|
|
195
|
-
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
|
+
)
|
|
196
230
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
)
|
|
204
242
|
|
|
205
|
-
|
|
206
|
-
|
|
243
|
+
if e.status != WebsocketStatus.server_error:
|
|
244
|
+
return
|
|
207
245
|
|
|
208
|
-
|
|
209
|
-
|
|
246
|
+
# await log.exception() # fixme(maximsmol)
|
|
247
|
+
raise
|
|
248
|
+
else:
|
|
249
|
+
await close_connection(send, status=WebsocketStatus.normal, data=data)
|
|
210
250
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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]}"
|
|
214
279
|
else:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if scope.method not in methods:
|
|
218
|
-
if len(methods) == 1:
|
|
219
|
-
methods_str = methods[0]
|
|
220
|
-
elif len(methods) == 2:
|
|
221
|
-
methods_str = f"{methods[0]} and {methods[1]}"
|
|
222
|
-
else:
|
|
223
|
-
methods_str = ", and ".join([", ".join(methods[:-1]), methods[-1]])
|
|
224
|
-
|
|
225
|
-
await send_http_data(
|
|
226
|
-
send,
|
|
227
|
-
HTTPStatus.METHOD_NOT_ALLOWED,
|
|
228
|
-
f"Only {methods_str} requests are supported",
|
|
229
|
-
)
|
|
230
|
-
return
|
|
280
|
+
methods_str = ", and ".join([", ".join(methods[:-1]), methods[-1]])
|
|
231
281
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
282
|
+
await send_http_data(
|
|
283
|
+
send,
|
|
284
|
+
HTTPStatus.METHOD_NOT_ALLOWED,
|
|
285
|
+
f"Only {methods_str} requests are supported",
|
|
286
|
+
)
|
|
287
|
+
return
|
|
237
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:
|
|
238
296
|
try:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if e.status == HTTPStatus.INTERNAL_SERVER_ERROR:
|
|
255
|
-
# await log.exception() # fixme(maximsmol)
|
|
256
|
-
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:
|
|
257
312
|
return
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
313
|
+
|
|
314
|
+
# await log.exception() # fixme(maximsmol)
|
|
315
|
+
raise
|
|
261
316
|
|
|
262
317
|
async def raw_app(
|
|
263
|
-
self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
|
|
264
|
-
):
|
|
318
|
+
self: Self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
|
|
319
|
+
) -> None:
|
|
265
320
|
try:
|
|
266
321
|
if scope["type"] == "lifespan":
|
|
322
|
+
# lifespan is not wrapped in a span because it has a `while(true)`
|
|
267
323
|
|
|
268
|
-
|
|
324
|
+
@trace_function_with_span(tracer)
|
|
325
|
+
async def ls_receive(s: Span) -> LifespanReceiveEvent:
|
|
269
326
|
x = await receive()
|
|
327
|
+
s.set_attribute("event_type", x["type"])
|
|
270
328
|
|
|
271
|
-
if x["type"] ==
|
|
272
|
-
return
|
|
329
|
+
if x["type"] == "lifespan.startup":
|
|
330
|
+
return x
|
|
273
331
|
|
|
274
|
-
if x["type"] ==
|
|
275
|
-
return
|
|
332
|
+
if x["type"] == "lifespan.shutdown":
|
|
333
|
+
return x
|
|
276
334
|
|
|
277
|
-
raise RuntimeError(
|
|
278
|
-
|
|
279
|
-
|
|
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"])
|
|
280
340
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
await send(data)
|
|
341
|
+
if e["type"] == "lifespan.shutdown.failed":
|
|
342
|
+
s.set_attribute("data.message", e["message"])
|
|
284
343
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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)
|
|
288
350
|
|
|
289
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)
|
|
290
356
|
|
|
291
|
-
|
|
292
|
-
|
|
357
|
+
try:
|
|
358
|
+
await log.info(span_name)
|
|
293
359
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)
|
|
297
364
|
|
|
298
|
-
|
|
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"])
|
|
299
369
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
370
|
+
if x["type"] == "websocket.connect":
|
|
371
|
+
return x
|
|
303
372
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
373
|
+
if x["type"] == "websocket.disconnect":
|
|
374
|
+
s.set_attribute("data.disconnect_code", x["code"])
|
|
375
|
+
return x
|
|
307
376
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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)
|
|
311
435
|
|
|
312
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)
|
|
313
441
|
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
316
462
|
|
|
317
|
-
|
|
318
|
-
|
|
463
|
+
if x["type"] == "http.disconnect":
|
|
464
|
+
return x
|
|
319
465
|
|
|
320
|
-
|
|
321
|
-
|
|
466
|
+
raise RuntimeError(
|
|
467
|
+
f"unknown http event type: {x['type']!r}"
|
|
468
|
+
)
|
|
322
469
|
|
|
323
|
-
|
|
470
|
+
@trace_function_with_span(tracer)
|
|
471
|
+
async def http_send(s: Span, e: HTTPSendEvent) -> None:
|
|
472
|
+
s.set_attribute("event_type", e["type"])
|
|
324
473
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
)
|
|
328
479
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
+
)
|
|
332
485
|
|
|
333
|
-
|
|
334
|
-
except Exception as e:
|
|
335
|
-
await log.exception("Fallback exception handler:")
|
|
336
|
-
raise e
|
|
486
|
+
await send(e)
|
|
337
487
|
|
|
488
|
+
return await self.scope_http(scope, http_receive, http_send)
|
|
489
|
+
finally:
|
|
490
|
+
context.detach(ctx_reset_token)
|
|
338
491
|
|
|
339
|
-
|
|
492
|
+
raise RuntimeError(f"unsupported protocol: {scope['type']!r}")
|
|
493
|
+
except Exception:
|
|
494
|
+
await log.exception("Fallback exception handler:")
|
|
495
|
+
raise
|