@rishibhushan/jenkins-mcp-server 1.0.6 → 1.1.0

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,358 @@
1
+ """
2
+ Metrics and Telemetry Module for Jenkins MCP Server
3
+
4
+ Tracks tool usage, performance, and errors for monitoring
5
+ and optimization purposes.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ from collections import defaultdict
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ToolMetric:
21
+ """Single tool execution metric"""
22
+ tool_name: str
23
+ execution_time_ms: float
24
+ success: bool
25
+ error_message: Optional[str] = None
26
+ timestamp: datetime = field(default_factory=datetime.now)
27
+ args: Optional[Dict[str, Any]] = None
28
+
29
+ def to_dict(self) -> Dict[str, Any]:
30
+ """Convert to dictionary"""
31
+ return {
32
+ "tool_name": self.tool_name,
33
+ "execution_time_ms": round(self.execution_time_ms, 2),
34
+ "success": self.success,
35
+ "error_message": self.error_message,
36
+ "timestamp": self.timestamp.isoformat(),
37
+ "args": self.args
38
+ }
39
+
40
+
41
+ @dataclass
42
+ class ToolStats:
43
+ """Aggregated statistics for a tool"""
44
+ total_calls: int = 0
45
+ successful_calls: int = 0
46
+ failed_calls: int = 0
47
+ total_time_ms: float = 0.0
48
+ min_time_ms: float = float('inf')
49
+ max_time_ms: float = 0.0
50
+
51
+ @property
52
+ def avg_time_ms(self) -> float:
53
+ """Calculate average execution time"""
54
+ if self.total_calls == 0:
55
+ return 0.0
56
+ return self.total_time_ms / self.total_calls
57
+
58
+ @property
59
+ def success_rate(self) -> float:
60
+ """Calculate success rate percentage"""
61
+ if self.total_calls == 0:
62
+ return 0.0
63
+ return (self.successful_calls / self.total_calls) * 100
64
+
65
+ def add_metric(self, metric: ToolMetric) -> None:
66
+ """Add a metric to the statistics"""
67
+ self.total_calls += 1
68
+ self.total_time_ms += metric.execution_time_ms
69
+
70
+ if metric.success:
71
+ self.successful_calls += 1
72
+ else:
73
+ self.failed_calls += 1
74
+
75
+ self.min_time_ms = min(self.min_time_ms, metric.execution_time_ms)
76
+ self.max_time_ms = max(self.max_time_ms, metric.execution_time_ms)
77
+
78
+ def to_dict(self) -> Dict[str, Any]:
79
+ """Convert to dictionary"""
80
+ return {
81
+ "total_calls": self.total_calls,
82
+ "successful_calls": self.successful_calls,
83
+ "failed_calls": self.failed_calls,
84
+ "success_rate_percent": round(self.success_rate, 2),
85
+ "avg_time_ms": round(self.avg_time_ms, 2),
86
+ "min_time_ms": round(self.min_time_ms, 2) if self.min_time_ms != float('inf') else 0,
87
+ "max_time_ms": round(self.max_time_ms, 2),
88
+ "total_time_ms": round(self.total_time_ms, 2)
89
+ }
90
+
91
+
92
+ class MetricsCollector:
93
+ """
94
+ Collects and aggregates metrics for tool executions.
95
+
96
+ Features:
97
+ - Per-tool statistics
98
+ - Recent execution history
99
+ - Error tracking
100
+ - Performance monitoring
101
+ """
102
+
103
+ def __init__(self, max_history: int = 1000):
104
+ """
105
+ Initialize metrics collector.
106
+
107
+ Args:
108
+ max_history: Maximum number of recent metrics to keep
109
+ """
110
+ self.max_history = max_history
111
+ self._metrics: List[ToolMetric] = []
112
+ self._tool_stats: Dict[str, ToolStats] = defaultdict(ToolStats)
113
+ self._lock = asyncio.Lock()
114
+ self._start_time = datetime.now()
115
+
116
+ logger.info(f"Metrics collector initialized (max_history={max_history})")
117
+
118
+ async def record_execution(
119
+ self,
120
+ tool_name: str,
121
+ execution_time_ms: float,
122
+ success: bool,
123
+ error_message: Optional[str] = None,
124
+ args: Optional[Dict[str, Any]] = None
125
+ ) -> None:
126
+ """
127
+ Record a tool execution.
128
+
129
+ Args:
130
+ tool_name: Name of the tool
131
+ execution_time_ms: Execution time in milliseconds
132
+ success: Whether execution was successful
133
+ error_message: Error message if failed
134
+ args: Tool arguments (optional, for debugging)
135
+ """
136
+ metric = ToolMetric(
137
+ tool_name=tool_name,
138
+ execution_time_ms=execution_time_ms,
139
+ success=success,
140
+ error_message=error_message,
141
+ args=args
142
+ )
143
+
144
+ async with self._lock:
145
+ # Add to history
146
+ self._metrics.append(metric)
147
+
148
+ # Trim history if needed
149
+ if len(self._metrics) > self.max_history:
150
+ self._metrics = self._metrics[-self.max_history:]
151
+
152
+ # Update aggregated stats
153
+ self._tool_stats[tool_name].add_metric(metric)
154
+
155
+ # Log based on result
156
+ if success:
157
+ logger.debug(f"Metric recorded: {tool_name} completed in {execution_time_ms:.2f}ms")
158
+ else:
159
+ logger.warning(f"Metric recorded: {tool_name} failed after {execution_time_ms:.2f}ms - {error_message}")
160
+
161
+ async def get_tool_stats(self, tool_name: Optional[str] = None) -> Dict[str, Any]:
162
+ """
163
+ Get statistics for a specific tool or all tools.
164
+
165
+ Args:
166
+ tool_name: Specific tool name, or None for all tools
167
+
168
+ Returns:
169
+ Dictionary with tool statistics
170
+ """
171
+ async with self._lock:
172
+ if tool_name:
173
+ if tool_name not in self._tool_stats:
174
+ return {
175
+ "tool_name": tool_name,
176
+ "stats": ToolStats().to_dict()
177
+ }
178
+
179
+ return {
180
+ "tool_name": tool_name,
181
+ "stats": self._tool_stats[tool_name].to_dict()
182
+ }
183
+
184
+ # Return all tools
185
+ return {
186
+ tool_name: stats.to_dict()
187
+ for tool_name, stats in self._tool_stats.items()
188
+ }
189
+
190
+ async def get_recent_metrics(self, limit: int = 100) -> List[Dict[str, Any]]:
191
+ """
192
+ Get recent metrics.
193
+
194
+ Args:
195
+ limit: Maximum number of metrics to return
196
+
197
+ Returns:
198
+ List of recent metrics
199
+ """
200
+ async with self._lock:
201
+ recent = self._metrics[-limit:]
202
+ return [m.to_dict() for m in recent]
203
+
204
+ async def get_failed_executions(self, limit: int = 50) -> List[Dict[str, Any]]:
205
+ """
206
+ Get recent failed executions.
207
+
208
+ Args:
209
+ limit: Maximum number of failures to return
210
+
211
+ Returns:
212
+ List of failed execution metrics
213
+ """
214
+ async with self._lock:
215
+ failures = [m for m in self._metrics if not m.success]
216
+ recent_failures = failures[-limit:]
217
+ return [m.to_dict() for m in recent_failures]
218
+
219
+ async def get_slow_executions(
220
+ self,
221
+ threshold_ms: float = 1000,
222
+ limit: int = 50
223
+ ) -> List[Dict[str, Any]]:
224
+ """
225
+ Get executions that exceeded a time threshold.
226
+
227
+ Args:
228
+ threshold_ms: Threshold in milliseconds
229
+ limit: Maximum number of results
230
+
231
+ Returns:
232
+ List of slow execution metrics
233
+ """
234
+ async with self._lock:
235
+ slow = [
236
+ m for m in self._metrics
237
+ if m.execution_time_ms > threshold_ms
238
+ ]
239
+ recent_slow = slow[-limit:]
240
+ return [m.to_dict() for m in recent_slow]
241
+
242
+ async def get_summary(self) -> Dict[str, Any]:
243
+ """
244
+ Get overall metrics summary.
245
+
246
+ Returns:
247
+ Dictionary with summary statistics
248
+ """
249
+ async with self._lock:
250
+ total_executions = len(self._metrics)
251
+ successful = sum(1 for m in self._metrics if m.success)
252
+ failed = total_executions - successful
253
+
254
+ if total_executions > 0:
255
+ avg_time = sum(m.execution_time_ms for m in self._metrics) / total_executions
256
+ success_rate = (successful / total_executions) * 100
257
+ else:
258
+ avg_time = 0.0
259
+ success_rate = 0.0
260
+
261
+ uptime = datetime.now() - self._start_time
262
+
263
+ return {
264
+ "uptime_seconds": uptime.total_seconds(),
265
+ "uptime_human": str(uptime).split('.')[0], # Remove microseconds
266
+ "total_executions": total_executions,
267
+ "successful_executions": successful,
268
+ "failed_executions": failed,
269
+ "success_rate_percent": round(success_rate, 2),
270
+ "avg_execution_time_ms": round(avg_time, 2),
271
+ "unique_tools_used": len(self._tool_stats),
272
+ "most_used_tool": self._get_most_used_tool(),
273
+ "slowest_tool": self._get_slowest_tool()
274
+ }
275
+
276
+ def _get_most_used_tool(self) -> Optional[str]:
277
+ """Get the most frequently used tool"""
278
+ if not self._tool_stats:
279
+ return None
280
+
281
+ return max(
282
+ self._tool_stats.items(),
283
+ key=lambda x: x[1].total_calls
284
+ )[0]
285
+
286
+ def _get_slowest_tool(self) -> Optional[str]:
287
+ """Get the tool with highest average execution time"""
288
+ if not self._tool_stats:
289
+ return None
290
+
291
+ return max(
292
+ self._tool_stats.items(),
293
+ key=lambda x: x[1].avg_time_ms
294
+ )[0]
295
+
296
+ async def reset(self) -> None:
297
+ """Reset all metrics"""
298
+ async with self._lock:
299
+ self._metrics.clear()
300
+ self._tool_stats.clear()
301
+ self._start_time = datetime.now()
302
+ logger.info("Metrics reset")
303
+
304
+ async def export_metrics(self) -> Dict[str, Any]:
305
+ """
306
+ Export all metrics data.
307
+
308
+ Returns:
309
+ Complete metrics export
310
+ """
311
+ async with self._lock:
312
+ return {
313
+ "summary": await self.get_summary(),
314
+ "tool_stats": await self.get_tool_stats(),
315
+ "recent_metrics": await self.get_recent_metrics(limit=100),
316
+ "failed_executions": await self.get_failed_executions(limit=50),
317
+ "slow_executions": await self.get_slow_executions(threshold_ms=1000, limit=50)
318
+ }
319
+
320
+
321
+ # Global metrics collector instance
322
+ _metrics_collector: Optional[MetricsCollector] = None
323
+
324
+
325
+ def get_metrics_collector() -> MetricsCollector:
326
+ """Get or create the global metrics collector instance"""
327
+ global _metrics_collector
328
+ if _metrics_collector is None:
329
+ _metrics_collector = MetricsCollector()
330
+ return _metrics_collector
331
+
332
+
333
+ # Convenience functions
334
+ async def record_tool_execution(
335
+ tool_name: str,
336
+ execution_time_ms: float,
337
+ success: bool,
338
+ error_message: Optional[str] = None,
339
+ args: Optional[Dict[str, Any]] = None
340
+ ) -> None:
341
+ """Record a tool execution"""
342
+ await get_metrics_collector().record_execution(
343
+ tool_name,
344
+ execution_time_ms,
345
+ success,
346
+ error_message,
347
+ args
348
+ )
349
+
350
+
351
+ async def get_metrics_summary() -> Dict[str, Any]:
352
+ """Get metrics summary"""
353
+ return await get_metrics_collector().get_summary()
354
+
355
+
356
+ async def get_tool_metrics(tool_name: Optional[str] = None) -> Dict[str, Any]:
357
+ """Get tool-specific metrics"""
358
+ return await get_metrics_collector().get_tool_stats(tool_name)