mcp-instana 0.1.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.
@@ -0,0 +1,531 @@
1
+ """
2
+ Agent Monitoring Events MCP Tools Module
3
+
4
+ This module provides agent monitoring events-specific MCP tools for Instana monitoring.
5
+ """
6
+
7
+ import sys
8
+ from typing import Dict, Any, Optional, List, Union
9
+ from datetime import datetime
10
+
11
+ # Import the correct class name (EventsApi with lowercase 'i')
12
+ from instana_client.api.events_api import EventsApi
13
+
14
+ from .instana_client_base import BaseInstanaClient, register_as_tool
15
+
16
+ class AgentMonitoringEventsMCPTools(BaseInstanaClient):
17
+ """Tools for agent monitoring events in Instana MCP."""
18
+
19
+ def __init__(self, read_token: str, base_url: str):
20
+ """Initialize the Agent Monitoring Events MCP tools client."""
21
+ super().__init__(read_token=read_token, base_url=base_url)
22
+
23
+ # Configure the API client with the correct base URL and authentication
24
+ from instana_client.configuration import Configuration
25
+ configuration = Configuration()
26
+ configuration.host = base_url
27
+ configuration.api_key['ApiKeyAuth'] = read_token
28
+ configuration.api_key_prefix['ApiKeyAuth'] = 'apiToken'
29
+
30
+ # Create an API client with this configuration
31
+ from instana_client.api_client import ApiClient
32
+ api_client = ApiClient(configuration=configuration)
33
+
34
+ # Initialize the Instana SDK's EventsApi with our configured client
35
+ self.events_api = EventsApi(api_client=api_client)
36
+
37
+
38
+ @register_as_tool
39
+ async def get_event(self, event_id: str, ctx=None) -> Dict[str, Any]:
40
+ """
41
+ Get a specific event by ID.
42
+
43
+ Args:
44
+ event_id: The ID of the event to retrieve
45
+ ctx: The MCP context (optional)
46
+
47
+ Returns:
48
+ Dictionary containing the event data or error information
49
+ """
50
+ try:
51
+ # Call the get_event method from the SDK
52
+ result = self.events_api.get_event(event_id=event_id)
53
+
54
+ return result
55
+ except Exception as e:
56
+ return {"error": f"Failed to get event: {str(e)}"}
57
+
58
+ @register_as_tool
59
+ async def get_kubernetes_info_events(self,
60
+ from_time: Optional[int] = None,
61
+ to_time: Optional[int] = None,
62
+ time_range: Optional[str] = None,
63
+ max_events: Optional[int] = 50, # Added parameter to limit events
64
+ ctx=None) -> Dict[str, Any]:
65
+ """
66
+ Get Kubernetes info events based on the provided parameters and return a detailed analysis.
67
+
68
+ This tool retrieves Kubernetes events from Instana and provides a detailed analysis focusing on top problems,
69
+ their details, and actionable fix suggestions. You can specify a time range using timestamps or natural language
70
+ like "last 24 hours" or "last 2 days".
71
+
72
+ Args:
73
+ from_time: Start timestamp in milliseconds since epoch (optional)
74
+ to_time: End timestamp in milliseconds since epoch (optional)
75
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
76
+ max_events: Maximum number of events to process (default: 50)
77
+ ctx: The MCP context (optional)
78
+
79
+ Returns:
80
+ Dictionary containing detailed Kubernetes events analysis or error information
81
+ """
82
+ try:
83
+ # Process natural language time range if provided
84
+ if time_range:
85
+ print(f"DEBUG: Processing natural language time range: '{time_range}'", file=sys.stderr)
86
+
87
+ # Current time in milliseconds
88
+ current_time_ms = int(datetime.now().timestamp() * 1000)
89
+
90
+ # Default to 24 hours if just "last few hours" is specified
91
+ if time_range.lower() in ["last few hours", "last hours", "few hours"]:
92
+ hours = 24
93
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
94
+ to_time = current_time_ms
95
+ print(f"DEBUG: Interpreted as last {hours} hours", file=sys.stderr)
96
+ # Extract hours if specified
97
+ elif "hour" in time_range.lower():
98
+ import re
99
+ hour_match = re.search(r'(\d+)\s*hour', time_range.lower())
100
+ hours = int(hour_match.group(1)) if hour_match else 24
101
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
102
+ to_time = current_time_ms
103
+ # Extract days if specified
104
+ elif "day" in time_range.lower():
105
+ import re
106
+ day_match = re.search(r'(\d+)\s*day', time_range.lower())
107
+ days = int(day_match.group(1)) if day_match else 1
108
+ from_time = current_time_ms - (days * 24 * 60 * 60 * 1000)
109
+ to_time = current_time_ms
110
+ # Handle "last week"
111
+ elif "week" in time_range.lower():
112
+ import re
113
+ week_match = re.search(r'(\d+)\s*week', time_range.lower())
114
+ weeks = int(week_match.group(1)) if week_match else 1
115
+ from_time = current_time_ms - (weeks * 7 * 24 * 60 * 60 * 1000)
116
+ to_time = current_time_ms
117
+ # Handle "last month"
118
+ elif "month" in time_range.lower():
119
+ import re
120
+ month_match = re.search(r'(\d+)\s*month', time_range.lower())
121
+ months = int(month_match.group(1)) if month_match else 1
122
+ from_time = current_time_ms - (months * 30 * 24 * 60 * 60 * 1000)
123
+ to_time = current_time_ms
124
+ # Default to 24 hours for any other time range
125
+ else:
126
+ hours = 24
127
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
128
+ to_time = current_time_ms
129
+
130
+ # Set default time range if not provided
131
+ if not to_time:
132
+ to_time = int(datetime.now().timestamp() * 1000)
133
+ if not from_time:
134
+ from_time = to_time - (24 * 60 * 60 * 1000) # Default to 24 hours
135
+
136
+ # Call the kubernetes_info_events method from the SDK
137
+ result = self.events_api.kubernetes_info_events(
138
+ to=to_time,
139
+ var_from=from_time,
140
+ window_size=None,
141
+ filter_event_updates=None,
142
+ exclude_triggered_before=None
143
+ )
144
+
145
+ # Print the raw result for debugging
146
+ print(f"DEBUG: Raw API result type: {type(result)}", file=sys.stderr)
147
+ print(f"DEBUG: Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}", file=sys.stderr)
148
+
149
+ # If there are no events, return early
150
+ if not result or (isinstance(result, list) and len(result) == 0):
151
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
152
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
153
+ return {
154
+ "analysis": f"No Kubernetes events found between {from_date} and {to_date}.",
155
+ "time_range": f"{from_date} to {to_date}",
156
+ "events_count": 0
157
+ }
158
+
159
+ # Process the events to create a summary
160
+ events = result if isinstance(result, list) else [result]
161
+
162
+ # Get the total number of events before limiting
163
+ total_events_count = len(events)
164
+
165
+ # Limit the number of events to process
166
+ events = events[:max_events]
167
+ print(f"DEBUG: Limited to processing {len(events)} events out of {total_events_count} total events", file=sys.stderr)
168
+
169
+ # Convert InfraEventResult objects to dictionaries if needed
170
+ event_dicts = []
171
+ for event in events:
172
+ if hasattr(event, 'to_dict'):
173
+ event_dicts.append(event.to_dict())
174
+ else:
175
+ event_dicts.append(event)
176
+
177
+ # Group events by problem type
178
+ problem_groups = {}
179
+
180
+ # Process each event
181
+ for event in event_dicts:
182
+ problem = event.get("problem", "Unknown")
183
+
184
+ # Initialize problem group if not exists
185
+ if problem not in problem_groups:
186
+ problem_groups[problem] = {
187
+ "count": 0,
188
+ "affected_namespaces": set(),
189
+ "affected_entities": set(),
190
+ "details": set(),
191
+ "fix_suggestions": set(),
192
+ "sample_events": []
193
+ }
194
+
195
+ # Update problem group
196
+ problem_groups[problem]["count"] += 1
197
+
198
+ # Extract namespace from entityLabel
199
+ entity_label = event.get("entityLabel", "")
200
+ if "/" in entity_label:
201
+ namespace, entity = entity_label.split("/", 1)
202
+ problem_groups[problem]["affected_namespaces"].add(namespace)
203
+ problem_groups[problem]["affected_entities"].add(entity)
204
+
205
+ # Add detail and fix suggestion
206
+ detail = event.get("detail", "")
207
+ if detail:
208
+ problem_groups[problem]["details"].add(detail)
209
+
210
+ fix_suggestion = event.get("fixSuggestion", "")
211
+ if fix_suggestion:
212
+ problem_groups[problem]["fix_suggestions"].add(fix_suggestion)
213
+
214
+ # Add sample event (up to 3 per problem)
215
+ if len(problem_groups[problem]["sample_events"]) < 3:
216
+ simple_event = {
217
+ "eventId": event.get("eventId", ""),
218
+ "start": event.get("start", 0),
219
+ "entityLabel": event.get("entityLabel", ""),
220
+ "detail": detail
221
+ }
222
+ problem_groups[problem]["sample_events"].append(simple_event)
223
+
224
+ # Sort problems by count (most frequent first)
225
+ sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
226
+
227
+ # Format the time range in a human-readable format
228
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
229
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
230
+
231
+ # Create a detailed analysis of each problem
232
+ problem_analyses = []
233
+
234
+ # Process each problem
235
+ for problem_name, problem_data in sorted_problems:
236
+ # Create a detailed problem analysis
237
+ problem_analysis = {
238
+ "problem": problem_name,
239
+ "count": problem_data["count"],
240
+ "affected_namespaces": list(problem_data["affected_namespaces"]),
241
+ "details": list(problem_data["details"]),
242
+ "fix_suggestions": list(problem_data["fix_suggestions"]),
243
+ "sample_events": problem_data["sample_events"]
244
+ }
245
+
246
+ problem_analyses.append(problem_analysis)
247
+
248
+ # Create a comprehensive analysis
249
+ analysis_result = {
250
+ "summary": f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.",
251
+ "time_range": f"{from_date} to {to_date}",
252
+ "events_count": total_events_count,
253
+ "events_analyzed": len(events),
254
+ "problem_analyses": problem_analyses[:10] # Limit to top 10 problems for readability
255
+ }
256
+
257
+ # Create a more user-friendly text summary for direct display
258
+ markdown_summary = "# Kubernetes Events Analysis\n\n"
259
+ markdown_summary += f"Analysis based on {len(events)} of {total_events_count} Kubernetes events between {from_date} and {to_date}.\n\n"
260
+
261
+ markdown_summary += "## Top Problems\n\n"
262
+
263
+ # Add each problem to the markdown summary
264
+ for problem_analysis in problem_analyses[:5]: # Limit to top 5 for readability
265
+ problem_name = problem_analysis["problem"]
266
+ count = problem_analysis["count"]
267
+
268
+ markdown_summary += f"### {problem_name} ({count} events)\n\n"
269
+
270
+ # Add affected namespaces if available
271
+ if problem_analysis.get("affected_namespaces"):
272
+ namespaces = ", ".join(problem_analysis["affected_namespaces"][:5])
273
+ if len(problem_analysis["affected_namespaces"]) > 5:
274
+ namespaces += f" and {len(problem_analysis['affected_namespaces']) - 5} more"
275
+ markdown_summary += f"**Affected Namespaces:** {namespaces}\n\n"
276
+
277
+ # Add fix suggestions
278
+ if problem_analysis.get("fix_suggestions"):
279
+ markdown_summary += "**Fix Suggestions:**\n\n"
280
+ for suggestion in list(problem_analysis["fix_suggestions"])[:3]: # Limit to top 3 suggestions
281
+ markdown_summary += f"- {suggestion}\n"
282
+
283
+ markdown_summary += "\n"
284
+
285
+ # Add the markdown summary to the result
286
+ analysis_result["markdown_summary"] = markdown_summary
287
+
288
+ return analysis_result
289
+
290
+ except Exception as e:
291
+ print(f"Error in get_kubernetes_info_events: {e}", file=sys.stderr)
292
+ import traceback
293
+ traceback.print_exc(file=sys.stderr)
294
+ return {
295
+ "error": f"Failed to get Kubernetes info events: {str(e)}"
296
+ }
297
+
298
+ @register_as_tool
299
+ async def get_agent_monitoring_events(self,
300
+ query: Optional[str] = None,
301
+ from_time: Optional[int] = None,
302
+ to_time: Optional[int] = None,
303
+ size: Optional[int] = 100,
304
+ max_events: Optional[int] = 50, # Added parameter to limit events
305
+ time_range: Optional[str] = None, # Added parameter for natural language time range
306
+ ctx=None) -> Dict[str, Any]:
307
+ """
308
+ Get agent monitoring events from Instana and return a detailed analysis.
309
+
310
+ This tool retrieves agent monitoring events from Instana and provides a detailed analysis focusing on
311
+ monitoring issues, their frequency, and affected entities. You can specify a time range using timestamps
312
+ or natural language like "last 24 hours" or "last 2 days".
313
+
314
+ Args:
315
+ query: Query string to filter events (optional)
316
+ from_time: Start timestamp in milliseconds since epoch (optional, defaults to 1 hour ago)
317
+ to_time: End timestamp in milliseconds since epoch (optional, defaults to now)
318
+ size: Maximum number of events to return from API (optional, default 100)
319
+ max_events: Maximum number of events to process for analysis (optional, default 50)
320
+ time_range: Natural language time range like "last 24 hours", "last 2 days", "last week" (optional)
321
+ ctx: The MCP context (optional)
322
+
323
+ Returns:
324
+ Dictionary containing summarized agent monitoring events data or error information
325
+ """
326
+ try:
327
+ # Process natural language time range if provided
328
+ if time_range:
329
+ print(f"DEBUG: Processing natural language time range: '{time_range}'", file=sys.stderr)
330
+
331
+ # Current time in milliseconds
332
+ current_time_ms = int(datetime.now().timestamp() * 1000)
333
+
334
+ # Default to 24 hours if just "last few hours" is specified
335
+ if time_range.lower() in ["last few hours", "last hours", "few hours"]:
336
+ hours = 24
337
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
338
+ to_time = current_time_ms
339
+ print(f"DEBUG: Interpreted as last {hours} hours", file=sys.stderr)
340
+ # Extract hours if specified
341
+ elif "hour" in time_range.lower():
342
+ import re
343
+ hour_match = re.search(r'(\d+)\s*hour', time_range.lower())
344
+ hours = int(hour_match.group(1)) if hour_match else 24
345
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
346
+ to_time = current_time_ms
347
+ # Extract days if specified
348
+ elif "day" in time_range.lower():
349
+ import re
350
+ day_match = re.search(r'(\d+)\s*day', time_range.lower())
351
+ days = int(day_match.group(1)) if day_match else 1
352
+ from_time = current_time_ms - (days * 24 * 60 * 60 * 1000)
353
+ to_time = current_time_ms
354
+ # Handle "last week"
355
+ elif "week" in time_range.lower():
356
+ import re
357
+ week_match = re.search(r'(\d+)\s*week', time_range.lower())
358
+ weeks = int(week_match.group(1)) if week_match else 1
359
+ from_time = current_time_ms - (weeks * 7 * 24 * 60 * 60 * 1000)
360
+ to_time = current_time_ms
361
+ # Handle "last month"
362
+ elif "month" in time_range.lower():
363
+ import re
364
+ month_match = re.search(r'(\d+)\s*month', time_range.lower())
365
+ months = int(month_match.group(1)) if month_match else 1
366
+ from_time = current_time_ms - (months * 30 * 24 * 60 * 60 * 1000)
367
+ to_time = current_time_ms
368
+ # Default to 24 hours for any other time range
369
+ else:
370
+ hours = 24
371
+ from_time = current_time_ms - (hours * 60 * 60 * 1000)
372
+ to_time = current_time_ms
373
+
374
+ print(f"get_agent_monitoring_events called with query={query}, from_time={from_time}, to_time={to_time}, size={size}", file=sys.stderr)
375
+
376
+ # Set default time range if not provided
377
+ if not to_time:
378
+ to_time = int(datetime.now().timestamp() * 1000)
379
+
380
+ if not from_time:
381
+ from_time = to_time - (60 * 60 * 1000) # Default to 1 hour
382
+
383
+ # Call the agent_monitoring_events method from the SDK
384
+ result = self.events_api.agent_monitoring_events(
385
+ to=to_time,
386
+ var_from=from_time,
387
+ window_size=None,
388
+ filter_event_updates=None,
389
+ exclude_triggered_before=None
390
+ )
391
+
392
+ # Print the raw result for debugging
393
+ print(f"DEBUG: Raw API result type: {type(result)}", file=sys.stderr)
394
+ print(f"DEBUG: Raw API result length: {len(result) if isinstance(result, list) else 'not a list'}", file=sys.stderr)
395
+
396
+ # If there are no events, return early
397
+ if not result or (isinstance(result, list) and len(result) == 0):
398
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
399
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
400
+ return {
401
+ "analysis": f"No agent monitoring events found between {from_date} and {to_date}.",
402
+ "time_range": f"{from_date} to {to_date}",
403
+ "events_count": 0
404
+ }
405
+
406
+ # Process the events to create a summary
407
+ events = result if isinstance(result, list) else [result]
408
+
409
+ # Get the total number of events before limiting
410
+ total_events_count = len(events)
411
+
412
+ # Limit the number of events to process
413
+ events = events[:max_events]
414
+ print(f"DEBUG: Limited to processing {len(events)} events out of {total_events_count} total events", file=sys.stderr)
415
+
416
+ # Convert objects to dictionaries if needed
417
+ event_dicts = []
418
+ for event in events:
419
+ if hasattr(event, 'to_dict'):
420
+ event_dicts.append(event.to_dict())
421
+ else:
422
+ event_dicts.append(event)
423
+
424
+ # Group events by problem type
425
+ problem_groups = {}
426
+
427
+ # Process each event
428
+ for event in event_dicts:
429
+ # Extract the monitoring issue from the problem field
430
+ full_problem = event.get("problem", "Unknown")
431
+ # Strip "Monitoring issue: " prefix if present
432
+ problem = full_problem.replace("Monitoring issue: ", "") if "Monitoring issue: " in full_problem else full_problem
433
+
434
+ # Initialize problem group if not exists
435
+ if problem not in problem_groups:
436
+ problem_groups[problem] = {
437
+ "count": 0,
438
+ "affected_entities": set(),
439
+ "entity_types": set(),
440
+ "sample_events": []
441
+ }
442
+
443
+ # Update problem group
444
+ problem_groups[problem]["count"] += 1
445
+
446
+ # Add entity information
447
+ entity_name = event.get("entityName", "Unknown")
448
+ entity_label = event.get("entityLabel", "Unknown")
449
+ entity_type = event.get("entityType", "Unknown")
450
+
451
+ entity_info = f"{entity_name} ({entity_label})"
452
+ problem_groups[problem]["affected_entities"].add(entity_info)
453
+ problem_groups[problem]["entity_types"].add(entity_type)
454
+
455
+ # Add sample event (up to 3 per problem)
456
+ if len(problem_groups[problem]["sample_events"]) < 3:
457
+ simple_event = {
458
+ "eventId": event.get("eventId", ""),
459
+ "start": event.get("start", 0),
460
+ "entityName": entity_name,
461
+ "entityLabel": entity_label,
462
+ "severity": event.get("severity", 0)
463
+ }
464
+ problem_groups[problem]["sample_events"].append(simple_event)
465
+
466
+ # Sort problems by count (most frequent first)
467
+ sorted_problems = sorted(problem_groups.items(), key=lambda x: x[1]["count"], reverse=True)
468
+
469
+ # Format the time range in a human-readable format
470
+ from_date = datetime.fromtimestamp(from_time/1000).strftime('%Y-%m-%d %H:%M:%S')
471
+ to_date = datetime.fromtimestamp(to_time/1000).strftime('%Y-%m-%d %H:%M:%S')
472
+
473
+ # Create a detailed analysis of each problem
474
+ problem_analyses = []
475
+
476
+ # Process each problem
477
+ for problem_name, problem_data in sorted_problems:
478
+ # Create a detailed problem analysis
479
+ problem_analysis = {
480
+ "problem": problem_name,
481
+ "count": problem_data["count"],
482
+ "affected_entities": list(problem_data["affected_entities"]),
483
+ "entity_types": list(problem_data["entity_types"]),
484
+ "sample_events": problem_data["sample_events"]
485
+ }
486
+
487
+ problem_analyses.append(problem_analysis)
488
+
489
+ # Create a comprehensive analysis
490
+ analysis_result = {
491
+ "summary": f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.",
492
+ "time_range": f"{from_date} to {to_date}",
493
+ "events_count": total_events_count,
494
+ "events_analyzed": len(events),
495
+ "problem_analyses": problem_analyses[:10] # Limit to top 10 problems for readability
496
+ }
497
+
498
+ # Create a more user-friendly text summary for direct display
499
+ markdown_summary = "# Agent Monitoring Events Analysis\n\n"
500
+ markdown_summary += f"Analysis based on {len(events)} of {total_events_count} agent monitoring events between {from_date} and {to_date}.\n\n"
501
+
502
+ markdown_summary += "## Top Monitoring Issues\n\n"
503
+
504
+ # Add each problem to the markdown summary
505
+ for problem_analysis in problem_analyses[:5]: # Limit to top 5 for readability
506
+ problem_name = problem_analysis["problem"]
507
+ count = problem_analysis["count"]
508
+
509
+ markdown_summary += f"### {problem_name} ({count} events)\n\n"
510
+
511
+ # Add affected entities if available
512
+ if problem_analysis.get("affected_entities"):
513
+ entities = ", ".join(problem_analysis["affected_entities"][:5])
514
+ if len(problem_analysis["affected_entities"]) > 5:
515
+ entities += f" and {len(problem_analysis['affected_entities']) - 5} more"
516
+ markdown_summary += f"**Affected Entities:** {entities}\n\n"
517
+
518
+ markdown_summary += "\n"
519
+
520
+ # Add the markdown summary to the result
521
+ analysis_result["markdown_summary"] = markdown_summary
522
+
523
+ return analysis_result
524
+
525
+ except Exception as e:
526
+ print(f"Error in get_agent_monitoring_events: {e}", file=sys.stderr)
527
+ import traceback
528
+ traceback.print_exc(file=sys.stderr)
529
+ return {
530
+ "error": f"Failed to get agent monitoring events: {str(e)}"
531
+ }