1
+ import ipaddress
1
2
import os
2
3
import sys
3
4
from typing import Any
23
24
from ddtrace .internal .schema .span_attribute_schema import SpanDirection
24
25
from ddtrace .internal .utils import get_blocked
25
26
from ddtrace .internal .utils import set_blocked
27
+ from ddtrace .internal .utils .formats import asbool
26
28
from ddtrace .trace import Span
27
29
28
30
34
36
service_name = config ._get_service (default = "asgi" ),
35
37
request_span_name = "asgi.request" ,
36
38
distributed_tracing = True ,
37
- _trace_asgi_websocket = os .getenv ("DD_ASGI_TRACE_WEBSOCKET" , default = False ),
39
+ _trace_asgi_websocket = asbool (os .getenv ("DD_ASGI_TRACE_WEBSOCKET" , default = False )),
40
+ # TODO: set as initially false until we gradually release feature
41
+ _trace_asgi_websocket_messages = asbool (os .getenv ("DD_TRACE_WEBSOCKET_MESSAGES_ENABLED" , default = True )),
42
+ _asgi_websockets_inherit_sampling = asbool (
43
+ os .getenv ("DD_TRACE_WEBSOCKET_MESSAGES_INHERIT_SAMPLING" , default = True )
44
+ ),
38
45
),
39
46
)
40
47
@@ -135,7 +142,6 @@ async def __call__(self, scope, receive, send):
135
142
return await self .app (scope , receive , send )
136
143
137
144
method = "WEBSOCKET"
138
-
139
145
else :
140
146
return await self .app (scope , receive , send )
141
147
try :
@@ -177,11 +183,12 @@ async def __call__(self, scope, receive, send):
177
183
ctx .set_item ("req_span" , span )
178
184
179
185
# set span.kind to the type of request being performed
186
+ # question: should this be network.client.ip or
187
+
180
188
span .set_tag_str (SPAN_KIND , SpanKind .SERVER )
181
189
182
190
if scope ["type" ] == "websocket" :
183
191
span .set_tag_str ("http.upgraded" , "websocket" )
184
- # TODO: add websocket session id
185
192
186
193
if "datadog" not in scope :
187
194
scope ["datadog" ] = {"request_spans" : [span ]}
@@ -255,13 +262,14 @@ async def __call__(self, scope, receive, send):
255
262
async def wrapped_send (message ):
256
263
"""
257
264
websocket.message.type
258
- websocket.session.id*
259
265
websocket.message.length
260
- websocket.message.frames*
261
- websocket.message.send_time*
266
+ websocket.message.frames
262
267
"""
263
268
try :
264
- if message .get ("type" ) == "websocket.send" :
269
+ if (
270
+ self .integration_config ._trace_asgi_websocket_messages
271
+ and message .get ("type" ) == "websocket.send"
272
+ ):
265
273
with self .tracer .trace (
266
274
"websocket.send" ,
267
275
service = span .service ,
@@ -270,15 +278,38 @@ async def wrapped_send(message):
270
278
) as send_span :
271
279
send_span .set_tag_str (COMPONENT , self .integration_config .integration_name )
272
280
send_span .set_tag_str (SPAN_KIND , SpanKind .PRODUCER )
281
+
282
+ # set tags related to peer.hostname
283
+ client = scope .get ("client" )
284
+ if len (client ) >= 1 :
285
+ client_ip = client [0 ]
286
+ span .set_tag_str ("out.host" , client_ip )
287
+ try :
288
+ ipaddress .ip_address (client_ip ) # validate ip address
289
+ span .set_tag_str ("network.client.ip" , client_ip )
290
+ except ValueError :
291
+ pass
273
292
# set link to http handshake span
274
293
# The link should have the attribute dd.kind set to resuming.
275
294
# If the instrumented library supports multicast or broadcast sending,
276
295
# there must be a link to the handshake span of every affected connection;
277
- # any websocket.session.id tag then should be added to the span link instead.
278
- elif message .get ("type" ) == "websocket.close" :
296
+ send_span .set_link (
297
+ trace_id = span .trace_id , span_id = span .span_id , attributes = {"dd.kind" : "resuming" }
298
+ )
299
+ send_span .set_metric ("websocket.message.frames" , 1 )
300
+ if "text" in message :
301
+ send_span .set_tag_str ("websocket.message.type" , "text" )
302
+ send_span .set_metric ("websocket.message.length" , len (message ["text" ].encode ("utf-8" )))
303
+ elif "binary" in message :
304
+ send_span .set_tag_str ("websocket.message.type" , "binary" )
305
+ send_span .set_metric ("websocket.message.length" , len (message ["bytes" ]))
306
+
307
+ elif (
308
+ self .integration_config ._trace_asgi_websocket_messages
309
+ and message .get ("type" ) == "websocket.close"
310
+ ):
279
311
"""
280
312
tags:
281
- websocket.session.id*
282
313
websocket.close.code
283
314
websocket.close.reason
284
315
@@ -291,7 +322,15 @@ async def wrapped_send(message):
291
322
) as close_span :
292
323
close_span .set_tag_str (COMPONENT , self .integration_config .integration_name )
293
324
close_span .set_tag_str (SPAN_KIND , SpanKind .PRODUCER )
294
- # TODO: add span links?
325
+ client = scope .get ("client" )
326
+ if len (client ) >= 1 :
327
+ client_ip = client [0 ]
328
+ span .set_tag_str ("out.host" , client_ip )
329
+ try :
330
+ ipaddress .ip_address (client_ip ) # validate ip address
331
+ span .set_tag_str ("network.client.ip" , client_ip )
332
+ except ValueError :
333
+ pass
295
334
if "text" in message :
296
335
close_span .set_tag_str ("websocket.message.type" , "text" )
297
336
close_span .set_metric ("websocket.message.length" , len (message ["text" ].encode ("utf-8" )))
@@ -343,27 +382,35 @@ async def wrapped_receive():
343
382
"""
344
383
tags:
345
384
websocket.message.type
346
- websocket.session.id*
347
385
websocket.message.length
348
- websocket.message.frames*
349
- websocket.message.receive_time*
386
+ websocket.message.frames
387
+
350
388
_dd.dm.inherited
351
389
_dd.dm.service
352
390
_dd.dm.resource
353
391
"""
354
392
355
393
try :
356
394
message = await receive ()
357
- if message ["type" ] == "websocket.receive" :
358
- with self .tracer .trace (
359
- "websocket.receive" ,
395
+ if (
396
+ self .integration_config ._trace_asgi_websocket_messages
397
+ and message ["type" ] == "websocket.receive"
398
+ ):
399
+ with self .tracer .start_span (
400
+ name = "websocket.receive" ,
360
401
service = span .service ,
361
402
resource = f"websocket { scope .get ('path' , '' )} " ,
362
403
span_type = "websocket" ,
404
+ child_of = None ,
363
405
) as ws_span :
364
406
ws_span .set_tag_str (COMPONENT , self .integration_config .integration_name )
365
407
ws_span .set_tag_str (SPAN_KIND , SpanKind .CONSUMER )
366
408
core .dispatch ("asgi.websocket.receive" , (message ,))
409
+ # the span should have link to http handshake span
410
+ # the link should have attribute dd.kind set to executed_by
411
+ ws_span .set_link (
412
+ trace_id = span .trace_id , span_id = span .span_id , attributes = {"dd.kind" : "executed_by" }
413
+ )
367
414
368
415
if "text" in message :
369
416
ws_span .set_tag_str ("websocket.message.type" , "text" )
@@ -372,27 +419,34 @@ async def wrapped_receive():
372
419
ws_span .set_tag_str ("websocket.message.type" , "binary" )
373
420
ws_span .set_metric ("websocket.message.length" , len (message ["bytes" ]))
374
421
375
- ws_span .set_metric ("_dd.dm.inherited" , 1 )
376
- parent_span = self .tracer .current_root_span ()
377
- if parent_span is not None :
378
- ws_span .set_tag_str ("_dd.dm.service" , parent_span .service )
379
- ws_span .set_tag_str ("_dd.dm.resource" , parent_span .resource )
422
+ # since asgi is a high level framework, frames is always 1
423
+ ws_span .set_metric ("websocket.message.frames" , 1 )
380
424
381
- elif message ["type" ] == "websocket.disconnect" :
425
+ if self .integration_config ._asgi_websockets_inherit_sampling :
426
+ ws_span .set_metric ("_dd.dm.inherited" , 1 )
427
+ ws_span .set_tag_str ("_dd.dm.service" , span .service )
428
+ ws_span .set_tag_str ("_dd.dm.resource" , span .resource )
429
+
430
+ elif (
431
+ self .integration_config ._trace_asgi_websocket_messages
432
+ and message ["type" ] == "websocket.disconnect"
433
+ ):
382
434
# peer closes the connection
383
- with self .tracer .trace (
384
- "websocket.close" ,
435
+ # in this case the span will be trace root (will behave like the websocket.receive use case)
436
+ with self .tracer .start_span (
437
+ name = "websocket.close" ,
385
438
service = span .service ,
386
439
resource = f"websocket { scope .get ('path' , '' )} " ,
387
440
span_type = "websocket" ,
441
+ child_of = None ,
388
442
) as close_span :
389
443
close_span .set_tag_str (COMPONENT , self .integration_config .integration_name )
390
444
close_span .set_tag_str (SPAN_KIND , SpanKind .CONSUMER )
391
- close_span . set_metric ( "_dd.dm.inherited" , 1 )
392
- parent_span = self .tracer . current_root_span ()
393
- if parent_span is not None :
394
- close_span .set_tag_str ("_dd.dm.service" , parent_span .service )
395
- close_span .set_tag_str ("_dd.dm.resource" , parent_span .resource )
445
+
446
+ if self .integration_config . _asgi_websockets_inherit_sampling :
447
+ close_span . set_metric ( "_dd.dm.inherited" , 1 )
448
+ close_span .set_tag_str ("_dd.dm.service" , span .service )
449
+ close_span .set_tag_str ("_dd.dm.resource" , span .resource )
396
450
397
451
code = message .get ("code" )
398
452
reason = message .get ("reason" )
@@ -427,7 +481,7 @@ async def wrapped_blocked_send(message):
427
481
message ["more_body" ] = False
428
482
core .dispatch ("asgi.finalize_response" , (content , None ))
429
483
try :
430
- return await send (message )
484
+ result = await send (message )
431
485
finally :
432
486
trace_utils .set_http_meta (
433
487
span , self .integration_config , status_code = status , response_headers = headers
@@ -440,7 +494,6 @@ async def wrapped_blocked_send(message):
440
494
try :
441
495
core .dispatch ("asgi.start_request" , ("asgi" ,))
442
496
# Do not block right here. Wait for route to be resolved in starlette/patch.py
443
- # TODO: do I need to change to wrapped_receive?
444
497
return await self .app (scope , wrapped_recv , wrapped_send )
445
498
except BlockingException as e :
446
499
set_blocked (e .args [0 ])
0 commit comments