@@ -195,21 +195,15 @@ async def search_spans(self, query: SpanQuery) -> list[SpanData]:
195195 f"{ [(f .field , f .operator .value ) for f in client_filters ]} "
196196 )
197197
198- # Convert SpanQuery to TraceQuery for Dynatrace API
198+ # Convert SpanQuery to a minimal TraceQuery for Dynatrace API:
199+ # use it only to bound the search window and basic scoping
200+ # and rely on client-side filtering for span-level predicates.
199201 trace_query = TraceQuery (
200202 service_name = query .service_name ,
201203 operation_name = query .operation_name ,
202204 start_time = query .start_time ,
203205 end_time = query .end_time ,
204- min_duration_ms = query .min_duration_ms ,
205- max_duration_ms = query .max_duration_ms ,
206- tags = query .tags ,
207206 limit = query .limit * 2 , # Fetch more traces to ensure we get enough spans
208- has_error = query .has_error ,
209- gen_ai_system = query .gen_ai_system ,
210- gen_ai_request_model = query .gen_ai_request_model ,
211- gen_ai_response_model = query .gen_ai_response_model ,
212- filters = query .filters ,
213207 )
214208
215209 # Search traces
@@ -473,16 +467,25 @@ def _parse_dynatrace_span(
473467 span_id = str (span_id_raw )
474468 operation_name = str (operation_name_raw )
475469
476- # Parse timestamps (Dynatrace uses milliseconds since epoch)
470+ # Parse timestamps (Dynatrace uses milliseconds since epoch) and normalize to UTC
477471 start_time_ms = span_data .get ("startTime" , span_data .get ("start_time" , 0 ))
478472 if isinstance (start_time_ms , str ):
479- # Try to parse ISO format
473+ # Try to parse ISO format first
480474 try :
481475 start_time = datetime .fromisoformat (start_time_ms .replace ("Z" , "+00:00" ))
476+ if start_time .tzinfo is None :
477+ start_time = start_time .replace (tzinfo = timezone .utc )
478+ else :
479+ start_time = start_time .astimezone (timezone .utc )
482480 except Exception :
483- start_time = datetime .fromtimestamp (int (start_time_ms ) / 1000 )
481+ # Fallback: treat as milliseconds since epoch
482+ start_time = datetime .fromtimestamp (
483+ int (start_time_ms ) / 1000 , tz = timezone .utc
484+ )
484485 else :
485- start_time = datetime .fromtimestamp (start_time_ms / 1000 , tz = timezone .utc )
486+ start_time = datetime .fromtimestamp (int (start_time_ms ) / 1000 , tz = timezone .utc )
487+
488+
486489
487490 duration_ms = span_data .get ("duration" , span_data .get ("duration_ms" , 0 ))
488491 if isinstance (duration_ms , str ):
@@ -539,28 +542,47 @@ def _parse_dynatrace_span(
539542
540543 # Parse events/logs
541544 events : list [SpanEvent ] = []
542- for event_data in span_data .get ("events" , span_data .get ("logs" , [])):
543- event_attrs : dict [str , str | int | float | bool ] = {}
544- if isinstance (event_data , dict ):
545- if "attributes" in event_data :
546- event_attrs .update (event_data ["attributes" ])
547- elif "fields" in event_data :
548- # Handle Jaeger-style fields
549- for field in event_data ["fields" ]:
550- if isinstance (field , dict ):
551- key = field .get ("key" )
552- value = field .get ("value" )
553- if key :
554- event_attrs [key ] = value
555-
556- event_name = event_data .get ("name" , "event" ) if isinstance (event_data , dict ) else "event"
557- event_timestamp = (
558- event_data .get ("timestamp" , 0 ) if isinstance (event_data , dict ) else 0
559- )
545+ events_source = span_data .get ("events" )
546+ if events_source is None :
547+ events_source = span_data .get ("logs" , [])
548+
549+ for event_data in events_source :
550+ if not isinstance (event_data , dict ):
551+ continue
560552
553+ event_attrs : dict [str , str | int | float | bool ] = {}
554+ if "attributes" in event_data and isinstance (event_data ["attributes" ], dict ):
555+ event_attrs .update (event_data ["attributes" ])
556+ elif "fields" in event_data :
557+ # Handle Jaeger-style fields
558+ for field in event_data ["fields" ] or []:
559+ if isinstance (field , dict ):
560+ key = field .get ("key" )
561+ value = field .get ("value" )
562+ if key :
563+ event_attrs [key ] = value
564+
565+ event_name = event_data .get ("name" , "event" )
566+
567+ raw_ts = event_data .get ("timestamp" , 0 )
568+ if isinstance (raw_ts , str ):
569+ try :
570+ dt = datetime .fromisoformat (raw_ts .replace ("Z" , "+00:00" ))
571+ if dt .tzinfo is None :
572+ dt = dt .replace (tzinfo = timezone .utc )
573+ else :
574+ dt = dt .astimezone (timezone .utc )
575+ event_timestamp = int (dt .timestamp () * 1_000_000_000 )
576+ except Exception :
577+ event_timestamp = 0
578+ elif isinstance (raw_ts , (int , float )):
579+ # Dynatrace timestamps are typically in milliseconds; convert to nanoseconds
580+ event_timestamp = int (raw_ts * 1_000_000 )
581+ else :
582+ event_timestamp = 0
561583 events .append (
562584 SpanEvent (
563- name = event_name ,
585+ name = str ( event_name ) ,
564586 timestamp = event_timestamp ,
565587 attributes = event_attrs ,
566588 )
0 commit comments