chuk-tool-processor 0.7.0__py3-none-any.whl → 0.10__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.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/__init__.py +114 -0
- chuk_tool_processor/core/__init__.py +31 -0
- chuk_tool_processor/core/exceptions.py +218 -12
- chuk_tool_processor/core/processor.py +391 -43
- chuk_tool_processor/execution/wrappers/__init__.py +42 -0
- chuk_tool_processor/execution/wrappers/caching.py +43 -10
- chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
- chuk_tool_processor/execution/wrappers/rate_limiting.py +31 -1
- chuk_tool_processor/execution/wrappers/retry.py +93 -53
- chuk_tool_processor/logging/__init__.py +5 -8
- chuk_tool_processor/logging/context.py +2 -5
- chuk_tool_processor/mcp/__init__.py +3 -0
- chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor/mcp/models.py +87 -0
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +38 -2
- chuk_tool_processor/mcp/setup_mcp_sse.py +38 -2
- chuk_tool_processor/mcp/setup_mcp_stdio.py +92 -12
- chuk_tool_processor/mcp/stream_manager.py +109 -6
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +18 -5
- chuk_tool_processor/mcp/transport/sse_transport.py +16 -3
- chuk_tool_processor/models/__init__.py +20 -0
- chuk_tool_processor/models/tool_call.py +34 -1
- chuk_tool_processor/models/tool_export_mixin.py +4 -4
- chuk_tool_processor/models/tool_spec.py +350 -0
- chuk_tool_processor/models/validated_tool.py +22 -2
- chuk_tool_processor/observability/__init__.py +30 -0
- chuk_tool_processor/observability/metrics.py +312 -0
- chuk_tool_processor/observability/setup.py +105 -0
- chuk_tool_processor/observability/tracing.py +346 -0
- chuk_tool_processor/py.typed +0 -0
- chuk_tool_processor/registry/interface.py +7 -7
- chuk_tool_processor/registry/providers/__init__.py +2 -1
- chuk_tool_processor/registry/tool_export.py +1 -6
- chuk_tool_processor-0.10.dist-info/METADATA +2326 -0
- chuk_tool_processor-0.10.dist-info/RECORD +69 -0
- chuk_tool_processor-0.7.0.dist-info/METADATA +0 -1230
- chuk_tool_processor-0.7.0.dist-info/RECORD +0 -61
- {chuk_tool_processor-0.7.0.dist-info → chuk_tool_processor-0.10.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.7.0.dist-info → chuk_tool_processor-0.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus metrics integration for chuk-tool-processor.
|
|
3
|
+
|
|
4
|
+
Provides drop-in Prometheus metrics with a /metrics HTTP endpoint.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from chuk_tool_processor.logging import get_logger
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
16
|
+
|
|
17
|
+
logger = get_logger("chuk_tool_processor.observability.metrics")
|
|
18
|
+
|
|
19
|
+
# Global metrics instance
|
|
20
|
+
_metrics: PrometheusMetrics | None = None
|
|
21
|
+
_metrics_enabled = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PrometheusMetrics:
|
|
25
|
+
"""
|
|
26
|
+
Prometheus metrics collector for tool execution.
|
|
27
|
+
|
|
28
|
+
Provides standard metrics:
|
|
29
|
+
- tool_executions_total: Counter of tool executions by tool, status
|
|
30
|
+
- tool_execution_duration_seconds: Histogram of execution duration
|
|
31
|
+
- tool_cache_operations_total: Counter of cache operations
|
|
32
|
+
- tool_circuit_breaker_state: Gauge of circuit breaker state
|
|
33
|
+
- tool_retry_attempts_total: Counter of retry attempts
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
"""Initialize Prometheus metrics."""
|
|
38
|
+
try:
|
|
39
|
+
from prometheus_client import Counter, Gauge, Histogram
|
|
40
|
+
|
|
41
|
+
self._initialized = True
|
|
42
|
+
|
|
43
|
+
# Tool execution metrics
|
|
44
|
+
self.tool_executions_total: Counter = Counter(
|
|
45
|
+
"tool_executions_total",
|
|
46
|
+
"Total number of tool executions",
|
|
47
|
+
["tool", "namespace", "status"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.tool_execution_duration_seconds: Histogram = Histogram(
|
|
51
|
+
"tool_execution_duration_seconds",
|
|
52
|
+
"Tool execution duration in seconds",
|
|
53
|
+
["tool", "namespace"],
|
|
54
|
+
buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Cache metrics
|
|
58
|
+
self.tool_cache_operations_total: Counter = Counter(
|
|
59
|
+
"tool_cache_operations_total",
|
|
60
|
+
"Total number of cache operations",
|
|
61
|
+
["tool", "operation", "result"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Circuit breaker metrics
|
|
65
|
+
self.tool_circuit_breaker_state: Gauge = Gauge(
|
|
66
|
+
"tool_circuit_breaker_state",
|
|
67
|
+
"Circuit breaker state (0=CLOSED, 1=OPEN, 2=HALF_OPEN)",
|
|
68
|
+
["tool"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.tool_circuit_breaker_failures_total: Counter = Counter(
|
|
72
|
+
"tool_circuit_breaker_failures_total",
|
|
73
|
+
"Total circuit breaker failures",
|
|
74
|
+
["tool"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Retry metrics
|
|
78
|
+
self.tool_retry_attempts_total: Counter = Counter(
|
|
79
|
+
"tool_retry_attempts_total",
|
|
80
|
+
"Total retry attempts",
|
|
81
|
+
["tool", "attempt", "success"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Rate limiting metrics
|
|
85
|
+
self.tool_rate_limit_checks_total: Counter = Counter(
|
|
86
|
+
"tool_rate_limit_checks_total",
|
|
87
|
+
"Total rate limit checks",
|
|
88
|
+
["tool", "allowed"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
logger.info("Prometheus metrics initialized")
|
|
92
|
+
|
|
93
|
+
except ImportError as e:
|
|
94
|
+
logger.warning(f"Prometheus client not installed: {e}. Metrics disabled.")
|
|
95
|
+
self._initialized = False
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def enabled(self) -> bool:
|
|
99
|
+
"""Check if metrics are enabled."""
|
|
100
|
+
return self._initialized
|
|
101
|
+
|
|
102
|
+
def record_tool_execution(
|
|
103
|
+
self,
|
|
104
|
+
tool: str,
|
|
105
|
+
namespace: str | None,
|
|
106
|
+
duration: float,
|
|
107
|
+
success: bool,
|
|
108
|
+
cached: bool = False,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Record tool execution metrics.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
tool: Tool name
|
|
115
|
+
namespace: Tool namespace
|
|
116
|
+
duration: Execution duration in seconds
|
|
117
|
+
success: Whether execution succeeded
|
|
118
|
+
cached: Whether result was cached
|
|
119
|
+
"""
|
|
120
|
+
if not self._initialized:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
ns = namespace or "default"
|
|
124
|
+
status = "success" if success else "error"
|
|
125
|
+
|
|
126
|
+
# Record execution count
|
|
127
|
+
self.tool_executions_total.labels(tool=tool, namespace=ns, status=status).inc()
|
|
128
|
+
|
|
129
|
+
# Record duration (skip for cached results as they're instant)
|
|
130
|
+
if not cached:
|
|
131
|
+
self.tool_execution_duration_seconds.labels(tool=tool, namespace=ns).observe(duration)
|
|
132
|
+
|
|
133
|
+
def record_cache_operation(
|
|
134
|
+
self,
|
|
135
|
+
tool: str,
|
|
136
|
+
operation: str,
|
|
137
|
+
hit: bool | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Record cache operation metrics.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
tool: Tool name
|
|
144
|
+
operation: Cache operation (lookup, set, invalidate)
|
|
145
|
+
hit: Whether cache hit (for lookup operations)
|
|
146
|
+
"""
|
|
147
|
+
if not self._initialized:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
result = "hit" if hit else "miss" if hit is not None else "set"
|
|
151
|
+
self.tool_cache_operations_total.labels(tool=tool, operation=operation, result=result).inc()
|
|
152
|
+
|
|
153
|
+
def record_circuit_breaker_state(
|
|
154
|
+
self,
|
|
155
|
+
tool: str,
|
|
156
|
+
state: str,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Record circuit breaker state.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
tool: Tool name
|
|
163
|
+
state: Circuit state (CLOSED, OPEN, HALF_OPEN)
|
|
164
|
+
"""
|
|
165
|
+
if not self._initialized:
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
state_value = {"CLOSED": 0, "OPEN": 1, "HALF_OPEN": 2}.get(state.upper(), 0)
|
|
169
|
+
self.tool_circuit_breaker_state.labels(tool=tool).set(state_value)
|
|
170
|
+
|
|
171
|
+
def record_circuit_breaker_failure(self, tool: str) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Record circuit breaker failure.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
tool: Tool name
|
|
177
|
+
"""
|
|
178
|
+
if not self._initialized:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
self.tool_circuit_breaker_failures_total.labels(tool=tool).inc()
|
|
182
|
+
|
|
183
|
+
def record_retry_attempt(
|
|
184
|
+
self,
|
|
185
|
+
tool: str,
|
|
186
|
+
attempt: int,
|
|
187
|
+
success: bool,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Record retry attempt.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
tool: Tool name
|
|
194
|
+
attempt: Attempt number
|
|
195
|
+
success: Whether attempt succeeded
|
|
196
|
+
"""
|
|
197
|
+
if not self._initialized:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
self.tool_retry_attempts_total.labels(
|
|
201
|
+
tool=tool,
|
|
202
|
+
attempt=str(attempt),
|
|
203
|
+
success=str(success),
|
|
204
|
+
).inc()
|
|
205
|
+
|
|
206
|
+
def record_rate_limit_check(
|
|
207
|
+
self,
|
|
208
|
+
tool: str,
|
|
209
|
+
allowed: bool,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Record rate limit check.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
tool: Tool name
|
|
216
|
+
allowed: Whether request was allowed
|
|
217
|
+
"""
|
|
218
|
+
if not self._initialized:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
self.tool_rate_limit_checks_total.labels(tool=tool, allowed=str(allowed)).inc()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def init_metrics() -> PrometheusMetrics:
|
|
225
|
+
"""
|
|
226
|
+
Initialize Prometheus metrics.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
PrometheusMetrics instance
|
|
230
|
+
"""
|
|
231
|
+
global _metrics, _metrics_enabled
|
|
232
|
+
|
|
233
|
+
_metrics = PrometheusMetrics()
|
|
234
|
+
_metrics_enabled = _metrics.enabled
|
|
235
|
+
|
|
236
|
+
return _metrics
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_metrics() -> PrometheusMetrics | None:
|
|
240
|
+
"""
|
|
241
|
+
Get current metrics instance.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
PrometheusMetrics instance or None if not initialized
|
|
245
|
+
"""
|
|
246
|
+
return _metrics
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def is_metrics_enabled() -> bool:
|
|
250
|
+
"""Check if metrics are enabled."""
|
|
251
|
+
return _metrics_enabled
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def start_metrics_server(port: int = 9090, host: str = "0.0.0.0") -> None: # nosec B104
|
|
255
|
+
"""
|
|
256
|
+
Start Prometheus metrics HTTP server.
|
|
257
|
+
|
|
258
|
+
Serves metrics at http://{host}:{port}/metrics
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
port: Port to listen on
|
|
262
|
+
host: Host to bind to
|
|
263
|
+
|
|
264
|
+
Example:
|
|
265
|
+
from chuk_tool_processor.observability import start_metrics_server
|
|
266
|
+
|
|
267
|
+
# Start metrics server on port 9090
|
|
268
|
+
start_metrics_server(port=9090)
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
from prometheus_client import start_http_server
|
|
272
|
+
|
|
273
|
+
start_http_server(port=port, addr=host)
|
|
274
|
+
logger.info(f"Prometheus metrics server started on http://{host}:{port}/metrics")
|
|
275
|
+
|
|
276
|
+
except ImportError:
|
|
277
|
+
logger.error("prometheus-client not installed. Install with: pip install prometheus-client")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Failed to start metrics server: {e}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class MetricsTimer:
|
|
283
|
+
"""
|
|
284
|
+
Context manager for timing operations with Prometheus metrics.
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
with MetricsTimer() as timer:
|
|
288
|
+
result = await tool.execute()
|
|
289
|
+
|
|
290
|
+
# Record duration
|
|
291
|
+
metrics.record_tool_execution("calculator", "default", timer.duration, success=True)
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(self) -> None:
|
|
295
|
+
self.start_time: float | None = None
|
|
296
|
+
self.end_time: float | None = None
|
|
297
|
+
|
|
298
|
+
def __enter__(self) -> MetricsTimer:
|
|
299
|
+
self.start_time = time.perf_counter()
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
303
|
+
self.end_time = time.perf_counter()
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def duration(self) -> float:
|
|
307
|
+
"""Get duration in seconds."""
|
|
308
|
+
if self.start_time is None:
|
|
309
|
+
return 0.0
|
|
310
|
+
if self.end_time is None:
|
|
311
|
+
return time.perf_counter() - self.start_time
|
|
312
|
+
return self.end_time - self.start_time
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified setup for OpenTelemetry observability.
|
|
3
|
+
|
|
4
|
+
Provides a single function to enable tracing and metrics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from chuk_tool_processor.logging import get_logger
|
|
10
|
+
|
|
11
|
+
from .metrics import init_metrics, start_metrics_server
|
|
12
|
+
from .tracing import init_tracer
|
|
13
|
+
|
|
14
|
+
logger = get_logger("chuk_tool_processor.observability.setup")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def setup_observability(
|
|
18
|
+
*,
|
|
19
|
+
service_name: str = "chuk-tool-processor",
|
|
20
|
+
enable_tracing: bool = True,
|
|
21
|
+
enable_metrics: bool = True,
|
|
22
|
+
metrics_port: int = 9090,
|
|
23
|
+
metrics_host: str = "0.0.0.0", # nosec B104
|
|
24
|
+
) -> dict[str, bool]:
|
|
25
|
+
"""
|
|
26
|
+
Setup OpenTelemetry tracing and Prometheus metrics.
|
|
27
|
+
|
|
28
|
+
This is the main entry point for enabling observability in your application.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
service_name: Service name for tracing
|
|
32
|
+
enable_tracing: Enable OpenTelemetry tracing
|
|
33
|
+
enable_metrics: Enable Prometheus metrics
|
|
34
|
+
metrics_port: Port for Prometheus metrics server
|
|
35
|
+
metrics_host: Host for Prometheus metrics server
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict with status of tracing and metrics initialization
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
from chuk_tool_processor.observability import setup_observability
|
|
42
|
+
|
|
43
|
+
# Enable everything
|
|
44
|
+
setup_observability(
|
|
45
|
+
service_name="my-tool-service",
|
|
46
|
+
enable_tracing=True,
|
|
47
|
+
enable_metrics=True,
|
|
48
|
+
metrics_port=9090
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Your tool execution is now automatically traced and instrumented!
|
|
52
|
+
|
|
53
|
+
Environment Variables:
|
|
54
|
+
- OTEL_EXPORTER_OTLP_ENDPOINT: OTLP endpoint (default: http://localhost:4317)
|
|
55
|
+
- OTEL_SERVICE_NAME: Service name (overrides service_name parameter)
|
|
56
|
+
"""
|
|
57
|
+
status = {
|
|
58
|
+
"tracing_enabled": False,
|
|
59
|
+
"metrics_enabled": False,
|
|
60
|
+
"metrics_server_started": False,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Initialize tracing
|
|
64
|
+
if enable_tracing:
|
|
65
|
+
try:
|
|
66
|
+
tracer = init_tracer(service_name=service_name)
|
|
67
|
+
status["tracing_enabled"] = tracer is not None
|
|
68
|
+
logger.info(f"OpenTelemetry tracing enabled for '{service_name}'")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Failed to initialize tracing: {e}")
|
|
71
|
+
|
|
72
|
+
# Initialize metrics
|
|
73
|
+
if enable_metrics:
|
|
74
|
+
try:
|
|
75
|
+
metrics = init_metrics()
|
|
76
|
+
status["metrics_enabled"] = metrics is not None and metrics.enabled
|
|
77
|
+
|
|
78
|
+
if status["metrics_enabled"]:
|
|
79
|
+
# Start metrics server
|
|
80
|
+
try:
|
|
81
|
+
start_metrics_server(port=metrics_port, host=metrics_host)
|
|
82
|
+
status["metrics_server_started"] = True
|
|
83
|
+
logger.info(f"Prometheus metrics available at http://{metrics_host}:{metrics_port}/metrics")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Failed to start metrics server: {e}")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Failed to initialize metrics: {e}")
|
|
89
|
+
|
|
90
|
+
# Log summary
|
|
91
|
+
if status["tracing_enabled"] or status["metrics_enabled"]:
|
|
92
|
+
features = []
|
|
93
|
+
if status["tracing_enabled"]:
|
|
94
|
+
features.append("tracing")
|
|
95
|
+
if status["metrics_enabled"]:
|
|
96
|
+
features.append("metrics")
|
|
97
|
+
|
|
98
|
+
logger.info(f"Observability initialized: {', '.join(features)}")
|
|
99
|
+
else:
|
|
100
|
+
logger.warning(
|
|
101
|
+
"Observability not initialized. Install dependencies:\n"
|
|
102
|
+
" pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp prometheus-client"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return status
|