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/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,10 +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
- 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
- class LatchASGIServer:
65
- http_routes: dict[str, http.Route]
66
- websocket_routes: dict[str, websocket.Route]
67
- startup_tasks: list[Awaitable] = []
68
- 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")
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
- http_routes: dict[str, http.Route],
73
- websocket_routes: dict[str, websocket.Route],
74
- startup_tasks: list[Awaitable] = [],
75
- shutdown_tasks: list[Awaitable] = [],
76
- ):
77
- self.http_routes = http_routes
78
- self.websocket_routes = websocket_routes
79
- self.startup_tasks = startup_tasks
80
- 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
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{scope.asgi.version} @ spec"
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 isinstance(message, LifespanStartupEvent):
97
- 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
+ ):
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
- with tracer.start_as_current_span("send completion event"):
104
- await send(
105
- LifespanStartupCompleteEvent(
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
- with tracer.start_as_current_span("send failure event"):
111
- await send(
112
- LifespanStartupFailedEvent(
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 e
118
- elif isinstance(message, LifespanShutdownEvent):
119
- 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
+ ):
120
181
  try:
121
182
  await asyncio.gather(*self.shutdown_tasks)
122
183
 
123
- with tracer.start_as_current_span("send completion event"):
124
- await send(
125
- LifespanShutdownCompleteEvent(
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
- with tracer.start_as_current_span("send failure event"):
131
- await send(
132
- LifespanShutdownFailedEvent(
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 e
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
- ctx_reset_token: object | None = None
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
- 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"])
153
212
 
154
- handler = self.websocket_routes.get(scope.path)
155
- if handler is None:
156
- 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
157
220
 
158
- await log.info(msg)
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
- try:
168
- ctx = websocket.Context(scope, receive, send)
169
-
170
- await accept_websocket_connection(ctx.send, ctx.receive)
171
- res = await handler(ctx)
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
- async def scope_http(
197
- self, scope: HTTPScope, receive: HTTPReceiveCallable, send: HTTPSendCallable
198
- ):
199
- ctx_reset_token: object | None = None
200
- try:
201
- new_ctx = context.set_value(http_request_span_key, get_current_span())
202
- 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
+ )
203
242
 
204
- current_http_request_span().set_attribute("resource.name", scope.path)
205
- await log.info(f"{scope.method} {scope.path}")
243
+ if e.status != WebsocketStatus.server_error:
244
+ return
206
245
 
207
- with tracer.start_as_current_span("find route handler"):
208
- 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)
209
250
 
210
- if not isinstance(route, tuple):
211
- methods = ["POST"]
212
- 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]}"
213
279
  else:
214
- methods, handler = route
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
- if handler is None:
232
- # todo(maximsmol): better error message
233
- await log.info("Not found")
234
- await send_http_data(send, HTTPStatus.NOT_FOUND, "Not found")
235
- return
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
- try:
239
- ctx = http.Context(scope, receive, send)
240
- res = await handler(ctx)
241
-
242
- if res is not None:
243
- with tracer.start_as_current_span("send response"):
244
- await send_auto(send, HTTPStatus.OK, res)
245
- return
246
- except HTTPErrorResponse as e:
247
- raise e
248
- except Exception as e:
249
- # todo(maximsmol): better error message
250
- raise HTTPInternalServerError("Internal error") from e
251
- except HTTPErrorResponse as e:
252
- await send_auto(send, e.status, {"error": e.data}, headers=e.headers)
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
- finally:
258
- if ctx_reset_token is not None:
259
- context.detach(ctx_reset_token)
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
- async def ls_receive():
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"] == type_str(LifespanStartupEvent):
271
- return untraced_validate(x, LifespanStartupEvent)
329
+ if x["type"] == "lifespan.startup":
330
+ return x
272
331
 
273
- if x["type"] == type_str(LifespanShutdownEvent):
274
- return untraced_validate(x, LifespanShutdownEvent)
332
+ if x["type"] == "lifespan.shutdown":
333
+ return x
275
334
 
276
- raise RuntimeError(
277
- f"unknown lifespan event type: {repr(x['type'])}"
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
- async def ls_send(e: LifespanSendEvent):
281
- data = dataclasses.asdict(e)
282
- await send(data)
341
+ if e["type"] == "lifespan.shutdown.failed":
342
+ s.set_attribute("data.message", e["message"])
283
343
 
284
- return await self.scope_lifespan(
285
- untraced_validate(scope, LifespanScope), ls_receive, ls_send
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
- async def ws_receive():
291
- x = await receive()
357
+ try:
358
+ await log.info(span_name)
292
359
 
293
- for e in get_args(WebsocketReceiveEventT):
294
- if x["type"] != type_str(e):
295
- 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)
296
364
 
297
- return untraced_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"])
298
369
 
299
- raise RuntimeError(
300
- f"unknown websocket event type: {repr(x['type'])}"
301
- )
370
+ if x["type"] == "websocket.connect":
371
+ return x
302
372
 
303
- async def ws_send(e: WebsocketSendEventT):
304
- data = dataclasses.asdict(e)
305
- await send(data)
373
+ if x["type"] == "websocket.disconnect":
374
+ s.set_attribute("data.disconnect_code", x["code"])
375
+ return x
306
376
 
307
- return await self.scope_websocket(
308
- untraced_validate(scope, WebsocketScope), ws_receive, ws_send
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
- async def http_receive():
314
- 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
315
462
 
316
- if x["type"] == type_str(HTTPRequestEvent):
317
- return validate(x, HTTPRequestEvent)
463
+ if x["type"] == "http.disconnect":
464
+ return x
318
465
 
319
- if x["type"] == type_str(HTTPDisconnectEvent):
320
- return validate(x, HTTPDisconnectEvent)
466
+ raise RuntimeError(
467
+ f"unknown http event type: {x['type']!r}"
468
+ )
321
469
 
322
- 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"])
323
473
 
324
- async def http_send(e: HTTPSendEvent):
325
- data = dataclasses.asdict(e)
326
- 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
+ )
327
479
 
328
- return await self.scope_http(
329
- validate(scope, HTTPScope), http_receive, http_send
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
- raise RuntimeError(f"unsupported protocol: {repr(scope['type'])}")
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
- 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