mcp-instana 0.1.0__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 (67) hide show
  1. mcp_instana-0.2.0.dist-info/METADATA +1229 -0
  2. mcp_instana-0.2.0.dist-info/RECORD +59 -0
  3. {mcp_instana-0.1.0.dist-info → mcp_instana-0.2.0.dist-info}/WHEEL +1 -1
  4. mcp_instana-0.2.0.dist-info/entry_points.txt +4 -0
  5. mcp_instana-0.1.0.dist-info/LICENSE → mcp_instana-0.2.0.dist-info/licenses/LICENSE.md +3 -3
  6. src/application/__init__.py +1 -0
  7. src/{client/application_alert_config_mcp_tools.py → application/application_alert_config.py} +251 -273
  8. src/application/application_analyze.py +628 -0
  9. src/application/application_catalog.py +155 -0
  10. src/application/application_global_alert_config.py +653 -0
  11. src/{client/application_metrics_mcp_tools.py → application/application_metrics.py} +113 -131
  12. src/{client/application_resources_mcp_tools.py → application/application_resources.py} +131 -151
  13. src/application/application_settings.py +1731 -0
  14. src/application/application_topology.py +111 -0
  15. src/automation/action_catalog.py +416 -0
  16. src/automation/action_history.py +338 -0
  17. src/core/__init__.py +1 -0
  18. src/core/server.py +586 -0
  19. src/core/utils.py +213 -0
  20. src/event/__init__.py +1 -0
  21. src/event/events_tools.py +850 -0
  22. src/infrastructure/__init__.py +1 -0
  23. src/{client/infrastructure_analyze_mcp_tools.py → infrastructure/infrastructure_analyze.py} +207 -206
  24. src/{client/infrastructure_catalog_mcp_tools.py → infrastructure/infrastructure_catalog.py} +197 -265
  25. src/infrastructure/infrastructure_metrics.py +171 -0
  26. src/{client/infrastructure_resources_mcp_tools.py → infrastructure/infrastructure_resources.py} +198 -227
  27. src/{client/infrastructure_topology_mcp_tools.py → infrastructure/infrastructure_topology.py} +110 -109
  28. src/log/__init__.py +1 -0
  29. src/log/log_alert_configuration.py +331 -0
  30. src/prompts/__init__.py +16 -0
  31. src/prompts/application/__init__.py +1 -0
  32. src/prompts/application/application_alerts.py +54 -0
  33. src/prompts/application/application_catalog.py +26 -0
  34. src/prompts/application/application_metrics.py +57 -0
  35. src/prompts/application/application_resources.py +26 -0
  36. src/prompts/application/application_settings.py +75 -0
  37. src/prompts/application/application_topology.py +30 -0
  38. src/prompts/events/__init__.py +1 -0
  39. src/prompts/events/events_tools.py +161 -0
  40. src/prompts/infrastructure/infrastructure_analyze.py +72 -0
  41. src/prompts/infrastructure/infrastructure_catalog.py +53 -0
  42. src/prompts/infrastructure/infrastructure_metrics.py +45 -0
  43. src/prompts/infrastructure/infrastructure_resources.py +74 -0
  44. src/prompts/infrastructure/infrastructure_topology.py +38 -0
  45. src/prompts/settings/__init__.py +0 -0
  46. src/prompts/settings/custom_dashboard.py +157 -0
  47. src/prompts/website/__init__.py +1 -0
  48. src/prompts/website/website_analyze.py +35 -0
  49. src/prompts/website/website_catalog.py +40 -0
  50. src/prompts/website/website_configuration.py +105 -0
  51. src/prompts/website/website_metrics.py +34 -0
  52. src/settings/__init__.py +1 -0
  53. src/settings/custom_dashboard_tools.py +417 -0
  54. src/website/__init__.py +0 -0
  55. src/website/website_analyze.py +433 -0
  56. src/website/website_catalog.py +171 -0
  57. src/website/website_configuration.py +770 -0
  58. src/website/website_metrics.py +241 -0
  59. mcp_instana-0.1.0.dist-info/METADATA +0 -649
  60. mcp_instana-0.1.0.dist-info/RECORD +0 -19
  61. mcp_instana-0.1.0.dist-info/entry_points.txt +0 -3
  62. src/client/What is the sum of queue depth for all q +0 -55
  63. src/client/events_mcp_tools.py +0 -531
  64. src/client/instana_client_base.py +0 -93
  65. src/client/log_alert_configuration_mcp_tools.py +0 -316
  66. src/client/show the top 5 services with the highest +0 -28
  67. src/mcp_server.py +0 -343
@@ -0,0 +1,850 @@
1
+
2
+ """
3
+ Agent Monitoring Events MCP Tools Module
4
+
5
+ This module provides agent monitoring events-specific MCP tools for Instana monitoring.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import re
11
+ from datetime import datetime
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
27
+
28
+ from src.core.utils import BaseInstanaClient, register_as_tool, with_header_auth
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
33
+
34
+ def __init__(self, read_token: str, base_url: str):
35
+ super().__init__(read_token=read_token, base_url=base_url)
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
+
156
+ @register_as_tool
157
+ @with_header_auth(EventsApi)
158
+ async def get_event(self, event_id: str, ctx=None, api_client=None) -> Dict[str, Any]:
159
+ """
160
+ Get a specific event by ID.
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
+
169
+ Args:
170
+ event_id: The ID of the event to retrieve
171
+ ctx: The MCP context (optional)
172
+ api_client: API client for testing (optional)
173
+
174
+ Returns:
175
+ Dictionary containing the event data or error information
176
+ """
177
+ try:
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
236
+
237
+ except Exception as e:
238
+ logger.error(f"Error in get_event: {e}", exc_info=True)
239
+ return {"error": f"Failed to get event: {e!s}", "event_id": event_id}
240
+
241
+ @register_as_tool
242
+ @with_header_auth(EventsApi)
243
+ async def get_kubernetes_info_events(self,
244
+ from_time: Optional[int] = None,
245
+ to_time: Optional[int] = None,
246
+ time_range: Optional[str] = None,
247
+ max_events: Optional[int] = 50,
248
+ ctx=None, api_client=None) -> Dict[str, Any]:
249
+ """
250
+ Get Kubernetes info events based on the provided parameters and return a detailed analysis.
251
+
252
+ This tool retrieves Kubernetes events from Instana and provides a detailed analysis focusing on top problems,
253
+ their details, and actionable fix suggestions. You can specify a time range using timestamps or natural language
254
+ like "last 24 hours" or "last 2 days".
255
+
256
+ Examples:
257
+ Get Kubernetes events from the last 24 hours:
258
+ - time_range: "last 24 hours"
259
+
260
+ Args:
261
+ from_time: Start timestamp in milliseconds since epoch (optional)
262
+ to_time: End timestamp in milliseconds since epoch (optional)
263
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
264
+ max_events: Maximum number of events to process (default: 50)
265
+ ctx: The MCP context (optional)
266
+ api_client: API client for testing (optional)
267
+
268
+ Returns:
269
+ Dictionary containing detailed Kubernetes events analysis or error information
270
+ """
271
+ try:
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)
288
+ return {
289
+ "error": f"Failed to get Kubernetes info events: {api_error}",
290
+ "details": str(api_error)
291
+ }
292
+ events = result if isinstance(result, list) else ([result] if result else [])
293
+ total_events_count = len(events)
294
+ events = events[:max_events]
295
+ event_dicts = []
296
+ for event in events:
297
+ if hasattr(event, 'to_dict'):
298
+ event_dicts.append(event.to_dict())
299
+ else:
300
+ event_dicts.append(event)
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
+ }
308
+ problem_groups = {}
309
+ for event in event_dicts:
310
+ problem = event.get("problem", "Unknown")
311
+ if problem not in problem_groups:
312
+ problem_groups[problem] = {
313
+ "count": 0,
314
+ "affected_namespaces": set(),
315
+ "affected_entities": set(),
316
+ "details": set(),
317
+ "fix_suggestions": set(),
318
+ "sample_events": []
319
+ }
320
+ problem_groups[problem]["count"] += 1
321
+ entity_label = event.get("entityLabel", "")
322
+ if "/" in entity_label:
323
+ namespace, entity = entity_label.split("/", 1)
324
+ problem_groups[problem]["affected_namespaces"].add(namespace)
325
+ problem_groups[problem]["affected_entities"].add(entity)
326
+ detail = event.get("detail", "")
327
+ if detail:
328
+ problem_groups[problem]["details"].add(detail)
329
+ fix_suggestion = event.get("fixSuggestion", "")
330
+ if fix_suggestion:
331
+ problem_groups[problem]["fix_suggestions"].add(fix_suggestion)
332
+ if len(problem_groups[problem]["sample_events"]) < 3:
333
+ simple_event = {
334
+ "eventId": event.get("eventId", ""),
335
+ "start": event.get("start", 0),
336
+ "entityLabel": event.get("entityLabel", ""),
337
+ "detail": detail
338
+ }
339
+ problem_groups[problem]["sample_events"].append(simple_event)
340
+ sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
341
+ problem_analyses = []
342
+ for problem_name, problem_data in sorted_problems:
343
+ problem_analysis = {
344
+ "problem": problem_name,
345
+ "count": problem_data["count"],
346
+ "affected_namespaces": list(problem_data["affected_namespaces"]),
347
+ "details": list(problem_data["details"]),
348
+ "fix_suggestions": list(problem_data["fix_suggestions"]),
349
+ "sample_events": problem_data["sample_events"]
350
+ }
351
+ problem_analyses.append(problem_analysis)
352
+ analysis_result = {
353
+ "summary": f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.",
354
+ "time_range": f"{from_date} to {to_date}",
355
+ "events_count": total_events_count,
356
+ "events_analyzed": len(events),
357
+ "problem_analyses": problem_analyses[:10]
358
+ }
359
+ markdown_summary = "# Kubernetes Events Analysis\n\n"
360
+ markdown_summary += f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.\n\n"
361
+ markdown_summary += "## Top Problems\n\n"
362
+ for problem_analysis in problem_analyses[:5]:
363
+ problem_name = problem_analysis["problem"]
364
+ count = problem_analysis["count"]
365
+ markdown_summary += f"### {problem_name} ({count} events)\n\n"
366
+ if problem_analysis.get("affected_namespaces"):
367
+ namespaces = ", ".join(problem_analysis["affected_namespaces"][:5])
368
+ if len(problem_analysis["affected_namespaces"]) > 5:
369
+ namespaces += f" and {len(problem_analysis['affected_namespaces']) - 5} more"
370
+ markdown_summary += f"**Affected Namespaces:** {namespaces}\n\n"
371
+ if problem_analysis.get("fix_suggestions"):
372
+ markdown_summary += "**Fix Suggestions:**\n\n"
373
+ for suggestion in list(problem_analysis["fix_suggestions"])[:3]:
374
+ markdown_summary += f"- {suggestion}\n"
375
+ markdown_summary += "\n"
376
+ analysis_result["markdown_summary"] = markdown_summary
377
+ analysis_result["events"] = event_dicts
378
+ return analysis_result
379
+ except Exception as e:
380
+ logger.error(f"Error in get_kubernetes_info_events: {e}", exc_info=True)
381
+ return {
382
+ "error": f"Failed to get Kubernetes info events: {e!s}",
383
+ "details": str(e)
384
+ }
385
+
386
+ @register_as_tool
387
+ @with_header_auth(EventsApi)
388
+ async def get_agent_monitoring_events(self,
389
+ query: Optional[str] = None,
390
+ from_time: Optional[int] = None,
391
+ to_time: Optional[int] = None,
392
+ size: Optional[int] = 100,
393
+ max_events: Optional[int] = 50,
394
+ time_range: Optional[str] = None,
395
+ ctx=None, api_client=None) -> Dict[str, Any]:
396
+ """
397
+ Get agent monitoring events from Instana and return a detailed analysis.
398
+
399
+ This tool retrieves agent monitoring events from Instana and provides a detailed analysis focusing on
400
+ monitoring issues, their frequency, and affected entities. You can specify a time range using timestamps
401
+ or natural language like "last 24 hours" or "last 2 days".
402
+
403
+ Examples:
404
+ Get agent monitoring events from the last 24 hours:
405
+ - time_range: "last 24 hours"
406
+
407
+ Args:
408
+ query: Query string to filter events (optional)
409
+ from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
410
+ to_time: End timestamp in milliseconds since epoch (optional, defaults to now)
411
+ size: Maximum number of events to return from API (optional, default 100)
412
+ max_events: Maximum number of events to process for analysis (optional, default 50)
413
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
414
+ ctx: The MCP context (optional)
415
+ api_client: API client for testing (optional)
416
+
417
+ Returns:
418
+ Dictionary containing summarized agent monitoring events data or error information
419
+ """
420
+ try:
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)
423
+ if not from_time:
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)
439
+ return {
440
+ "error": f"Failed to get agent monitoring events: {api_error}",
441
+ "details": str(api_error)
442
+ }
443
+ events = result if isinstance(result, list) else ([result] if result else [])
444
+ total_events_count = len(events)
445
+ events = events[:max_events]
446
+ event_dicts = []
447
+ for event in events:
448
+ if hasattr(event, 'to_dict'):
449
+ event_dicts.append(event.to_dict())
450
+ else:
451
+ event_dicts.append(event)
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
+ }
459
+ problem_groups = {}
460
+ for event in event_dicts:
461
+ full_problem = event.get("problem", "Unknown")
462
+ problem = full_problem.replace("Monitoring issue: ", "") if "Monitoring issue: " in full_problem else full_problem
463
+ if problem not in problem_groups:
464
+ problem_groups[problem] = {
465
+ "count": 0,
466
+ "affected_entities": set(),
467
+ "entity_types": set(),
468
+ "sample_events": []
469
+ }
470
+ problem_groups[problem]["count"] += 1
471
+ entity_name = event.get("entityName", "Unknown")
472
+ entity_label = event.get("entityLabel", "Unknown")
473
+ entity_type = event.get("entityType", "Unknown")
474
+ entity_info = f"{entity_name} ({entity_label})"
475
+ problem_groups[problem]["affected_entities"].add(entity_info)
476
+ problem_groups[problem]["entity_types"].add(entity_type)
477
+ if len(problem_groups[problem]["sample_events"]) < 3:
478
+ simple_event = {
479
+ "eventId": event.get("eventId", ""),
480
+ "start": event.get("start", 0),
481
+ "entityName": entity_name,
482
+ "entityLabel": entity_label,
483
+ "severity": event.get("severity", 0)
484
+ }
485
+ problem_groups[problem]["sample_events"].append(simple_event)
486
+ sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
487
+ problem_analyses = []
488
+ for problem_name, problem_data in sorted_problems:
489
+ problem_analysis = {
490
+ "problem": problem_name,
491
+ "count": problem_data["count"],
492
+ "affected_entities": list(problem_data["affected_entities"]),
493
+ "entity_types": list(problem_data["entity_types"]),
494
+ "sample_events": problem_data["sample_events"]
495
+ }
496
+ problem_analyses.append(problem_analysis)
497
+ analysis_result = {
498
+ "summary": f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.",
499
+ "time_range": f"{from_date} to {to_date}",
500
+ "events_count": total_events_count,
501
+ "events_analyzed": len(events),
502
+ "problem_analyses": problem_analyses[:10]
503
+ }
504
+ markdown_summary = "# Agent Monitoring Events Analysis\n\n"
505
+ markdown_summary += f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.\n\n"
506
+ markdown_summary += "## Top Monitoring Issues\n\n"
507
+ for problem_analysis in problem_analyses[:5]:
508
+ problem_name = problem_analysis["problem"]
509
+ count = problem_analysis["count"]
510
+ markdown_summary += f"### {problem_name} ({count} events)\n\n"
511
+ if problem_analysis.get("affected_entities"):
512
+ entities = ", ".join(problem_analysis["affected_entities"][:5])
513
+ if len(problem_analysis["affected_entities"]) > 5:
514
+ entities += f" and {len(problem_analysis['affected_entities']) - 5} more"
515
+ markdown_summary += f"**Affected Entities:** {entities}\n\n"
516
+ markdown_summary += "\n"
517
+ analysis_result["markdown_summary"] = markdown_summary
518
+ analysis_result["events"] = event_dicts
519
+ return analysis_result
520
+ except Exception as e:
521
+ logger.error(f"Error in get_agent_monitoring_events: {e}", exc_info=True)
522
+ return {
523
+ "error": f"Failed to get agent monitoring events: {e!s}",
524
+ "details": str(e)
525
+ }
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
+ }