chuk-tool-processor 0.6.13__py3-none-any.whl → 0.9.7__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.

Files changed (35) hide show
  1. chuk_tool_processor/core/__init__.py +31 -0
  2. chuk_tool_processor/core/exceptions.py +218 -12
  3. chuk_tool_processor/core/processor.py +38 -7
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +2 -1
  6. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  7. chuk_tool_processor/execution/wrappers/caching.py +48 -13
  8. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  9. chuk_tool_processor/execution/wrappers/rate_limiting.py +31 -1
  10. chuk_tool_processor/execution/wrappers/retry.py +93 -53
  11. chuk_tool_processor/logging/metrics.py +2 -2
  12. chuk_tool_processor/mcp/mcp_tool.py +5 -5
  13. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +44 -2
  14. chuk_tool_processor/mcp/setup_mcp_sse.py +44 -2
  15. chuk_tool_processor/mcp/setup_mcp_stdio.py +2 -0
  16. chuk_tool_processor/mcp/stream_manager.py +130 -75
  17. chuk_tool_processor/mcp/transport/__init__.py +10 -0
  18. chuk_tool_processor/mcp/transport/http_streamable_transport.py +193 -108
  19. chuk_tool_processor/mcp/transport/models.py +100 -0
  20. chuk_tool_processor/mcp/transport/sse_transport.py +155 -59
  21. chuk_tool_processor/mcp/transport/stdio_transport.py +58 -10
  22. chuk_tool_processor/models/__init__.py +20 -0
  23. chuk_tool_processor/models/tool_call.py +34 -1
  24. chuk_tool_processor/models/tool_spec.py +350 -0
  25. chuk_tool_processor/models/validated_tool.py +22 -2
  26. chuk_tool_processor/observability/__init__.py +30 -0
  27. chuk_tool_processor/observability/metrics.py +312 -0
  28. chuk_tool_processor/observability/setup.py +105 -0
  29. chuk_tool_processor/observability/tracing.py +345 -0
  30. chuk_tool_processor/plugins/discovery.py +1 -1
  31. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  32. {chuk_tool_processor-0.6.13.dist-info → chuk_tool_processor-0.9.7.dist-info}/RECORD +34 -27
  33. chuk_tool_processor-0.6.13.dist-info/METADATA +0 -698
  34. {chuk_tool_processor-0.6.13.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  35. {chuk_tool_processor-0.6.13.dist-info → chuk_tool_processor-0.9.7.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
11
+
12
+ from chuk_tool_processor.logging import get_logger
13
+
14
+ if TYPE_CHECKING:
15
+ from prometheus_client import Counter, Gauge, Histogram # type: ignore[import-not-found]
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, exc_val, exc_tb) -> 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