kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.17.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.
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
- kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +19 -0
- kubectl_mcp_tool/mcp_server.py +246 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/__init__.py +20 -0
- kubectl_mcp_tool/tools/backup.py +881 -0
- kubectl_mcp_tool/tools/capi.py +727 -0
- kubectl_mcp_tool/tools/certs.py +709 -0
- kubectl_mcp_tool/tools/cilium.py +582 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/keda.py +464 -0
- kubectl_mcp_tool/tools/kiali.py +652 -0
- kubectl_mcp_tool/tools/kubevirt.py +803 -0
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- tests/test_browser.py +2 -2
- tests/test_config.py +386 -0
- tests/test_ecosystem.py +331 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- tests/test_tools.py +70 -8
- kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Unit tests for the observability module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestStatsCollector:
|
|
10
|
+
"""Tests for StatsCollector class."""
|
|
11
|
+
|
|
12
|
+
@pytest.mark.unit
|
|
13
|
+
def test_singleton_pattern(self):
|
|
14
|
+
"""Test that StatsCollector is a singleton."""
|
|
15
|
+
from kubectl_mcp_tool.observability.stats import StatsCollector
|
|
16
|
+
|
|
17
|
+
instance1 = StatsCollector()
|
|
18
|
+
instance2 = StatsCollector()
|
|
19
|
+
|
|
20
|
+
assert instance1 is instance2
|
|
21
|
+
|
|
22
|
+
@pytest.mark.unit
|
|
23
|
+
def test_get_stats_collector(self):
|
|
24
|
+
"""Test get_stats_collector function."""
|
|
25
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
26
|
+
|
|
27
|
+
collector = get_stats_collector()
|
|
28
|
+
assert collector is not None
|
|
29
|
+
|
|
30
|
+
# Should return same instance
|
|
31
|
+
collector2 = get_stats_collector()
|
|
32
|
+
assert collector is collector2
|
|
33
|
+
|
|
34
|
+
@pytest.mark.unit
|
|
35
|
+
def test_record_tool_call_success(self):
|
|
36
|
+
"""Test recording a successful tool call."""
|
|
37
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
38
|
+
|
|
39
|
+
collector = get_stats_collector()
|
|
40
|
+
collector.reset()
|
|
41
|
+
|
|
42
|
+
collector.record_tool_call("test_tool", success=True, duration=0.5)
|
|
43
|
+
|
|
44
|
+
assert collector.tool_calls_total == 1
|
|
45
|
+
assert collector.tool_errors_total == 0
|
|
46
|
+
|
|
47
|
+
stats = collector.get_tool_stats("test_tool")
|
|
48
|
+
assert stats["calls"] == 1
|
|
49
|
+
assert stats["errors"] == 0
|
|
50
|
+
assert stats["total_duration_seconds"] == 0.5
|
|
51
|
+
|
|
52
|
+
@pytest.mark.unit
|
|
53
|
+
def test_record_tool_call_error(self):
|
|
54
|
+
"""Test recording a failed tool call."""
|
|
55
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
56
|
+
|
|
57
|
+
collector = get_stats_collector()
|
|
58
|
+
collector.reset()
|
|
59
|
+
|
|
60
|
+
collector.record_tool_call("test_tool", success=False, duration=0.1)
|
|
61
|
+
|
|
62
|
+
assert collector.tool_calls_total == 1
|
|
63
|
+
assert collector.tool_errors_total == 1
|
|
64
|
+
|
|
65
|
+
stats = collector.get_tool_stats("test_tool")
|
|
66
|
+
assert stats["calls"] == 1
|
|
67
|
+
assert stats["errors"] == 1
|
|
68
|
+
assert stats["error_rate"] == 1.0
|
|
69
|
+
|
|
70
|
+
@pytest.mark.unit
|
|
71
|
+
def test_record_tool_error(self):
|
|
72
|
+
"""Test record_tool_error shorthand."""
|
|
73
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
74
|
+
|
|
75
|
+
collector = get_stats_collector()
|
|
76
|
+
collector.reset()
|
|
77
|
+
|
|
78
|
+
collector.record_tool_error("error_tool")
|
|
79
|
+
|
|
80
|
+
assert collector.tool_calls_total == 1
|
|
81
|
+
assert collector.tool_errors_total == 1
|
|
82
|
+
|
|
83
|
+
@pytest.mark.unit
|
|
84
|
+
def test_record_http_request(self):
|
|
85
|
+
"""Test recording HTTP requests."""
|
|
86
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
87
|
+
|
|
88
|
+
collector = get_stats_collector()
|
|
89
|
+
collector.reset()
|
|
90
|
+
|
|
91
|
+
collector.record_http_request("/stats", "GET")
|
|
92
|
+
collector.record_http_request("/metrics", "GET")
|
|
93
|
+
collector.record_http_request("/mcp", "POST")
|
|
94
|
+
|
|
95
|
+
assert collector.http_requests_total == 3
|
|
96
|
+
|
|
97
|
+
stats = collector.get_stats()
|
|
98
|
+
assert stats["http_requests_by_endpoint"]["/stats"] == 1
|
|
99
|
+
assert stats["http_requests_by_endpoint"]["/metrics"] == 1
|
|
100
|
+
assert stats["http_requests_by_endpoint"]["/mcp"] == 1
|
|
101
|
+
assert stats["http_requests_by_method"]["GET"] == 2
|
|
102
|
+
assert stats["http_requests_by_method"]["POST"] == 1
|
|
103
|
+
|
|
104
|
+
@pytest.mark.unit
|
|
105
|
+
def test_uptime(self):
|
|
106
|
+
"""Test uptime property."""
|
|
107
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
108
|
+
|
|
109
|
+
collector = get_stats_collector()
|
|
110
|
+
collector.reset()
|
|
111
|
+
|
|
112
|
+
time.sleep(0.1)
|
|
113
|
+
uptime = collector.uptime
|
|
114
|
+
|
|
115
|
+
assert uptime >= 0.1
|
|
116
|
+
assert uptime < 1.0
|
|
117
|
+
|
|
118
|
+
@pytest.mark.unit
|
|
119
|
+
def test_get_stats(self):
|
|
120
|
+
"""Test get_stats returns complete statistics."""
|
|
121
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
122
|
+
|
|
123
|
+
collector = get_stats_collector()
|
|
124
|
+
collector.reset()
|
|
125
|
+
|
|
126
|
+
collector.record_tool_call("tool_a", success=True, duration=0.1)
|
|
127
|
+
collector.record_tool_call("tool_a", success=True, duration=0.2)
|
|
128
|
+
collector.record_tool_call("tool_b", success=False, duration=0.3)
|
|
129
|
+
|
|
130
|
+
stats = collector.get_stats()
|
|
131
|
+
|
|
132
|
+
assert "uptime_seconds" in stats
|
|
133
|
+
assert stats["tool_calls_total"] == 3
|
|
134
|
+
assert stats["tool_errors_total"] == 1
|
|
135
|
+
assert stats["unique_tools_called"] == 2
|
|
136
|
+
assert "tool_calls_by_name" in stats
|
|
137
|
+
assert "tool_a" in stats["tool_calls_by_name"]
|
|
138
|
+
assert "tool_b" in stats["tool_calls_by_name"]
|
|
139
|
+
|
|
140
|
+
@pytest.mark.unit
|
|
141
|
+
def test_get_tool_stats_nonexistent(self):
|
|
142
|
+
"""Test get_tool_stats returns None for nonexistent tool."""
|
|
143
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
144
|
+
|
|
145
|
+
collector = get_stats_collector()
|
|
146
|
+
collector.reset()
|
|
147
|
+
|
|
148
|
+
stats = collector.get_tool_stats("nonexistent_tool")
|
|
149
|
+
assert stats is None
|
|
150
|
+
|
|
151
|
+
@pytest.mark.unit
|
|
152
|
+
def test_reset(self):
|
|
153
|
+
"""Test reset clears all statistics."""
|
|
154
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
155
|
+
|
|
156
|
+
collector = get_stats_collector()
|
|
157
|
+
|
|
158
|
+
collector.record_tool_call("test_tool", success=True)
|
|
159
|
+
collector.record_http_request("/test", "GET")
|
|
160
|
+
|
|
161
|
+
collector.reset()
|
|
162
|
+
|
|
163
|
+
assert collector.tool_calls_total == 0
|
|
164
|
+
assert collector.tool_errors_total == 0
|
|
165
|
+
assert collector.http_requests_total == 0
|
|
166
|
+
|
|
167
|
+
@pytest.mark.unit
|
|
168
|
+
def test_thread_safety(self):
|
|
169
|
+
"""Test that StatsCollector is thread-safe."""
|
|
170
|
+
from kubectl_mcp_tool.observability.stats import get_stats_collector
|
|
171
|
+
|
|
172
|
+
collector = get_stats_collector()
|
|
173
|
+
collector.reset()
|
|
174
|
+
|
|
175
|
+
def record_calls():
|
|
176
|
+
for i in range(100):
|
|
177
|
+
collector.record_tool_call(f"tool_{i % 10}", success=True)
|
|
178
|
+
|
|
179
|
+
threads = [threading.Thread(target=record_calls) for _ in range(10)]
|
|
180
|
+
|
|
181
|
+
for t in threads:
|
|
182
|
+
t.start()
|
|
183
|
+
for t in threads:
|
|
184
|
+
t.join()
|
|
185
|
+
|
|
186
|
+
assert collector.tool_calls_total == 1000
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestPrometheusMetrics:
|
|
190
|
+
"""Tests for Prometheus metrics module."""
|
|
191
|
+
|
|
192
|
+
@pytest.mark.unit
|
|
193
|
+
def test_is_prometheus_available(self):
|
|
194
|
+
"""Test is_prometheus_available function."""
|
|
195
|
+
from kubectl_mcp_tool.observability.metrics import is_prometheus_available
|
|
196
|
+
|
|
197
|
+
# Should return bool regardless of prometheus_client installation
|
|
198
|
+
result = is_prometheus_available()
|
|
199
|
+
assert isinstance(result, bool)
|
|
200
|
+
|
|
201
|
+
@pytest.mark.unit
|
|
202
|
+
def test_get_metrics(self):
|
|
203
|
+
"""Test get_metrics returns Prometheus format."""
|
|
204
|
+
from kubectl_mcp_tool.observability.metrics import get_metrics, is_prometheus_available
|
|
205
|
+
|
|
206
|
+
metrics = get_metrics()
|
|
207
|
+
|
|
208
|
+
assert isinstance(metrics, str)
|
|
209
|
+
|
|
210
|
+
if is_prometheus_available():
|
|
211
|
+
# Should have some metric content
|
|
212
|
+
assert len(metrics) > 0
|
|
213
|
+
else:
|
|
214
|
+
# Should return informative message
|
|
215
|
+
assert "not available" in metrics
|
|
216
|
+
|
|
217
|
+
@pytest.mark.unit
|
|
218
|
+
def test_record_tool_call_metric(self):
|
|
219
|
+
"""Test record_tool_call_metric function."""
|
|
220
|
+
from kubectl_mcp_tool.observability.metrics import (
|
|
221
|
+
record_tool_call_metric,
|
|
222
|
+
is_prometheus_available,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Should not raise even if prometheus_client is not installed
|
|
226
|
+
record_tool_call_metric("test_tool", success=True, duration=0.5)
|
|
227
|
+
record_tool_call_metric("test_tool", success=False, duration=0.1)
|
|
228
|
+
|
|
229
|
+
@pytest.mark.unit
|
|
230
|
+
def test_record_tool_error_metric(self):
|
|
231
|
+
"""Test record_tool_error_metric function."""
|
|
232
|
+
from kubectl_mcp_tool.observability.metrics import record_tool_error_metric
|
|
233
|
+
|
|
234
|
+
# Should not raise even if prometheus_client is not installed
|
|
235
|
+
record_tool_error_metric("test_tool", error_type="validation")
|
|
236
|
+
record_tool_error_metric("test_tool", error_type="timeout")
|
|
237
|
+
|
|
238
|
+
@pytest.mark.unit
|
|
239
|
+
def test_record_tool_duration_metric(self):
|
|
240
|
+
"""Test record_tool_duration_metric function."""
|
|
241
|
+
from kubectl_mcp_tool.observability.metrics import record_tool_duration_metric
|
|
242
|
+
|
|
243
|
+
# Should not raise even if prometheus_client is not installed
|
|
244
|
+
record_tool_duration_metric("test_tool", 0.5)
|
|
245
|
+
record_tool_duration_metric("test_tool", 1.5)
|
|
246
|
+
|
|
247
|
+
@pytest.mark.unit
|
|
248
|
+
def test_record_http_request_metric(self):
|
|
249
|
+
"""Test record_http_request_metric function."""
|
|
250
|
+
from kubectl_mcp_tool.observability.metrics import record_http_request_metric
|
|
251
|
+
|
|
252
|
+
# Should not raise even if prometheus_client is not installed
|
|
253
|
+
record_http_request_metric("/stats", "GET", 200)
|
|
254
|
+
record_http_request_metric("/metrics", "GET", 500)
|
|
255
|
+
|
|
256
|
+
@pytest.mark.unit
|
|
257
|
+
def test_set_server_info(self):
|
|
258
|
+
"""Test set_server_info function."""
|
|
259
|
+
from kubectl_mcp_tool.observability.metrics import set_server_info
|
|
260
|
+
|
|
261
|
+
# Should not raise even if prometheus_client is not installed
|
|
262
|
+
set_server_info("1.16.0", "stdio")
|
|
263
|
+
|
|
264
|
+
@pytest.mark.unit
|
|
265
|
+
def test_get_metrics_content_type(self):
|
|
266
|
+
"""Test get_metrics_content_type function."""
|
|
267
|
+
from kubectl_mcp_tool.observability.metrics import get_metrics_content_type
|
|
268
|
+
|
|
269
|
+
content_type = get_metrics_content_type()
|
|
270
|
+
assert isinstance(content_type, str)
|
|
271
|
+
assert "text" in content_type
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestTracing:
|
|
275
|
+
"""Tests for OpenTelemetry tracing module."""
|
|
276
|
+
|
|
277
|
+
@pytest.mark.unit
|
|
278
|
+
def test_is_tracing_available(self):
|
|
279
|
+
"""Test is_tracing_available function."""
|
|
280
|
+
from kubectl_mcp_tool.observability.tracing import is_tracing_available
|
|
281
|
+
|
|
282
|
+
# Should return bool regardless of opentelemetry installation
|
|
283
|
+
result = is_tracing_available()
|
|
284
|
+
assert isinstance(result, bool)
|
|
285
|
+
|
|
286
|
+
@pytest.mark.unit
|
|
287
|
+
def test_get_tracer_before_init(self):
|
|
288
|
+
"""Test get_tracer returns None before initialization."""
|
|
289
|
+
from kubectl_mcp_tool.observability.tracing import get_tracer, shutdown_tracing
|
|
290
|
+
|
|
291
|
+
# Ensure clean state
|
|
292
|
+
shutdown_tracing()
|
|
293
|
+
|
|
294
|
+
tracer = get_tracer()
|
|
295
|
+
# May be None if not initialized, or a tracer if previously initialized
|
|
296
|
+
# Just verify it doesn't raise
|
|
297
|
+
|
|
298
|
+
@pytest.mark.unit
|
|
299
|
+
def test_traced_tool_call_no_op(self):
|
|
300
|
+
"""Test traced_tool_call works as no-op when tracing unavailable."""
|
|
301
|
+
from kubectl_mcp_tool.observability.tracing import traced_tool_call, shutdown_tracing
|
|
302
|
+
|
|
303
|
+
shutdown_tracing()
|
|
304
|
+
|
|
305
|
+
with traced_tool_call("test_tool", {"key": "value"}) as span:
|
|
306
|
+
# Should work without raising
|
|
307
|
+
result = 1 + 1
|
|
308
|
+
|
|
309
|
+
assert result == 2
|
|
310
|
+
|
|
311
|
+
@pytest.mark.unit
|
|
312
|
+
def test_traced_tool_call_with_exception(self):
|
|
313
|
+
"""Test traced_tool_call propagates exceptions."""
|
|
314
|
+
from kubectl_mcp_tool.observability.tracing import traced_tool_call
|
|
315
|
+
|
|
316
|
+
with pytest.raises(ValueError, match="test error"):
|
|
317
|
+
with traced_tool_call("test_tool") as span:
|
|
318
|
+
raise ValueError("test error")
|
|
319
|
+
|
|
320
|
+
@pytest.mark.unit
|
|
321
|
+
def test_add_span_attribute(self):
|
|
322
|
+
"""Test add_span_attribute function."""
|
|
323
|
+
from kubectl_mcp_tool.observability.tracing import add_span_attribute
|
|
324
|
+
|
|
325
|
+
# Should not raise even if tracing is not available
|
|
326
|
+
add_span_attribute("test_key", "test_value")
|
|
327
|
+
add_span_attribute("test_int", 42)
|
|
328
|
+
add_span_attribute("test_float", 3.14)
|
|
329
|
+
add_span_attribute("test_bool", True)
|
|
330
|
+
|
|
331
|
+
@pytest.mark.unit
|
|
332
|
+
def test_record_span_exception(self):
|
|
333
|
+
"""Test record_span_exception function."""
|
|
334
|
+
from kubectl_mcp_tool.observability.tracing import record_span_exception
|
|
335
|
+
|
|
336
|
+
# Should not raise even if tracing is not available
|
|
337
|
+
record_span_exception(ValueError("test error"))
|
|
338
|
+
|
|
339
|
+
@pytest.mark.unit
|
|
340
|
+
def test_shutdown_tracing(self):
|
|
341
|
+
"""Test shutdown_tracing function."""
|
|
342
|
+
from kubectl_mcp_tool.observability.tracing import shutdown_tracing
|
|
343
|
+
|
|
344
|
+
# Should not raise even if tracing was not initialized
|
|
345
|
+
shutdown_tracing()
|
|
346
|
+
shutdown_tracing() # Multiple calls should be safe
|
|
347
|
+
|
|
348
|
+
@pytest.mark.unit
|
|
349
|
+
def test_init_tracing_without_endpoint(self):
|
|
350
|
+
"""Test init_tracing without OTLP endpoint."""
|
|
351
|
+
from kubectl_mcp_tool.observability.tracing import (
|
|
352
|
+
init_tracing,
|
|
353
|
+
is_tracing_available,
|
|
354
|
+
shutdown_tracing,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
shutdown_tracing()
|
|
358
|
+
|
|
359
|
+
if is_tracing_available():
|
|
360
|
+
# Clear any OTLP endpoint
|
|
361
|
+
with patch.dict("os.environ", {}, clear=True):
|
|
362
|
+
result = init_tracing(service_name="test-service")
|
|
363
|
+
assert result is True # Should initialize with no exporter
|
|
364
|
+
shutdown_tracing()
|
|
365
|
+
else:
|
|
366
|
+
result = init_tracing()
|
|
367
|
+
assert result is False # OpenTelemetry not available
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestObservabilityModule:
|
|
371
|
+
"""Tests for observability module exports."""
|
|
372
|
+
|
|
373
|
+
@pytest.mark.unit
|
|
374
|
+
def test_module_imports(self):
|
|
375
|
+
"""Test that all observability functions can be imported."""
|
|
376
|
+
from kubectl_mcp_tool.observability import (
|
|
377
|
+
StatsCollector,
|
|
378
|
+
get_stats_collector,
|
|
379
|
+
get_metrics,
|
|
380
|
+
record_tool_call_metric,
|
|
381
|
+
record_tool_error_metric,
|
|
382
|
+
record_tool_duration_metric,
|
|
383
|
+
is_prometheus_available,
|
|
384
|
+
init_tracing,
|
|
385
|
+
traced_tool_call,
|
|
386
|
+
get_tracer,
|
|
387
|
+
is_tracing_available,
|
|
388
|
+
shutdown_tracing,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert StatsCollector is not None
|
|
392
|
+
assert get_stats_collector is not None
|
|
393
|
+
assert get_metrics is not None
|
|
394
|
+
assert init_tracing is not None
|
|
395
|
+
assert traced_tool_call is not None
|
|
396
|
+
|
|
397
|
+
@pytest.mark.unit
|
|
398
|
+
def test_stats_and_metrics_integration(self):
|
|
399
|
+
"""Test stats and metrics work together."""
|
|
400
|
+
from kubectl_mcp_tool.observability import (
|
|
401
|
+
get_stats_collector,
|
|
402
|
+
record_tool_call_metric,
|
|
403
|
+
get_metrics,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
collector = get_stats_collector()
|
|
407
|
+
collector.reset()
|
|
408
|
+
|
|
409
|
+
# Record calls in both stats and metrics
|
|
410
|
+
for i in range(5):
|
|
411
|
+
collector.record_tool_call("integration_tool", success=True, duration=0.1)
|
|
412
|
+
record_tool_call_metric("integration_tool", success=True, duration=0.1)
|
|
413
|
+
|
|
414
|
+
stats = collector.get_stats()
|
|
415
|
+
metrics = get_metrics()
|
|
416
|
+
|
|
417
|
+
assert stats["tool_calls_total"] == 5
|
|
418
|
+
assert isinstance(metrics, str)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestSamplerConfiguration:
|
|
422
|
+
"""Tests for OpenTelemetry sampler configuration."""
|
|
423
|
+
|
|
424
|
+
@pytest.mark.unit
|
|
425
|
+
def test_sampler_always_on(self):
|
|
426
|
+
"""Test OTEL_TRACES_SAMPLER=always_on."""
|
|
427
|
+
from kubectl_mcp_tool.observability.tracing import is_tracing_available
|
|
428
|
+
|
|
429
|
+
if not is_tracing_available():
|
|
430
|
+
pytest.skip("OpenTelemetry not available")
|
|
431
|
+
|
|
432
|
+
from kubectl_mcp_tool.observability.tracing import _get_sampler
|
|
433
|
+
|
|
434
|
+
with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER": "always_on"}):
|
|
435
|
+
sampler = _get_sampler()
|
|
436
|
+
assert sampler is not None
|
|
437
|
+
|
|
438
|
+
@pytest.mark.unit
|
|
439
|
+
def test_sampler_always_off(self):
|
|
440
|
+
"""Test OTEL_TRACES_SAMPLER=always_off."""
|
|
441
|
+
from kubectl_mcp_tool.observability.tracing import is_tracing_available
|
|
442
|
+
|
|
443
|
+
if not is_tracing_available():
|
|
444
|
+
pytest.skip("OpenTelemetry not available")
|
|
445
|
+
|
|
446
|
+
from kubectl_mcp_tool.observability.tracing import _get_sampler
|
|
447
|
+
|
|
448
|
+
with patch.dict("os.environ", {"OTEL_TRACES_SAMPLER": "always_off"}):
|
|
449
|
+
sampler = _get_sampler()
|
|
450
|
+
assert sampler is not None
|
|
451
|
+
|
|
452
|
+
@pytest.mark.unit
|
|
453
|
+
def test_sampler_trace_id_ratio(self):
|
|
454
|
+
"""Test OTEL_TRACES_SAMPLER=traceidratio."""
|
|
455
|
+
from kubectl_mcp_tool.observability.tracing import is_tracing_available
|
|
456
|
+
|
|
457
|
+
if not is_tracing_available():
|
|
458
|
+
pytest.skip("OpenTelemetry not available")
|
|
459
|
+
|
|
460
|
+
from kubectl_mcp_tool.observability.tracing import _get_sampler
|
|
461
|
+
|
|
462
|
+
with patch.dict("os.environ", {
|
|
463
|
+
"OTEL_TRACES_SAMPLER": "traceidratio",
|
|
464
|
+
"OTEL_TRACES_SAMPLER_ARG": "0.5"
|
|
465
|
+
}):
|
|
466
|
+
sampler = _get_sampler()
|
|
467
|
+
assert sampler is not None
|
|
468
|
+
|
|
469
|
+
@pytest.mark.unit
|
|
470
|
+
def test_sampler_invalid_ratio(self):
|
|
471
|
+
"""Test invalid OTEL_TRACES_SAMPLER_ARG defaults to 1.0."""
|
|
472
|
+
from kubectl_mcp_tool.observability.tracing import is_tracing_available
|
|
473
|
+
|
|
474
|
+
if not is_tracing_available():
|
|
475
|
+
pytest.skip("OpenTelemetry not available")
|
|
476
|
+
|
|
477
|
+
from kubectl_mcp_tool.observability.tracing import _get_sampler
|
|
478
|
+
|
|
479
|
+
with patch.dict("os.environ", {
|
|
480
|
+
"OTEL_TRACES_SAMPLER": "traceidratio",
|
|
481
|
+
"OTEL_TRACES_SAMPLER_ARG": "invalid"
|
|
482
|
+
}):
|
|
483
|
+
# Should not raise, defaults to 1.0
|
|
484
|
+
sampler = _get_sampler()
|
|
485
|
+
assert sampler is not None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class TestToolStatsDataclass:
|
|
489
|
+
"""Tests for ToolStats dataclass."""
|
|
490
|
+
|
|
491
|
+
@pytest.mark.unit
|
|
492
|
+
def test_tool_stats_defaults(self):
|
|
493
|
+
"""Test ToolStats default values."""
|
|
494
|
+
from kubectl_mcp_tool.observability.stats import ToolStats
|
|
495
|
+
|
|
496
|
+
stats = ToolStats()
|
|
497
|
+
|
|
498
|
+
assert stats.calls == 0
|
|
499
|
+
assert stats.errors == 0
|
|
500
|
+
assert stats.total_duration == 0.0
|
|
501
|
+
assert stats.last_call_time is None
|
|
502
|
+
assert stats.last_error_time is None
|
|
503
|
+
|
|
504
|
+
@pytest.mark.unit
|
|
505
|
+
def test_tool_stats_custom_values(self):
|
|
506
|
+
"""Test ToolStats with custom values."""
|
|
507
|
+
from kubectl_mcp_tool.observability.stats import ToolStats
|
|
508
|
+
|
|
509
|
+
now = time.time()
|
|
510
|
+
stats = ToolStats(
|
|
511
|
+
calls=10,
|
|
512
|
+
errors=2,
|
|
513
|
+
total_duration=5.5,
|
|
514
|
+
last_call_time=now,
|
|
515
|
+
last_error_time=now - 100
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
assert stats.calls == 10
|
|
519
|
+
assert stats.errors == 2
|
|
520
|
+
assert stats.total_duration == 5.5
|
|
521
|
+
assert stats.last_call_time == now
|