9
9
import importlib
10
10
import logging
11
11
import os
12
+ from time import time_ns
12
13
from typing import Any , Callable , Dict , Iterator , List , Optional , Tuple , Union
13
14
from urllib .parse import urlparse
14
15
@@ -193,23 +194,27 @@ def _set_attributes(self, span: "AbstractSpan", *attrs: Tuple[str, Any]) -> None
193
194
if value is not None :
194
195
span .add_attribute (key , value )
195
196
196
- def _add_request_chat_message_event (self , span : "AbstractSpan" , ** kwargs : Any ) -> None :
197
+ def _add_request_chat_message_events (self , span : "AbstractSpan" , ** kwargs : Any ) -> int :
198
+ timestamp = 0
197
199
for message in kwargs .get ("messages" , []):
198
200
try :
199
201
message = message .as_dict ()
200
202
except AttributeError :
201
203
pass
202
204
203
205
if message .get ("role" ):
204
- name = f"gen_ai. { message . get ( 'role' ) } .message"
205
- span . span_instance . add_event (
206
- name = name ,
207
- attributes = {
206
+ timestamp = self . _record_event (
207
+ span ,
208
+ f"gen_ai. { message . get ( 'role' ) } .message" ,
209
+ {
208
210
"gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
209
211
"gen_ai.event.content" : json .dumps (message ),
210
212
},
213
+ timestamp ,
211
214
)
212
215
216
+ return timestamp
217
+
213
218
def _parse_url (self , url ):
214
219
parsed = urlparse (url )
215
220
server_address = parsed .hostname
@@ -280,8 +285,11 @@ def _get_finish_reason_for_choice(self, choice):
280
285
281
286
return "none"
282
287
283
- def _add_response_chat_message_event (self , span : "AbstractSpan" , result : _models .ChatCompletions ) -> None :
288
+ def _add_response_chat_message_events (self , span : "AbstractSpan" ,
289
+ result : _models .ChatCompletions , last_event_timestamp_ns : int
290
+ ) -> None :
284
291
for choice in result .choices :
292
+ attributes = {}
285
293
if _trace_inference_content :
286
294
full_response : Dict [str , Any ] = {
287
295
"message" : {"content" : choice .message .content },
@@ -312,7 +320,7 @@ def _add_response_chat_message_event(self, span: "AbstractSpan", result: _models
312
320
"gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
313
321
"gen_ai.event.content" : json .dumps (response ),
314
322
}
315
- span . span_instance . add_event ( name = "gen_ai.choice" , attributes = attributes )
323
+ last_event_timestamp_ns = self . _record_event ( span , "gen_ai.choice" , attributes , last_event_timestamp_ns )
316
324
317
325
def _add_response_chat_attributes (
318
326
self ,
@@ -336,15 +344,16 @@ def _add_response_chat_attributes(
336
344
if not finish_reasons is None :
337
345
span .add_attribute ("gen_ai.response.finish_reasons" , finish_reasons ) # type: ignore
338
346
339
- def _add_request_span_attributes (self , span : "AbstractSpan" , _span_name : str , args : Any , kwargs : Any ) -> None :
347
+ def _add_request_details (self , span : "AbstractSpan" , args : Any , kwargs : Any ) -> int :
340
348
self ._add_request_chat_attributes (span , * args , ** kwargs )
341
349
if _trace_inference_content :
342
- self ._add_request_chat_message_event (span , ** kwargs )
350
+ return self ._add_request_chat_message_events (span , ** kwargs )
351
+ return 0
343
352
344
- def _add_response_span_attributes (self , span : "AbstractSpan" , result : object ) -> None :
353
+ def _add_response_details (self , span : "AbstractSpan" , result : object , last_event_timestamp_ns : int ) -> None :
345
354
if isinstance (result , _models .ChatCompletions ):
346
355
self ._add_response_chat_attributes (span , result )
347
- self ._add_response_chat_message_event (span , result )
356
+ self ._add_response_chat_message_events (span , result , last_event_timestamp_ns )
348
357
# TODO add more models here
349
358
350
359
def _accumulate_response (self , item , accumulate : Dict [str , Any ]) -> None :
@@ -410,7 +419,7 @@ def _accumulate_async_streaming_response(self, item, accumulate: Dict[str, Any])
410
419
accumulate ["message" ]["tool_calls" ][- 1 ]["function" ]["arguments" ] += tool_call .function .arguments
411
420
412
421
def _wrapped_stream (
413
- self , stream_obj : _models .StreamingChatCompletions , span : "AbstractSpan"
422
+ self , stream_obj : _models .StreamingChatCompletions , span : "AbstractSpan" , previous_event_timestamp : int
414
423
) -> _models .StreamingChatCompletions :
415
424
class StreamWrapper (_models .StreamingChatCompletions ):
416
425
def __init__ (self , stream_obj , instrumentor ):
@@ -467,29 +476,27 @@ def __iter__( # pyright: ignore [reportIncompatibleMethodOverride]
467
476
accumulate ["message" ]["tool_calls" ] = list (
468
477
tool_calls_function_names_and_arguments_removed
469
478
)
470
-
471
- span .span_instance .add_event (
472
- name = "gen_ai.choice" ,
473
- attributes = {
474
- "gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
475
- "gen_ai.event.content" : json .dumps (accumulate ),
476
- },
477
- )
479
+ attributes = {
480
+ "gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
481
+ "gen_ai.event.content" : json .dumps (accumulate ),
482
+ }
483
+ self ._instrumentor ._record_event (span , "gen_ai.choice" , attributes , previous_event_timestamp )
478
484
span .finish ()
479
485
480
486
return StreamWrapper (stream_obj , self )
481
487
482
488
def _async_wrapped_stream (
483
- self , stream_obj : _models .AsyncStreamingChatCompletions , span : "AbstractSpan"
489
+ self , stream_obj : _models .AsyncStreamingChatCompletions , span : "AbstractSpan" , last_event_timestamp_ns : int
484
490
) -> _models .AsyncStreamingChatCompletions :
485
491
class AsyncStreamWrapper (_models .AsyncStreamingChatCompletions ):
486
- def __init__ (self , stream_obj , instrumentor , span ):
492
+ def __init__ (self , stream_obj , instrumentor , span , last_event_timestamp_ns ):
487
493
super ().__init__ (stream_obj ._response )
488
494
self ._instrumentor = instrumentor
489
495
self ._accumulate : Dict [str , Any ] = {}
490
496
self ._stream_obj = stream_obj
491
497
self .span = span
492
498
self ._last_result = None
499
+ self ._last_event_timestamp_ns = last_event_timestamp_ns
493
500
494
501
async def __anext__ (self ) -> "_models.StreamingChatCompletionsUpdate" :
495
502
try :
@@ -523,19 +530,44 @@ def _trace_stream_content(self) -> None:
523
530
self ._accumulate ["message" ]["tool_calls" ]
524
531
)
525
532
self ._accumulate ["message" ]["tool_calls" ] = list (tools_no_recording )
526
-
527
- self .span .span_instance .add_event (
528
- name = "gen_ai.choice" ,
529
- attributes = {
530
- "gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
531
- "gen_ai.event.content" : json .dumps (self ._accumulate ),
532
- },
533
- )
533
+ attributes = {
534
+ "gen_ai.system" : _INFERENCE_GEN_AI_SYSTEM_NAME ,
535
+ "gen_ai.event.content" : json .dumps (self ._accumulate ),
536
+ }
537
+ self ._last_event_timestamp_ns = self ._instrumentor ._record_event ( # pylint: disable=protected-access, line-too-long # pyright: ignore [reportFunctionMemberAccess]
538
+ span ,
539
+ "gen_ai.choice" ,
540
+ attributes ,
541
+ self ._last_event_timestamp_ns
542
+ )
534
543
span .finish ()
535
544
536
- async_stream_wrapper = AsyncStreamWrapper (stream_obj , self , span )
545
+ async_stream_wrapper = AsyncStreamWrapper (stream_obj , self , span , last_event_timestamp_ns )
537
546
return async_stream_wrapper
538
547
548
+ def _record_event (self , span : "AbstractSpan" , name : str ,
549
+ attributes : Dict [str , Any ], last_event_timestamp_ns : int
550
+ ) -> int :
551
+ timestamp = time_ns ()
552
+
553
+ # we're recording multiple events, some of them are emitted within (hundreds of) nanoseconds of each other.
554
+ # time.time_ns resolution is not high enough on windows to guarantee unique timestamps for each message.
555
+ # Also Azure Monitor truncates resolution to microseconds and some other backends truncate to milliseconds.
556
+ #
557
+ # But we need to give users a way to restore event order, so we're incrementing the timestamp
558
+ # by 1 microsecond for each message.
559
+ #
560
+ # This is a workaround, we'll find a generic and better solution - see
561
+ # https://github.com/open-telemetry/semantic-conventions/issues/1701
562
+ if last_event_timestamp_ns > 0 and timestamp <= (last_event_timestamp_ns + 1000 ):
563
+ timestamp = last_event_timestamp_ns + 1000
564
+
565
+ span .span_instance .add_event (name = name ,
566
+ attributes = attributes ,
567
+ timestamp = timestamp )
568
+
569
+ return timestamp
570
+
539
571
def _trace_sync_function (
540
572
self ,
541
573
function : Callable ,
@@ -580,16 +612,16 @@ def inner(*args, **kwargs):
580
612
name = span_name ,
581
613
kind = SpanKind .CLIENT , # pyright: ignore [reportPossiblyUnboundVariable]
582
614
)
615
+
583
616
try :
584
617
# tracing events not supported in azure-core-tracing-opentelemetry
585
618
# so need to access the span instance directly
586
619
with span_impl_type .change_context (span .span_instance ):
587
- self ._add_request_span_attributes (span , span_name , args , kwargs )
620
+ last_event_timestamp_ns = self ._add_request_details (span , args , kwargs )
588
621
result = function (* args , ** kwargs )
589
622
if kwargs .get ("stream" ) is True :
590
- return self ._wrapped_stream (result , span )
591
- self ._add_response_span_attributes (span , result )
592
-
623
+ return self ._wrapped_stream (result , span , last_event_timestamp_ns )
624
+ self ._add_response_details (span , result , last_event_timestamp_ns )
593
625
except Exception as exc :
594
626
# Set the span status to error
595
627
if isinstance (span .span_instance , Span ): # pyright: ignore [reportPossiblyUnboundVariable]
@@ -659,11 +691,11 @@ async def inner(*args, **kwargs):
659
691
# tracing events not supported in azure-core-tracing-opentelemetry
660
692
# so need to access the span instance directly
661
693
with span_impl_type .change_context (span .span_instance ):
662
- self ._add_request_span_attributes (span , span_name , args , kwargs )
694
+ last_event_timestamp_ns = self ._add_request_details (span , args , kwargs )
663
695
result = await function (* args , ** kwargs )
664
696
if kwargs .get ("stream" ) is True :
665
- return self ._async_wrapped_stream (result , span )
666
- self ._add_response_span_attributes (span , result )
697
+ return self ._async_wrapped_stream (result , span , last_event_timestamp_ns )
698
+ self ._add_response_details (span , result , last_event_timestamp_ns )
667
699
668
700
except Exception as exc :
669
701
# Set the span status to error
0 commit comments