Skip to content

Commit 4b972d8

Browse files
committed
Updated dynatrace
1 parent de7a6aa commit 4b972d8

File tree

1 file changed

+54
-32
lines changed

1 file changed

+54
-32
lines changed

src/opentelemetry_mcp/backends/dynatrace.py

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)