mcp-instana 0.1.1__py3-none-any.whl → 0.2.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.
Files changed (55) hide show
  1. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/METADATA +459 -138
  2. mcp_instana-0.2.0.dist-info/RECORD +59 -0
  3. src/application/application_analyze.py +373 -160
  4. src/application/application_catalog.py +3 -1
  5. src/application/application_global_alert_config.py +653 -0
  6. src/application/application_metrics.py +6 -2
  7. src/application/application_resources.py +3 -1
  8. src/application/application_settings.py +966 -370
  9. src/application/application_topology.py +6 -2
  10. src/automation/action_catalog.py +416 -0
  11. src/automation/action_history.py +338 -0
  12. src/core/server.py +159 -9
  13. src/core/utils.py +2 -2
  14. src/event/events_tools.py +602 -275
  15. src/infrastructure/infrastructure_analyze.py +7 -3
  16. src/infrastructure/infrastructure_catalog.py +3 -1
  17. src/infrastructure/infrastructure_metrics.py +6 -2
  18. src/infrastructure/infrastructure_resources.py +7 -5
  19. src/infrastructure/infrastructure_topology.py +5 -3
  20. src/prompts/__init__.py +16 -0
  21. src/prompts/application/__init__.py +1 -0
  22. src/prompts/application/application_alerts.py +54 -0
  23. src/prompts/application/application_catalog.py +26 -0
  24. src/prompts/application/application_metrics.py +57 -0
  25. src/prompts/application/application_resources.py +26 -0
  26. src/prompts/application/application_settings.py +75 -0
  27. src/prompts/application/application_topology.py +30 -0
  28. src/prompts/events/__init__.py +1 -0
  29. src/prompts/events/events_tools.py +161 -0
  30. src/prompts/infrastructure/infrastructure_analyze.py +72 -0
  31. src/prompts/infrastructure/infrastructure_catalog.py +53 -0
  32. src/prompts/infrastructure/infrastructure_metrics.py +45 -0
  33. src/prompts/infrastructure/infrastructure_resources.py +74 -0
  34. src/prompts/infrastructure/infrastructure_topology.py +38 -0
  35. src/prompts/settings/__init__.py +0 -0
  36. src/prompts/settings/custom_dashboard.py +157 -0
  37. src/prompts/website/__init__.py +1 -0
  38. src/prompts/website/website_analyze.py +35 -0
  39. src/prompts/website/website_catalog.py +40 -0
  40. src/prompts/website/website_configuration.py +105 -0
  41. src/prompts/website/website_metrics.py +34 -0
  42. src/settings/__init__.py +1 -0
  43. src/settings/custom_dashboard_tools.py +417 -0
  44. src/website/__init__.py +0 -0
  45. src/website/website_analyze.py +433 -0
  46. src/website/website_catalog.py +171 -0
  47. src/website/website_configuration.py +770 -0
  48. src/website/website_metrics.py +241 -0
  49. mcp_instana-0.1.1.dist-info/RECORD +0 -30
  50. src/prompts/mcp_prompts.py +0 -900
  51. src/prompts/prompt_loader.py +0 -29
  52. src/prompts/prompt_registry.json +0 -21
  53. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +0 -0
  54. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/entry_points.txt +0 -0
  55. {mcp_instana-0.1.1.dist-info → mcp_instana-0.2.0.dist-info}/licenses/LICENSE.md +0 -0
src/event/events_tools.py CHANGED
@@ -1,34 +1,171 @@
1
+
1
2
  """
2
3
  Agent Monitoring Events MCP Tools Module
3
4
 
4
5
  This module provides agent monitoring events-specific MCP tools for Instana monitoring.
5
6
  """
6
7
 
8
+ import json
7
9
  import logging
10
+ import re
8
11
  from datetime import datetime
9
- from typing import Any, Dict, Optional
10
-
11
- # Import the correct class name (EventsApi with lowercase 'i')
12
- from instana_client.api.events_api import EventsApi
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+ try:
15
+ from instana_client.api.events_api import (
16
+ EventsApi,
17
+ )
18
+ try:
19
+ has_get_events_id_query = True
20
+ except ImportError:
21
+ has_get_events_id_query = False
22
+ except ImportError:
23
+ import logging
24
+ logger = logging.getLogger(__name__)
25
+ logger.error("Failed to import event resources API", exc_info=True)
26
+ raise
13
27
 
14
28
  from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
15
29
 
16
- # Configure logger for this module
17
30
  logger = logging.getLogger(__name__)
18
31
 
19
32
  class AgentMonitoringEventsMCPTools(BaseInstanaClient):
20
- """Tools for agent monitoring events in Instana MCP."""
21
33
 
22
34
  def __init__(self, read_token: str, base_url: str):
23
- """Initialize the Agent Monitoring Events MCP tools client."""
24
35
  super().__init__(read_token=read_token, base_url=base_url)
25
36
 
37
+ def _process_time_range(self, time_range=None, from_time=None, to_time=None):
38
+ """
39
+ Process time range parameters to get standardized from_time and to_time values.
40
+
41
+ Args:
42
+ time_range: Natural language time range like "last 24 hours"
43
+ from_time: Start timestamp in milliseconds (optional)
44
+ to_time: End timestamp in milliseconds (optional)
45
+
46
+ Returns:
47
+ Tuple of (from_time, to_time) in milliseconds
48
+ """
49
+ # Current time in milliseconds
50
+ current_time_ms = int(datetime.now().timestamp() * 1000)
51
+
52
+ # Process natural language time range if provided
53
+ if time_range:
54
+ logger.debug(f"Processing natural language time range: '{time_range}'")
55
+
56
+ # Default to 24 hours if just "last few hours" is specified
57
+ if time_range.lower() in ["last few hours", "last hours", "few hours"]:
58
+ hours = 24
59
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
60
+ to_time = current_time_ms
61
+ # Extract hours if specified
62
+ elif "hour" in time_range.lower():
63
+ hour_match = re.search(r'(\d+)\s*hour', time_range.lower())
64
+ hours = int(hour_match.group(1)) if hour_match else 24
65
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
66
+ to_time = current_time_ms
67
+ # Extract days if specified
68
+ elif "day" in time_range.lower():
69
+ day_match = re.search(r'(\d+)\s*day', time_range.lower())
70
+ days = int(day_match.group(1)) if day_match else 1
71
+ from_time = current_time_ms - (days * 24 * 60 * 60 * 1000)
72
+ to_time = current_time_ms
73
+ # Handle "last week"
74
+ elif "week" in time_range.lower():
75
+ week_match = re.search(r'(\d+)\s*week', time_range.lower())
76
+ weeks = int(week_match.group(1)) if week_match else 1
77
+ from_time = current_time_ms - (weeks * 7 * 24 * 60 * 60 * 1000)
78
+ to_time = current_time_ms
79
+ # Handle "last month"
80
+ elif "month" in time_range.lower():
81
+ month_match = re.search(r'(\d+)\s*month', time_range.lower())
82
+ months = int(month_match.group(1)) if month_match else 1
83
+ from_time = current_time_ms - (months * 30 * 24 * 60 * 60 * 1000)
84
+ to_time = current_time_ms
85
+ # Default to 24 hours for any other time range
86
+ else:
87
+ hours = 24
88
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
89
+ to_time = current_time_ms
90
+
91
+ # Set default time range if not provided
92
+ if not to_time:
93
+ to_time = current_time_ms
94
+ if not from_time:
95
+ from_time = to_time - (24 * 60 * 60 * 1000) # Default to 24 hours
96
+
97
+ return from_time, to_time
98
+
99
+ def _process_result(self, result):
100
+
101
+ # Convert the result to a dictionary
102
+ if hasattr(result, 'to_dict'):
103
+ result_dict = result.to_dict()
104
+ elif isinstance(result, list):
105
+ # Convert list items if they have to_dict method
106
+ items = []
107
+ for item in result:
108
+ if hasattr(item, 'to_dict'):
109
+ items.append(item.to_dict())
110
+ else:
111
+ items.append(item)
112
+ # Wrap list in a dictionary
113
+ result_dict = {"items": items, "count": len(items)}
114
+ elif isinstance(result, dict):
115
+ # If it's already a dict, use it as is
116
+ result_dict = result
117
+ else:
118
+ # For any other format, convert to string and wrap in dict
119
+ result_dict = {"data": str(result)}
120
+
121
+ return result_dict
122
+
123
+ def _summarize_events_result(self, events, total_count=None, max_events=None):
124
+
125
+ if not events:
126
+ return {"events_count": 0, "summary": "No events found"}
127
+
128
+ # Use provided total count or length of events list
129
+ total_events_count = total_count or len(events)
130
+
131
+ # Limit events if max_events is specified
132
+ if max_events and len(events) > max_events:
133
+ events = events[:max_events]
134
+
135
+ # Group events by type
136
+ event_types = {}
137
+ for event in events:
138
+ event_type = event.get("eventType", "Unknown")
139
+ if event_type not in event_types:
140
+ event_types[event_type] = 0
141
+ event_types[event_type] += 1
142
+
143
+ # Sort event types by count
144
+ sorted_types = sorted(event_types.items(), key=lambda x: x[1], reverse=True)
145
+
146
+ # Create summary
147
+ summary = {
148
+ "events_count": total_events_count,
149
+ "events_analyzed": len(events),
150
+ "event_types": dict(sorted_types),
151
+ "top_event_types": sorted_types[:5] if len(sorted_types) > 5 else sorted_types
152
+ }
153
+
154
+ return summary
155
+
26
156
  @register_as_tool
27
157
  @with_header_auth(EventsApi)
28
158
  async def get_event(self, event_id: str, ctx=None, api_client=None) -> Dict[str, Any]:
29
159
  """
30
160
  Get a specific event by ID.
31
161
 
162
+ This tool retrieves detailed information about a specific event using its unique ID.
163
+ Use this when you need to examine a particular event's details, severity, or related entities.
164
+
165
+ Examples:
166
+ Get details of a specific incident:
167
+ - event_id: "1a2b3c4d5e6f"
168
+
32
169
  Args:
33
170
  event_id: The ID of the event to retrieve
34
171
  ctx: The MCP context (optional)
@@ -38,13 +175,68 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
38
175
  Dictionary containing the event data or error information
39
176
  """
40
177
  try:
41
- # Call the get_event method from the SDK
42
- result = api_client.get_event(event_id=event_id)
178
+ logger.debug(f"get_event called with event_id={event_id}")
179
+
180
+ if not event_id:
181
+ return {"error": "event_id parameter is required"}
182
+
183
+ # Try standard API call first
184
+ try:
185
+ result = api_client.get_event(event_id=event_id)
186
+
187
+ # New robust conversion to dict
188
+ if hasattr(result, "to_dict"):
189
+ result_dict = result.to_dict()
190
+ elif isinstance(result, dict):
191
+ result_dict = result
192
+ else:
193
+ # Convert to dictionary using __dict__ or as a fallback, create a new dict with string representation
194
+ result_dict = getattr(result, "__dict__", {"data": str(result)})
195
+
196
+ logger.debug(f"Successfully retrieved event with ID {event_id}")
197
+ return result_dict
198
+
199
+ except Exception as api_error:
200
+ # Check for specific error types
201
+ if hasattr(api_error, 'status'):
202
+ if api_error.status == 404:
203
+ return {"error": f"Event with ID {event_id} not found", "event_id": event_id}
204
+ elif api_error.status in (401, 403):
205
+ return {"error": "Authentication failed. Please check your API token and permissions."}
206
+
207
+ # Try fallback approach
208
+ logger.warning(f"Standard API call failed: {api_error}, trying fallback approach")
209
+
210
+ # Use the without_preload_content version to get the raw response
211
+ try:
212
+ response_data = api_client.get_event_without_preload_content(event_id=event_id)
213
+
214
+ # Check if the response was successful
215
+ if response_data.status != 200:
216
+ error_message = f"Failed to get event: HTTP {response_data.status}"
217
+ logger.error(error_message)
218
+ return {"error": error_message, "event_id": event_id}
219
+
220
+ # Read the response content
221
+ response_text = response_data.data.decode('utf-8')
222
+
223
+ # Parse the JSON manually
224
+ try:
225
+ result_dict = json.loads(response_text)
226
+ logger.debug(f"Successfully retrieved event with ID {event_id} using fallback")
227
+ return result_dict
228
+ except json.JSONDecodeError as json_err:
229
+ error_message = f"Failed to parse JSON response: {json_err}"
230
+ logger.error(error_message)
231
+ return {"error": error_message, "event_id": event_id}
232
+
233
+ except Exception as fallback_error:
234
+ logger.error(f"Fallback approach failed: {fallback_error}")
235
+ raise
43
236
 
44
- return result
45
237
  except Exception as e:
46
238
  logger.error(f"Error in get_event: {e}", exc_info=True)
47
- return {"error": f"Failed to get event: {e!s}"}
239
+ return {"error": f"Failed to get event: {e!s}", "event_id": event_id}
48
240
 
49
241
  @register_as_tool
50
242
  @with_header_auth(EventsApi)
@@ -52,7 +244,7 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
52
244
  from_time: Optional[int] = None,
53
245
  to_time: Optional[int] = None,
54
246
  time_range: Optional[str] = None,
55
- max_events: Optional[int] = 50, # Added parameter to limit events
247
+ max_events: Optional[int] = 50,
56
248
  ctx=None, api_client=None) -> Dict[str, Any]:
57
249
  """
58
250
  Get Kubernetes info events based on the provided parameters and return a detailed analysis.
@@ -61,6 +253,10 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
61
253
  their details, and actionable fix suggestions. You can specify a time range using timestamps or natural language
62
254
  like "last 24 hours" or "last 2 days".
63
255
 
256
+ Examples:
257
+ Get Kubernetes events from the last 24 hours:
258
+ - time_range: "last 24 hours"
259
+
64
260
  Args:
65
261
  from_time: Start timestamp in milliseconds since epoch (optional)
66
262
  to_time: End timestamp in milliseconds since epoch (optional)
@@ -73,108 +269,45 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
73
269
  Dictionary containing detailed Kubernetes events analysis or error information
74
270
  """
75
271
  try:
76
- # Process natural language time range if provided
77
- if time_range:
78
- logger.debug(f"Processing natural language time range: '{time_range}'")
79
-
80
- # Current time in milliseconds
81
- current_time_ms = int(datetime.now().timestamp() * 1000)
82
-
83
- # Default to 24 hours if just "last few hours" is specified
84
- if time_range.lower() in ["last few hours", "last hours", "few hours"]:
85
- hours = 24
86
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
87
- to_time = current_time_ms
88
- logger.debug(f"Interpreted as last {hours} hours")
89
- # Extract hours if specified
90
- elif "hour" in time_range.lower():
91
- import re
92
- hour_match = re.search(r'(\d+)\s*hour', time_range.lower())
93
- hours = int(hour_match.group(1)) if hour_match else 24
94
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
95
- to_time = current_time_ms
96
- # Extract days if specified
97
- elif "day" in time_range.lower():
98
- import re
99
- day_match = re.search(r'(\d+)\s*day', time_range.lower())
100
- days = int(day_match.group(1)) if day_match else 1
101
- from_time = current_time_ms - (days * 24 * 60 * 60 * 1000)
102
- to_time = current_time_ms
103
- # Handle "last week"
104
- elif "week" in time_range.lower():
105
- import re
106
- week_match = re.search(r'(\d+)\s*week', time_range.lower())
107
- weeks = int(week_match.group(1)) if week_match else 1
108
- from_time = current_time_ms - (weeks * 7 * 24 * 60 * 60 * 1000)
109
- to_time = current_time_ms
110
- # Handle "last month"
111
- elif "month" in time_range.lower():
112
- import re
113
- month_match = re.search(r'(\d+)\s*month', time_range.lower())
114
- months = int(month_match.group(1)) if month_match else 1
115
- from_time = current_time_ms - (months * 30 * 24 * 60 * 60 * 1000)
116
- to_time = current_time_ms
117
- # Default to 24 hours for any other time range
118
- else:
119
- hours = 24
120
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
121
- to_time = current_time_ms
122
-
123
- # Set default time range if not provided
124
- if not to_time:
125
- to_time = int(datetime.now().timestamp() * 1000)
126
- if not from_time:
127
- from_time = to_time - (24 * 60 * 60 * 1000) # Default to 24 hours
128
-
129
- # Call the kubernetes_info_events method from the SDK
130
- result = api_client.kubernetes_info_events(
131
- to=to_time,
132
- var_from=from_time,
133
- window_size=None,
134
- filter_event_updates=None,
135
- exclude_triggered_before=None
136
- )
137
-
138
- # Print the raw result for debugging
139
- logger.debug(f"Raw API result type: {type(result)}")
140
- logger.debug(f"Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}")
141
-
142
- # If there are no events, return early
143
- if not result or (isinstance(result, list) and len(result) == 0):
144
- from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
145
- to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
272
+ logger.debug(f"get_kubernetes_info_events called with time_range={time_range}, from_time={from_time}, to_time={to_time}, max_events={max_events}")
273
+ from_time, to_time = self._process_time_range(time_range, from_time, to_time)
274
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
275
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
276
+ try:
277
+ result = api_client.kubernetes_info_events(
278
+ var_from=from_time,
279
+ to=to_time,
280
+ window_size=max_events,
281
+ filter_event_updates=None,
282
+ exclude_triggered_before=None
283
+ )
284
+ logger.debug(f"Raw API result type: {type(result)}")
285
+ logger.debug(f"Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}")
286
+ except Exception as api_error:
287
+ logger.error(f"API call failed: {api_error}", exc_info=True)
146
288
  return {
147
- "analysis": f"No Kubernetes events found between {from_date} and {to_date}.",
148
- "time_range": f"{from_date} to {to_date}",
149
- "events_count": 0
289
+ "error": f"Failed to get Kubernetes info events: {api_error}",
290
+ "details": str(api_error)
150
291
  }
151
-
152
- # Process the events to create a summary
153
- events = result if isinstance(result, list) else [result]
154
-
155
- # Get the total number of events before limiting
292
+ events = result if isinstance(result, list) else ([result] if result else [])
156
293
  total_events_count = len(events)
157
-
158
- # Limit the number of events to process
159
294
  events = events[:max_events]
160
- logger.debug(f"Limited to processing {len(events)} events out of {total_events_count} total events")
161
-
162
- # Convert InfraEventResult objects to dictionaries if needed
163
295
  event_dicts = []
164
296
  for event in events:
165
297
  if hasattr(event, 'to_dict'):
166
298
  event_dicts.append(event.to_dict())
167
299
  else:
168
300
  event_dicts.append(event)
169
-
170
- # Group events by problem type
301
+ if not event_dicts:
302
+ return {
303
+ "events": [],
304
+ "events_count": 0,
305
+ "time_range": f"{from_date} to {to_date}",
306
+ "analysis": f"No Kubernetes events found between {from_date} and {to_date}."
307
+ }
171
308
  problem_groups = {}
172
-
173
- # Process each event
174
309
  for event in event_dicts:
175
310
  problem = event.get("problem", "Unknown")
176
-
177
- # Initialize problem group if not exists
178
311
  if problem not in problem_groups:
179
312
  problem_groups[problem] = {
180
313
  "count": 0,
@@ -184,27 +317,18 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
184
317
  "fix_suggestions": set(),
185
318
  "sample_events": []
186
319
  }
187
-
188
- # Update problem group
189
320
  problem_groups[problem]["count"] += 1
190
-
191
- # Extract namespace from entityLabel
192
321
  entity_label = event.get("entityLabel", "")
193
322
  if "/" in entity_label:
194
323
  namespace, entity = entity_label.split("/", 1)
195
324
  problem_groups[problem]["affected_namespaces"].add(namespace)
196
325
  problem_groups[problem]["affected_entities"].add(entity)
197
-
198
- # Add detail and fix suggestion
199
326
  detail = event.get("detail", "")
200
327
  if detail:
201
328
  problem_groups[problem]["details"].add(detail)
202
-
203
329
  fix_suggestion = event.get("fixSuggestion", "")
204
330
  if fix_suggestion:
205
331
  problem_groups[problem]["fix_suggestions"].add(fix_suggestion)
206
-
207
- # Add sample event (up to 3 per problem)
208
332
  if len(problem_groups[problem]["sample_events"]) < 3:
209
333
  simple_event = {
210
334
  "eventId": event.get("eventId", ""),
@@ -213,20 +337,9 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
213
337
  "detail": detail
214
338
  }
215
339
  problem_groups[problem]["sample_events"].append(simple_event)
216
-
217
- # Sort problems by count (most frequent first)
218
340
  sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
219
-
220
- # Format the time range in a human-readable format
221
- from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
222
- to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
223
-
224
- # Create a detailed analysis of each problem
225
341
  problem_analyses = []
226
-
227
- # Process each problem
228
342
  for problem_name, problem_data in sorted_problems:
229
- # Create a detailed problem analysis
230
343
  problem_analysis = {
231
344
  "problem": problem_name,
232
345
  "count": problem_data["count"],
@@ -235,55 +348,39 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
235
348
  "fix_suggestions": list(problem_data["fix_suggestions"]),
236
349
  "sample_events": problem_data["sample_events"]
237
350
  }
238
-
239
351
  problem_analyses.append(problem_analysis)
240
-
241
- # Create a comprehensive analysis
242
352
  analysis_result = {
243
353
  "summary": f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.",
244
354
  "time_range": f"{from_date} to {to_date}",
245
355
  "events_count": total_events_count,
246
356
  "events_analyzed": len(events),
247
- "problem_analyses": problem_analyses[:10] # Limit to top 10 problems for readability
357
+ "problem_analyses": problem_analyses[:10]
248
358
  }
249
-
250
- # Create a more user-friendly text summary for direct display
251
359
  markdown_summary = "# Kubernetes Events Analysis\n\n"
252
360
  markdown_summary += f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.\n\n"
253
-
254
361
  markdown_summary += "## Top Problems\n\n"
255
-
256
- # Add each problem to the markdown summary
257
- for problem_analysis in problem_analyses[:5]: # Limit to top 5 for readability
362
+ for problem_analysis in problem_analyses[:5]:
258
363
  problem_name = problem_analysis["problem"]
259
364
  count = problem_analysis["count"]
260
-
261
365
  markdown_summary += f"### {problem_name} ({count} events)\n\n"
262
-
263
- # Add affected namespaces if available
264
366
  if problem_analysis.get("affected_namespaces"):
265
367
  namespaces = ", ".join(problem_analysis["affected_namespaces"][:5])
266
368
  if len(problem_analysis["affected_namespaces"]) > 5:
267
369
  namespaces += f" and {len(problem_analysis['affected_namespaces']) - 5} more"
268
370
  markdown_summary += f"**Affected Namespaces:** {namespaces}\n\n"
269
-
270
- # Add fix suggestions
271
371
  if problem_analysis.get("fix_suggestions"):
272
372
  markdown_summary += "**Fix Suggestions:**\n\n"
273
- for suggestion in list(problem_analysis["fix_suggestions"])[:3]: # Limit to top 3 suggestions
373
+ for suggestion in list(problem_analysis["fix_suggestions"])[:3]:
274
374
  markdown_summary += f"- {suggestion}\n"
275
-
276
375
  markdown_summary += "\n"
277
-
278
- # Add the markdown summary to the result
279
376
  analysis_result["markdown_summary"] = markdown_summary
280
-
377
+ analysis_result["events"] = event_dicts
281
378
  return analysis_result
282
-
283
379
  except Exception as e:
284
380
  logger.error(f"Error in get_kubernetes_info_events: {e}", exc_info=True)
285
381
  return {
286
- "error": f"Failed to get Kubernetes info events: {e!s}"
382
+ "error": f"Failed to get Kubernetes info events: {e!s}",
383
+ "details": str(e)
287
384
  }
288
385
 
289
386
  @register_as_tool
@@ -293,8 +390,8 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
293
390
  from_time: Optional[int] = None,
294
391
  to_time: Optional[int] = None,
295
392
  size: Optional[int] = 100,
296
- max_events: Optional[int] = 50, # Added parameter to limit events
297
- time_range: Optional[str] = None, # Added parameter for natural language time range
393
+ max_events: Optional[int] = 50,
394
+ time_range: Optional[str] = None,
298
395
  ctx=None, api_client=None) -> Dict[str, Any]:
299
396
  """
300
397
  Get agent monitoring events from Instana and return a detailed analysis.
@@ -303,6 +400,10 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
303
400
  monitoring issues, their frequency, and affected entities. You can specify a time range using timestamps
304
401
  or natural language like "last 24 hours" or "last 2 days".
305
402
 
403
+ Examples:
404
+ Get agent monitoring events from the last 24 hours:
405
+ - time_range: "last 24 hours"
406
+
306
407
  Args:
307
408
  query: Query string to filter events (optional)
308
409
  from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
@@ -317,114 +418,48 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
317
418
  Dictionary containing summarized agent monitoring events data or error information
318
419
  """
319
420
  try:
320
- # Process natural language time range if provided
321
- if time_range:
322
- logger.debug(f"Processing natural language time range: '{time_range}'")
323
-
324
- # Current time in milliseconds
325
- current_time_ms = int(datetime.now().timestamp() * 1000)
326
-
327
- # Default to 24 hours if just "last few hours" is specified
328
- if time_range.lower() in ["last few hours", "last hours", "few hours"]:
329
- hours = 24
330
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
331
- to_time = current_time_ms
332
- logger.debug(f"Interpreted as last {hours} hours")
333
- # Extract hours if specified
334
- elif "hour" in time_range.lower():
335
- import re
336
- hour_match = re.search(r'(\d+)\s*hour', time_range.lower())
337
- hours = int(hour_match.group(1)) if hour_match else 24
338
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
339
- to_time = current_time_ms
340
- # Extract days if specified
341
- elif "day" in time_range.lower():
342
- import re
343
- day_match = re.search(r'(\d+)\s*day', time_range.lower())
344
- days = int(day_match.group(1)) if day_match else 1
345
- from_time = current_time_ms - (days * 24 * 60 * 60 * 1000)
346
- to_time = current_time_ms
347
- # Handle "last week"
348
- elif "week" in time_range.lower():
349
- import re
350
- week_match = re.search(r'(\d+)\s*week', time_range.lower())
351
- weeks = int(week_match.group(1)) if week_match else 1
352
- from_time = current_time_ms - (weeks * 7 * 24 * 60 * 60 * 1000)
353
- to_time = current_time_ms
354
- # Handle "last month"
355
- elif "month" in time_range.lower():
356
- import re
357
- month_match = re.search(r'(\d+)\s*month', time_range.lower())
358
- months = int(month_match.group(1)) if month_match else 1
359
- from_time = current_time_ms - (months * 30 * 24 * 60 * 60 * 1000)
360
- to_time = current_time_ms
361
- # Default to 24 hours for any other time range
362
- else:
363
- hours = 24
364
- from_time = current_time_ms - (hours * 60 * 60 * 1000)
365
- to_time = current_time_ms
366
-
367
- logger.debug(f"get_agent_monitoring_events called with query={query}, from_time={from_time}, to_time={to_time}, size={size}")
368
-
369
- # Set default time range if not provided
370
- if not to_time:
371
- to_time = int(datetime.now().timestamp() * 1000)
372
-
421
+ logger.debug(f"get_agent_monitoring_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
422
+ from_time, to_time = self._process_time_range(time_range, from_time, to_time)
373
423
  if not from_time:
374
- from_time = to_time - (60 * 60 * 1000) # Default to 1 hour
375
-
376
- # Call the agent_monitoring_events method from the SDK
377
- result = api_client.agent_monitoring_events(
378
- to=to_time,
379
- var_from=from_time,
380
- window_size=None,
381
- filter_event_updates=None,
382
- exclude_triggered_before=None
383
- )
384
-
385
- # Print the raw result for debugging
386
- logger.debug(f"Raw API result type: {type(result)}")
387
- logger.debug(f"Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}")
388
-
389
- # If there are no events, return early
390
- if not result or (isinstance(result, list) and len(result) == 0):
391
- from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
392
- to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
424
+ from_time = to_time - (60 * 60 * 1000)
425
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
426
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
427
+ try:
428
+ result = api_client.agent_monitoring_events(
429
+ var_from=from_time,
430
+ to=to_time,
431
+ window_size=max_events,
432
+ filter_event_updates=None,
433
+ exclude_triggered_before=None
434
+ )
435
+ logger.debug(f"Raw API result type: {type(result)}")
436
+ logger.debug(f"Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}")
437
+ except Exception as api_error:
438
+ logger.error(f"API call failed: {api_error}", exc_info=True)
393
439
  return {
394
- "analysis": f"No agent monitoring events found between {from_date} and {to_date}.",
395
- "time_range": f"{from_date} to {to_date}",
396
- "events_count": 0
440
+ "error": f"Failed to get agent monitoring events: {api_error}",
441
+ "details": str(api_error)
397
442
  }
398
-
399
- # Process the events to create a summary
400
- events = result if isinstance(result, list) else [result]
401
-
402
- # Get the total number of events before limiting
443
+ events = result if isinstance(result, list) else ([result] if result else [])
403
444
  total_events_count = len(events)
404
-
405
- # Limit the number of events to process
406
445
  events = events[:max_events]
407
- logger.debug(f"Limited to processing {len(events)} events out of {total_events_count} total events")
408
-
409
- # Convert objects to dictionaries if needed
410
446
  event_dicts = []
411
447
  for event in events:
412
448
  if hasattr(event, 'to_dict'):
413
449
  event_dicts.append(event.to_dict())
414
450
  else:
415
451
  event_dicts.append(event)
416
-
417
- # Group events by problem type
452
+ if not event_dicts:
453
+ return {
454
+ "events": [],
455
+ "events_count": 0,
456
+ "time_range": f"{from_date} to {to_date}",
457
+ "analysis": f"No agent monitoring events found between {from_date} and {to_date}."
458
+ }
418
459
  problem_groups = {}
419
-
420
- # Process each event
421
460
  for event in event_dicts:
422
- # Extract the monitoring issue from the problem field
423
461
  full_problem = event.get("problem", "Unknown")
424
- # Strip "Monitoring issue: " prefix if present
425
462
  problem = full_problem.replace("Monitoring issue: ", "") if "Monitoring issue: " in full_problem else full_problem
426
-
427
- # Initialize problem group if not exists
428
463
  if problem not in problem_groups:
429
464
  problem_groups[problem] = {
430
465
  "count": 0,
@@ -432,20 +467,13 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
432
467
  "entity_types": set(),
433
468
  "sample_events": []
434
469
  }
435
-
436
- # Update problem group
437
470
  problem_groups[problem]["count"] += 1
438
-
439
- # Add entity information
440
471
  entity_name = event.get("entityName", "Unknown")
441
472
  entity_label = event.get("entityLabel", "Unknown")
442
473
  entity_type = event.get("entityType", "Unknown")
443
-
444
474
  entity_info = f"{entity_name} ({entity_label})"
445
475
  problem_groups[problem]["affected_entities"].add(entity_info)
446
476
  problem_groups[problem]["entity_types"].add(entity_type)
447
-
448
- # Add sample event (up to 3 per problem)
449
477
  if len(problem_groups[problem]["sample_events"]) < 3:
450
478
  simple_event = {
451
479
  "eventId": event.get("eventId", ""),
@@ -455,20 +483,9 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
455
483
  "severity": event.get("severity", 0)
456
484
  }
457
485
  problem_groups[problem]["sample_events"].append(simple_event)
458
-
459
- # Sort problems by count (most frequent first)
460
486
  sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
461
-
462
- # Format the time range in a human-readable format
463
- from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
464
- to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
465
-
466
- # Create a detailed analysis of each problem
467
487
  problem_analyses = []
468
-
469
- # Process each problem
470
488
  for problem_name, problem_data in sorted_problems:
471
- # Create a detailed problem analysis
472
489
  problem_analysis = {
473
490
  "problem": problem_name,
474
491
  "count": problem_data["count"],
@@ -476,48 +493,358 @@ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
476
493
  "entity_types": list(problem_data["entity_types"]),
477
494
  "sample_events": problem_data["sample_events"]
478
495
  }
479
-
480
496
  problem_analyses.append(problem_analysis)
481
-
482
- # Create a comprehensive analysis
483
497
  analysis_result = {
484
498
  "summary": f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.",
485
499
  "time_range": f"{from_date} to {to_date}",
486
500
  "events_count": total_events_count,
487
501
  "events_analyzed": len(events),
488
- "problem_analyses": problem_analyses[:10] # Limit to top 10 problems for readability
502
+ "problem_analyses": problem_analyses[:10]
489
503
  }
490
-
491
- # Create a more user-friendly text summary for direct display
492
504
  markdown_summary = "# Agent Monitoring Events Analysis\n\n"
493
505
  markdown_summary += f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.\n\n"
494
-
495
506
  markdown_summary += "## Top Monitoring Issues\n\n"
496
-
497
- # Add each problem to the markdown summary
498
- for problem_analysis in problem_analyses[:5]: # Limit to top 5 for readability
507
+ for problem_analysis in problem_analyses[:5]:
499
508
  problem_name = problem_analysis["problem"]
500
509
  count = problem_analysis["count"]
501
-
502
510
  markdown_summary += f"### {problem_name} ({count} events)\n\n"
503
-
504
- # Add affected entities if available
505
511
  if problem_analysis.get("affected_entities"):
506
512
  entities = ", ".join(problem_analysis["affected_entities"][:5])
507
513
  if len(problem_analysis["affected_entities"]) > 5:
508
514
  entities += f" and {len(problem_analysis['affected_entities']) - 5} more"
509
515
  markdown_summary += f"**Affected Entities:** {entities}\n\n"
510
-
511
516
  markdown_summary += "\n"
512
-
513
- # Add the markdown summary to the result
514
517
  analysis_result["markdown_summary"] = markdown_summary
515
-
518
+ analysis_result["events"] = event_dicts
516
519
  return analysis_result
517
-
518
520
  except Exception as e:
519
521
  logger.error(f"Error in get_agent_monitoring_events: {e}", exc_info=True)
520
522
  return {
521
- "error": f"Failed to get agent monitoring events: {e!s}"
523
+ "error": f"Failed to get agent monitoring events: {e!s}",
524
+ "details": str(e)
522
525
  }
523
526
 
527
+
528
+ @register_as_tool
529
+ @with_header_auth(EventsApi)
530
+ async def get_issues(self,
531
+ query: Optional[str] = None,
532
+ from_time: Optional[int] = None,
533
+ to_time: Optional[int] = None,
534
+ filter_event_updates: Optional[bool] = None,
535
+ exclude_triggered_before: Optional[int] = None,
536
+ max_events: Optional[int] = 50,
537
+ size: Optional[int] = 100,
538
+ time_range: Optional[str] = None,
539
+ ctx=None, api_client=None) -> Dict[str, Any]:
540
+ """
541
+ Get issue events from Instana based on the provided parameters.
542
+
543
+ This tool retrieves issue events from Instana based on specified filters and time range.
544
+ Issues are events that represent problems that need attention but are not critical.
545
+
546
+ Examples:
547
+ Get all issue events from the last 24 hours:
548
+ - time_range: "last 24 hours"
549
+
550
+ Args:
551
+ query: Query string to filter events (optional)
552
+ from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
553
+ to_time: End timestamp in milliseconds since epoch (optional, defaults to now)
554
+ filter_event_updates: Whether to filter event updates (optional)
555
+ exclude_triggered_before: Exclude events triggered before this timestamp (optional)
556
+ max_events: Maximum number of events to process (default: 50)
557
+ size: Maximum number of events to return from API (default: 100)
558
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
559
+ ctx: The MCP context (optional)
560
+ api_client: API client for testing (optional)
561
+
562
+ Returns:
563
+ Dictionary containing the list of issue events or error information
564
+ """
565
+
566
+ try:
567
+ logger.debug(f"get_issue_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
568
+ from_time, to_time = self._process_time_range(time_range, from_time, to_time)
569
+ if not from_time:
570
+ from_time = to_time - (60 * 60 * 1000)
571
+ try:
572
+ response_data = api_client.get_events_without_preload_content(
573
+ var_from=from_time,
574
+ to=to_time,
575
+ window_size=size,
576
+ filter_event_updates=filter_event_updates,
577
+ exclude_triggered_before=exclude_triggered_before,
578
+ event_type_filters=["issue"]
579
+ )
580
+ if response_data.status != 200:
581
+ return {"error": f"Failed to get issue events: HTTP {response_data.status}"}
582
+ response_text = response_data.data.decode('utf-8')
583
+ result = json.loads(response_text)
584
+ if isinstance(result, list):
585
+ result_dict = {"events": result, "events_count": len(result)}
586
+ else:
587
+ result_dict = result
588
+ return result_dict
589
+ except Exception as api_error:
590
+ logger.error(f"API call failed: {api_error}", exc_info=True)
591
+ return {"error": f"Failed to get issue events: {api_error}"}
592
+ except Exception as e:
593
+ logger.error(f"Error in get_issue_events: {e}", exc_info=True)
594
+ return {"error": f"Failed to get issue events: {e!s}"}
595
+
596
+ @register_as_tool
597
+ @with_header_auth(EventsApi)
598
+ async def get_incidents(self,
599
+ query: Optional[str] = None,
600
+ from_time: Optional[int] = None,
601
+ to_time: Optional[int] = None,
602
+ filter_event_updates: Optional[bool] = None,
603
+ exclude_triggered_before: Optional[int] = None,
604
+ max_events: Optional[int] = 50,
605
+ size: Optional[int] = 100,
606
+ time_range: Optional[str] = None,
607
+ ctx=None, api_client=None) -> Dict[str, Any]:
608
+ """
609
+ Get incident events from Instana based on the provided parameters.
610
+
611
+ This tool retrieves incident events from Instana based on specified filters and time range.
612
+ Incidents are critical events that require immediate attention.
613
+
614
+ Examples:
615
+ Get all incident events from the last 24 hours:
616
+ - time_range: "last 24 hours"
617
+
618
+ Args:
619
+ query: Query string to filter events (optional)
620
+ from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
621
+ to_time: End timestamp in milliseconds since epoch (optional, defaults to now)
622
+ filter_event_updates: Whether to filter event updates (optional)
623
+ exclude_triggered_before: Exclude events triggered before this timestamp (optional)
624
+ max_events: Maximum number of events to process (default: 50)
625
+ size: Maximum number of events to return from API (default: 100)
626
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
627
+ ctx: The MCP context (optional)
628
+ api_client: API client for testing (optional)
629
+
630
+ Returns:
631
+ Dictionary containing the list of incident events or error information
632
+ """
633
+
634
+ try:
635
+ logger.debug(f"get_incident_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
636
+ from_time, to_time = self._process_time_range(time_range, from_time, to_time)
637
+ if not from_time:
638
+ from_time = to_time - (60 * 60 * 1000)
639
+ try:
640
+ response_data = api_client.get_events_without_preload_content(
641
+ var_from=from_time,
642
+ to=to_time,
643
+ window_size=size,
644
+ filter_event_updates=filter_event_updates,
645
+ exclude_triggered_before=exclude_triggered_before,
646
+ event_type_filters=["incident"]
647
+ )
648
+ if response_data.status != 200:
649
+ return {"error": f"Failed to get incident events: HTTP {response_data.status}"}
650
+ response_text = response_data.data.decode('utf-8')
651
+ result = json.loads(response_text)
652
+ if isinstance(result, list):
653
+ result_dict = {"events": result, "events_count": len(result)}
654
+ else:
655
+ result_dict = result
656
+ return result_dict
657
+ except Exception as api_error:
658
+ logger.error(f"API call failed: {api_error}", exc_info=True)
659
+ return {"error": f"Failed to get incident events: {api_error}"}
660
+ except Exception as e:
661
+ logger.error(f"Error in get_incident_events: {e}", exc_info=True)
662
+ return {"error": f"Failed to get incident events: {e!s}"}
663
+
664
+ @register_as_tool
665
+ @with_header_auth(EventsApi)
666
+ async def get_changes(self,
667
+ query: Optional[str] = None,
668
+ from_time: Optional[int] = None,
669
+ to_time: Optional[int] = None,
670
+ filter_event_updates: Optional[bool] = None,
671
+ exclude_triggered_before: Optional[int] = None,
672
+ max_events: Optional[int] = 50,
673
+ size: Optional[int] = 100,
674
+ time_range: Optional[str] = None,
675
+ ctx=None, api_client=None) -> Dict[str, Any]:
676
+ """
677
+ Get change events from Instana based on the provided parameters.
678
+
679
+ This tool retrieves change events from Instana based on specified filters and time range.
680
+ Change events represent modifications to the system, such as deployments or configuration changes.
681
+
682
+ Examples:
683
+ Get all change events from the last 24 hours:
684
+ - time_range: "last 24 hours"
685
+
686
+ Args:
687
+ query: Query string to filter events (optional)
688
+ from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
689
+ to_time: End timestamp in milliseconds since epoch (optional, defaults to now)
690
+ filter_event_updates: Whether to filter event updates (optional)
691
+ exclude_triggered_before: Exclude events triggered before this timestamp (optional)
692
+ max_events: Maximum number of events to process (default: 50)
693
+ size: Maximum number of events to return from API (default: 100)
694
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
695
+ ctx: The MCP context (optional)
696
+ api_client: API client for testing (optional)
697
+
698
+ Returns:
699
+ Dictionary containing the list of change events or error information
700
+ """
701
+
702
+ try:
703
+ logger.debug(f"get_change_events called with query={query}, time_range={time_range}, from_time={from_time}, to_time={to_time}, size={size}")
704
+ from_time, to_time = self._process_time_range(time_range, from_time, to_time)
705
+ if not from_time:
706
+ from_time = to_time - (60 * 60 * 1000)
707
+ try:
708
+ response_data = api_client.get_events_without_preload_content(
709
+ var_from=from_time,
710
+ to=to_time,
711
+ window_size=size,
712
+ filter_event_updates=filter_event_updates,
713
+ exclude_triggered_before=exclude_triggered_before,
714
+ event_type_filters=["change"]
715
+ )
716
+ if response_data.status != 200:
717
+ return {"error": f"Failed to get change events: HTTP {response_data.status}"}
718
+ response_text = response_data.data.decode('utf-8')
719
+ result = json.loads(response_text)
720
+ if isinstance(result, list):
721
+ result_dict = {"events": result, "events_count": len(result)}
722
+ else:
723
+ result_dict = result
724
+ return result_dict
725
+ except Exception as api_error:
726
+ logger.error(f"API call failed: {api_error}", exc_info=True)
727
+ return {"error": f"Failed to get change events: {api_error}"}
728
+ except Exception as e:
729
+ logger.error(f"Error in get_change_events: {e}", exc_info=True)
730
+ return {"error": f"Failed to get change events: {e!s}"}
731
+
732
+ @register_as_tool
733
+ @with_header_auth(EventsApi)
734
+ async def get_events_by_ids(
735
+ self,
736
+ event_ids: Union[List[str], str],
737
+ ctx=None, api_client=None) -> Dict[str, Any]:
738
+ """
739
+ Get events by their IDs.
740
+ This tool retrieves multiple events at once using their unique IDs.
741
+ It supports both batch retrieval and individual fallback requests if the batch API fails.
742
+
743
+ Examples:
744
+ Get events using a list of IDs:
745
+ - event_ids: ["1a2b3c4d5e6f", "7g8h9i0j1k2l"]
746
+
747
+ Args:
748
+ event_ids: List of event IDs to retrieve or a comma-separated string of IDs
749
+ ctx: The MCP context (optional)
750
+ api_client: API client for testing (optional)
751
+
752
+ Returns:
753
+ Dictionary containing the list of events or error information
754
+ """
755
+
756
+ try:
757
+ logger.debug(f"get_events_by_ids called with event_ids={event_ids}")
758
+
759
+ # Handle string input conversion
760
+ if isinstance(event_ids, str):
761
+ if event_ids.startswith('[') and event_ids.endswith(']'):
762
+ import ast
763
+ try:
764
+ event_ids = ast.literal_eval(event_ids)
765
+ except (SyntaxError, ValueError) as e:
766
+ logger.error(f"Failed to parse event_ids as list: {e}")
767
+ return {"error": f"Invalid event_ids format: {e}"}
768
+ else:
769
+ event_ids = [id.strip() for id in event_ids.split(',')]
770
+
771
+ # Validate input
772
+ if not event_ids:
773
+ return {"error": "No event IDs provided"}
774
+
775
+ logger.debug(f"Processing {len(event_ids)} event IDs")
776
+
777
+ # Use the batch API to retrieve all events at once
778
+ try:
779
+ logger.debug("Retrieving events using batch API")
780
+ events_result = api_client.get_events_by_ids(request_body=event_ids)
781
+
782
+ all_events = []
783
+ for event in events_result:
784
+ if hasattr(event, 'to_dict'):
785
+ event_dict = event.to_dict()
786
+ else:
787
+ event_dict = event
788
+ all_events.append(event_dict)
789
+
790
+ result = {
791
+ "events": all_events,
792
+ "events_count": len(all_events),
793
+ "successful_retrievals": len(all_events),
794
+ "failed_retrievals": 0,
795
+ "summary": self._summarize_events_result(all_events)
796
+ }
797
+
798
+ logger.debug(f"Retrieved {result['successful_retrievals']} events successfully using batch API")
799
+ return result
800
+
801
+ except Exception as batch_error:
802
+ logger.warning(f"Batch API failed: {batch_error}. Falling back to individual requests.")
803
+
804
+ # Fallback to individual requests using without_preload_content
805
+ all_events = []
806
+ for event_id in event_ids:
807
+ try:
808
+ logger.debug(f"Retrieving event ID: {event_id}")
809
+ response_data = api_client.get_events_by_ids_without_preload_content(request_body=[event_id])
810
+
811
+ # Check if the response was successful
812
+ if response_data.status != 200:
813
+ error_message = f"Failed to get event {event_id}: HTTP {response_data.status}"
814
+ logger.error(error_message)
815
+ all_events.append({"eventId": event_id, "error": error_message})
816
+ continue
817
+
818
+ # Read and parse the response content
819
+ response_text = response_data.data.decode('utf-8')
820
+ try:
821
+ event_dict = json.loads(response_text)
822
+ if isinstance(event_dict, list) and event_dict:
823
+ all_events.append(event_dict[0])
824
+ else:
825
+ all_events.append({"eventId": event_id, "error": "No event data returned"})
826
+ except json.JSONDecodeError as json_err:
827
+ error_message = f"Failed to parse JSON for event {event_id}: {json_err}"
828
+ logger.error(error_message)
829
+ all_events.append({"eventId": event_id, "error": error_message})
830
+
831
+ except Exception as e:
832
+ logger.error(f"Error retrieving event ID {event_id}: {e}", exc_info=True)
833
+ all_events.append({"eventId": event_id, "error": f"Failed to retrieve: {e!s}"})
834
+
835
+ result = {
836
+ "events": all_events,
837
+ "events_count": len(all_events),
838
+ "successful_retrievals": sum(1 for event in all_events if "error" not in event),
839
+ "failed_retrievals": sum(1 for event in all_events if "error" in event),
840
+ "summary": self._summarize_events_result([e for e in all_events if "error" not in e])
841
+ }
842
+
843
+ logger.debug(f"Retrieved {result['successful_retrievals']} events successfully, {result['failed_retrievals']} failed using individual requests")
844
+ return result
845
+ except Exception as e:
846
+ logger.error(f"Error in get_events_by_ids: {e}", exc_info=True)
847
+ return {
848
+ "error": f"Failed to get events by IDs: {e!s}",
849
+ "details": str(e)
850
+ }