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/server.py CHANGED
@@ -1,42 +1,44 @@
1
1
  import asyncio
2
- import dataclasses
3
- import traceback
2
+ from collections.abc import Awaitable
4
3
  from http import HTTPStatus
5
- from typing import Awaitable, get_args
4
+ from typing import Self
6
5
 
7
- import opentelemetry.context as context
8
- from hypercorn.typing import ASGIReceiveCallable, ASGISendCallable, Scope
9
- from latch_data_validation.data_validation import untraced_validate, validate
10
- from latch_o11y.o11y import log
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 get_current_span, get_tracer
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
- HTTPRequestEvent,
18
- HTTPScope,
27
+ HTTPReceiveEvent,
19
28
  HTTPSendCallable,
20
29
  HTTPSendEvent,
21
30
  LifespanReceiveCallable,
22
- LifespanScope,
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
- accept_websocket_connection,
53
- close_websocket_connection,
54
- current_websocket_request_span,
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
- class LatchASGIServer:
66
- http_routes: dict[str, http.Route]
67
- websocket_routes: dict[str, websocket.Route]
68
- startup_tasks: list[Awaitable] = []
69
- shutdown_tasks: list[Awaitable] = []
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
- http_routes: dict[str, http.Route],
74
- websocket_routes: dict[str, websocket.Route],
75
- startup_tasks: list[Awaitable] = [],
76
- shutdown_tasks: list[Awaitable] = [],
77
- ):
78
- self.http_routes = http_routes
79
- self.websocket_routes = websocket_routes
80
- self.startup_tasks = startup_tasks
81
- self.shutdown_tasks = shutdown_tasks
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{scope.asgi.version} @ spec"
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 isinstance(message, LifespanStartupEvent):
98
- with tracer.start_as_current_span("startup"):
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
- with tracer.start_as_current_span("send completion event"):
105
- await send(
106
- LifespanStartupCompleteEvent(
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
- with tracer.start_as_current_span("send failure event"):
112
- await send(
113
- LifespanStartupFailedEvent(
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 e
119
- elif isinstance(message, LifespanShutdownEvent):
120
- with tracer.start_as_current_span("shutdown"):
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
- with tracer.start_as_current_span("send completion event"):
125
- await send(
126
- LifespanShutdownCompleteEvent(
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
- with tracer.start_as_current_span("send failure event"):
132
- await send(
133
- LifespanShutdownFailedEvent(
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 e
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
- ctx_reset_token: object | None = None
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
- current_websocket_request_span().set_attribute("resource.name", scope.path)
210
+ with tracer.start_as_current_span("find route handler"):
211
+ handler = self.websocket_routes.get(scope["path"])
154
212
 
155
- handler = self.websocket_routes.get(scope.path)
156
- if handler is None:
157
- msg = f"Websocket {scope.path} not found"
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
- await log.info(msg)
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
- try:
169
- ctx = websocket.Context(scope, receive, send)
170
-
171
- await accept_websocket_connection(ctx.send, ctx.receive)
172
- res = await handler(ctx)
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
- async def scope_http(
198
- self, scope: HTTPScope, receive: HTTPReceiveCallable, send: HTTPSendCallable
199
- ):
200
- ctx_reset_token: object | None = None
201
- try:
202
- new_ctx = context.set_value(http_request_span_key, get_current_span())
203
- ctx_reset_token = context.attach(new_ctx)
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
- current_http_request_span().set_attribute("resource.name", scope.path)
206
- await log.info(f"{scope.method} {scope.path}")
243
+ if e.status != WebsocketStatus.server_error:
244
+ return
207
245
 
208
- with tracer.start_as_current_span("find route handler"):
209
- route = self.http_routes.get(scope.path)
246
+ # await log.exception() # fixme(maximsmol)
247
+ raise
248
+ else:
249
+ await close_connection(send, status=WebsocketStatus.normal, data=data)
210
250
 
211
- if not isinstance(route, tuple):
212
- methods = ["POST"]
213
- handler = route
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
- methods, handler = route
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
- if handler is None:
233
- # todo(maximsmol): better error message
234
- await log.info("Not found")
235
- await send_http_data(send, HTTPStatus.NOT_FOUND, "Not found")
236
- return
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
- try:
240
- ctx = http.Context(scope, receive, send)
241
- res = await handler(ctx)
242
-
243
- if res is not None:
244
- with tracer.start_as_current_span("send response"):
245
- await send_auto(send, HTTPStatus.OK, res)
246
- return
247
- except HTTPErrorResponse as e:
248
- raise e
249
- except Exception as e:
250
- # todo(maximsmol): better error message
251
- raise HTTPInternalServerError("Internal error") from e
252
- except HTTPErrorResponse as e:
253
- await send_auto(send, e.status, {"error": e.data}, headers=e.headers)
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
- finally:
259
- if ctx_reset_token is not None:
260
- context.detach(ctx_reset_token)
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
- async def ls_receive():
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"] == type_str(LifespanStartupEvent):
272
- return untraced_validate(x, LifespanStartupEvent)
329
+ if x["type"] == "lifespan.startup":
330
+ return x
273
331
 
274
- if x["type"] == type_str(LifespanShutdownEvent):
275
- return untraced_validate(x, LifespanShutdownEvent)
332
+ if x["type"] == "lifespan.shutdown":
333
+ return x
276
334
 
277
- raise RuntimeError(
278
- f"unknown lifespan event type: {repr(x['type'])}"
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
- async def ls_send(e: LifespanSendEvent):
282
- data = dataclasses.asdict(e)
283
- await send(data)
341
+ if e["type"] == "lifespan.shutdown.failed":
342
+ s.set_attribute("data.message", e["message"])
284
343
 
285
- return await self.scope_lifespan(
286
- untraced_validate(scope, LifespanScope), ls_receive, ls_send
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
- async def ws_receive():
292
- x = await receive()
357
+ try:
358
+ await log.info(span_name)
293
359
 
294
- for e in get_args(WebsocketReceiveEventT):
295
- if x["type"] != type_str(e):
296
- continue
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
- return validate(x, e)
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
- raise RuntimeError(
301
- f"unknown websocket event type: {repr(x['type'])}"
302
- )
370
+ if x["type"] == "websocket.connect":
371
+ return x
303
372
 
304
- async def ws_send(e: WebsocketSendEventT):
305
- data = dataclasses.asdict(e)
306
- await send(data)
373
+ if x["type"] == "websocket.disconnect":
374
+ s.set_attribute("data.disconnect_code", x["code"])
375
+ return x
307
376
 
308
- return await self.scope_websocket(
309
- validate(scope, WebsocketScope), ws_receive, ws_send
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
- async def http_receive():
315
- x = await receive()
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
- if x["type"] == type_str(HTTPRequestEvent):
318
- return validate(x, HTTPRequestEvent)
463
+ if x["type"] == "http.disconnect":
464
+ return x
319
465
 
320
- if x["type"] == type_str(HTTPDisconnectEvent):
321
- return validate(x, HTTPDisconnectEvent)
466
+ raise RuntimeError(
467
+ f"unknown http event type: {x['type']!r}"
468
+ )
322
469
 
323
- raise RuntimeError(f"unknown http event type: {repr(x['type'])}")
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
- async def http_send(e: HTTPSendEvent):
326
- data = dataclasses.asdict(e)
327
- await send(data)
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
- return await self.scope_http(
330
- validate(scope, HTTPScope), http_receive, http_send
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
- raise RuntimeError(f"unsupported protocol: {repr(scope['type'])}")
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
- set_global_textmap(DDTraceContextTextMapPropagator())
492
+ raise RuntimeError(f"unsupported protocol: {scope['type']!r}")
493
+ except Exception:
494
+ await log.exception("Fallback exception handler:")
495
+ raise