@rishibhushan/jenkins-mcp-server 1.0.7 → 1.1.1
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.
- package/README.md +188 -10
- package/bin/jenkins-mcp.js +249 -17
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/jenkins_mcp_server/__init__.py +19 -11
- package/src/jenkins_mcp_server/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/__pycache__/cache.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/__pycache__/config.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/__pycache__/jenkins_client.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/__pycache__/metrics.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/__pycache__/server.cpython-313.pyc +0 -0
- package/src/jenkins_mcp_server/cache.py +310 -0
- package/src/jenkins_mcp_server/config.py +54 -1
- package/src/jenkins_mcp_server/jenkins_client.py +69 -80
- package/src/jenkins_mcp_server/metrics.py +358 -0
- package/src/jenkins_mcp_server/server.py +1015 -108
|
@@ -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)
|