kailash 0.1.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.
- kailash/__init__.py +31 -0
- kailash/__main__.py +11 -0
- kailash/cli/__init__.py +5 -0
- kailash/cli/commands.py +563 -0
- kailash/manifest.py +778 -0
- kailash/nodes/__init__.py +23 -0
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/agents.py +417 -0
- kailash/nodes/ai/models.py +488 -0
- kailash/nodes/api/__init__.py +52 -0
- kailash/nodes/api/auth.py +567 -0
- kailash/nodes/api/graphql.py +480 -0
- kailash/nodes/api/http.py +598 -0
- kailash/nodes/api/rate_limiting.py +572 -0
- kailash/nodes/api/rest.py +665 -0
- kailash/nodes/base.py +1032 -0
- kailash/nodes/base_async.py +128 -0
- kailash/nodes/code/__init__.py +32 -0
- kailash/nodes/code/python.py +1021 -0
- kailash/nodes/data/__init__.py +125 -0
- kailash/nodes/data/readers.py +496 -0
- kailash/nodes/data/sharepoint_graph.py +623 -0
- kailash/nodes/data/sql.py +380 -0
- kailash/nodes/data/streaming.py +1168 -0
- kailash/nodes/data/vector_db.py +964 -0
- kailash/nodes/data/writers.py +529 -0
- kailash/nodes/logic/__init__.py +6 -0
- kailash/nodes/logic/async_operations.py +702 -0
- kailash/nodes/logic/operations.py +551 -0
- kailash/nodes/transform/__init__.py +5 -0
- kailash/nodes/transform/processors.py +379 -0
- kailash/runtime/__init__.py +6 -0
- kailash/runtime/async_local.py +356 -0
- kailash/runtime/docker.py +697 -0
- kailash/runtime/local.py +434 -0
- kailash/runtime/parallel.py +557 -0
- kailash/runtime/runner.py +110 -0
- kailash/runtime/testing.py +347 -0
- kailash/sdk_exceptions.py +307 -0
- kailash/tracking/__init__.py +7 -0
- kailash/tracking/manager.py +885 -0
- kailash/tracking/metrics_collector.py +342 -0
- kailash/tracking/models.py +535 -0
- kailash/tracking/storage/__init__.py +0 -0
- kailash/tracking/storage/base.py +113 -0
- kailash/tracking/storage/database.py +619 -0
- kailash/tracking/storage/filesystem.py +543 -0
- kailash/utils/__init__.py +0 -0
- kailash/utils/export.py +924 -0
- kailash/utils/templates.py +680 -0
- kailash/visualization/__init__.py +62 -0
- kailash/visualization/api.py +732 -0
- kailash/visualization/dashboard.py +951 -0
- kailash/visualization/performance.py +808 -0
- kailash/visualization/reports.py +1471 -0
- kailash/workflow/__init__.py +15 -0
- kailash/workflow/builder.py +245 -0
- kailash/workflow/graph.py +827 -0
- kailash/workflow/mermaid_visualizer.py +628 -0
- kailash/workflow/mock_registry.py +63 -0
- kailash/workflow/runner.py +302 -0
- kailash/workflow/state.py +238 -0
- kailash/workflow/visualization.py +588 -0
- kailash-0.1.0.dist-info/METADATA +710 -0
- kailash-0.1.0.dist-info/RECORD +69 -0
- kailash-0.1.0.dist-info/WHEEL +5 -0
- kailash-0.1.0.dist-info/entry_points.txt +2 -0
- kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
- kailash-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,342 @@
|
|
1
|
+
"""Enhanced metrics collection for task tracking.
|
2
|
+
|
3
|
+
This module provides comprehensive performance metrics collection during node execution,
|
4
|
+
including CPU usage, memory consumption, I/O operations, and custom metrics.
|
5
|
+
|
6
|
+
Design Purpose:
|
7
|
+
- Enable real-time performance monitoring during node execution
|
8
|
+
- Integrate seamlessly with TaskManager and visualization components
|
9
|
+
- Support both synchronous and asynchronous execution contexts
|
10
|
+
|
11
|
+
Upstream Dependencies:
|
12
|
+
- Runtime engines (local.py, parallel.py, docker.py) use this to collect metrics
|
13
|
+
- TaskManager uses this to store performance data
|
14
|
+
|
15
|
+
Downstream Consumers:
|
16
|
+
- Visualization components use collected metrics for performance graphs
|
17
|
+
- Export utilities include metrics in workflow reports
|
18
|
+
"""
|
19
|
+
|
20
|
+
import asyncio
|
21
|
+
import threading
|
22
|
+
import time
|
23
|
+
from contextlib import contextmanager
|
24
|
+
from dataclasses import dataclass, field
|
25
|
+
from typing import Any, Callable, Dict, Optional
|
26
|
+
|
27
|
+
try:
|
28
|
+
import psutil
|
29
|
+
|
30
|
+
PSUTIL_AVAILABLE = True
|
31
|
+
except ImportError:
|
32
|
+
PSUTIL_AVAILABLE = False
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class PerformanceMetrics:
|
37
|
+
"""Container for comprehensive performance metrics.
|
38
|
+
|
39
|
+
Attributes:
|
40
|
+
duration: Execution time in seconds
|
41
|
+
cpu_percent: Average CPU usage percentage
|
42
|
+
memory_mb: Peak memory usage in MB
|
43
|
+
memory_delta_mb: Memory increase during execution
|
44
|
+
io_read_bytes: Total bytes read during execution
|
45
|
+
io_write_bytes: Total bytes written during execution
|
46
|
+
io_read_count: Number of read operations
|
47
|
+
io_write_count: Number of write operations
|
48
|
+
thread_count: Number of threads used
|
49
|
+
context_switches: Number of context switches
|
50
|
+
custom: Dictionary of custom metrics
|
51
|
+
"""
|
52
|
+
|
53
|
+
duration: float = 0.0
|
54
|
+
cpu_percent: float = 0.0
|
55
|
+
memory_mb: float = 0.0
|
56
|
+
memory_delta_mb: float = 0.0
|
57
|
+
io_read_bytes: int = 0
|
58
|
+
io_write_bytes: int = 0
|
59
|
+
io_read_count: int = 0
|
60
|
+
io_write_count: int = 0
|
61
|
+
thread_count: int = 1
|
62
|
+
context_switches: int = 0
|
63
|
+
custom: Dict[str, Any] = field(default_factory=dict)
|
64
|
+
|
65
|
+
def to_task_metrics(self) -> Dict[str, Any]:
|
66
|
+
"""Convert to TaskMetrics compatible format."""
|
67
|
+
return {
|
68
|
+
"duration": self.duration,
|
69
|
+
"memory_usage_mb": self.memory_mb,
|
70
|
+
"cpu_usage": self.cpu_percent,
|
71
|
+
"custom_metrics": {
|
72
|
+
"memory_delta_mb": self.memory_delta_mb,
|
73
|
+
"io_read_bytes": self.io_read_bytes,
|
74
|
+
"io_write_bytes": self.io_write_bytes,
|
75
|
+
"io_read_count": self.io_read_count,
|
76
|
+
"io_write_count": self.io_write_count,
|
77
|
+
"thread_count": self.thread_count,
|
78
|
+
"context_switches": self.context_switches,
|
79
|
+
**self.custom,
|
80
|
+
},
|
81
|
+
}
|
82
|
+
|
83
|
+
|
84
|
+
class MetricsCollector:
|
85
|
+
"""Collects performance metrics during task execution.
|
86
|
+
|
87
|
+
This class provides context managers for collecting detailed performance
|
88
|
+
metrics during node execution, with support for both process-level and
|
89
|
+
system-level monitoring.
|
90
|
+
|
91
|
+
Usage:
|
92
|
+
collector = MetricsCollector()
|
93
|
+
with collector.collect() as metrics:
|
94
|
+
# Execute node code here
|
95
|
+
pass
|
96
|
+
performance_data = metrics.result()
|
97
|
+
"""
|
98
|
+
|
99
|
+
def __init__(self, sampling_interval: float = 0.1):
|
100
|
+
"""Initialize metrics collector.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
sampling_interval: How often to sample metrics (seconds)
|
104
|
+
"""
|
105
|
+
self.sampling_interval = sampling_interval
|
106
|
+
self._monitoring_enabled = PSUTIL_AVAILABLE
|
107
|
+
|
108
|
+
if not self._monitoring_enabled:
|
109
|
+
import warnings
|
110
|
+
|
111
|
+
warnings.warn(
|
112
|
+
"psutil not available. Performance metrics will be limited to duration only. "
|
113
|
+
"Install psutil for comprehensive metrics: pip install psutil"
|
114
|
+
)
|
115
|
+
|
116
|
+
@contextmanager
|
117
|
+
def collect(self, node_id: Optional[str] = None):
|
118
|
+
"""Context manager for collecting metrics during execution.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
node_id: Optional node identifier for tracking
|
122
|
+
|
123
|
+
Yields:
|
124
|
+
MetricsContext: Context object with result() method
|
125
|
+
"""
|
126
|
+
context = MetricsContext(
|
127
|
+
node_id=node_id,
|
128
|
+
sampling_interval=self.sampling_interval,
|
129
|
+
monitoring_enabled=self._monitoring_enabled,
|
130
|
+
)
|
131
|
+
|
132
|
+
try:
|
133
|
+
context.start()
|
134
|
+
yield context
|
135
|
+
finally:
|
136
|
+
context.stop()
|
137
|
+
|
138
|
+
async def collect_async(self, coro, node_id: Optional[str] = None):
|
139
|
+
"""Collect metrics for async execution.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
coro: Coroutine to execute
|
143
|
+
node_id: Optional node identifier
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
Tuple of (result, metrics)
|
147
|
+
"""
|
148
|
+
context = MetricsContext(
|
149
|
+
node_id=node_id,
|
150
|
+
sampling_interval=self.sampling_interval,
|
151
|
+
monitoring_enabled=self._monitoring_enabled,
|
152
|
+
)
|
153
|
+
|
154
|
+
try:
|
155
|
+
context.start()
|
156
|
+
result = await coro
|
157
|
+
return result, context.result()
|
158
|
+
finally:
|
159
|
+
context.stop()
|
160
|
+
|
161
|
+
|
162
|
+
class MetricsContext:
|
163
|
+
"""Context for collecting metrics during a specific execution."""
|
164
|
+
|
165
|
+
def __init__(
|
166
|
+
self, node_id: Optional[str], sampling_interval: float, monitoring_enabled: bool
|
167
|
+
):
|
168
|
+
self.node_id = node_id
|
169
|
+
self.sampling_interval = sampling_interval
|
170
|
+
self.monitoring_enabled = monitoring_enabled
|
171
|
+
|
172
|
+
self.start_time: Optional[float] = None
|
173
|
+
self.end_time: Optional[float] = None
|
174
|
+
self.process: Optional[Any] = None
|
175
|
+
self.initial_io: Optional[Any] = None
|
176
|
+
self.initial_memory: Optional[float] = None
|
177
|
+
self.peak_memory: float = 0.0
|
178
|
+
self.cpu_samples: list = []
|
179
|
+
self.monitoring_thread: Optional[threading.Thread] = None
|
180
|
+
self._stop_monitoring = threading.Event()
|
181
|
+
|
182
|
+
def start(self):
|
183
|
+
"""Start metrics collection."""
|
184
|
+
self.start_time = time.time()
|
185
|
+
|
186
|
+
if self.monitoring_enabled:
|
187
|
+
try:
|
188
|
+
self.process = psutil.Process()
|
189
|
+
self.initial_memory = self.process.memory_info().rss / 1024 / 1024 # MB
|
190
|
+
self.peak_memory = self.initial_memory
|
191
|
+
|
192
|
+
# Get initial I/O counters if available
|
193
|
+
if hasattr(self.process, "io_counters"):
|
194
|
+
try:
|
195
|
+
self.initial_io = self.process.io_counters()
|
196
|
+
except (psutil.AccessDenied, AttributeError):
|
197
|
+
self.initial_io = None
|
198
|
+
|
199
|
+
# Start monitoring thread
|
200
|
+
self._stop_monitoring.clear()
|
201
|
+
self.monitoring_thread = threading.Thread(
|
202
|
+
target=self._monitor_resources
|
203
|
+
)
|
204
|
+
self.monitoring_thread.daemon = True
|
205
|
+
self.monitoring_thread.start()
|
206
|
+
|
207
|
+
except Exception:
|
208
|
+
# Fallback if process monitoring fails
|
209
|
+
self.monitoring_enabled = False
|
210
|
+
|
211
|
+
def stop(self):
|
212
|
+
"""Stop metrics collection."""
|
213
|
+
self.end_time = time.time()
|
214
|
+
|
215
|
+
if self.monitoring_enabled and self.monitoring_thread:
|
216
|
+
self._stop_monitoring.set()
|
217
|
+
self.monitoring_thread.join(timeout=1.0)
|
218
|
+
|
219
|
+
def _monitor_resources(self):
|
220
|
+
"""Monitor resources in background thread."""
|
221
|
+
while not self._stop_monitoring.is_set():
|
222
|
+
try:
|
223
|
+
# Sample CPU usage
|
224
|
+
cpu = self.process.cpu_percent(interval=None)
|
225
|
+
if cpu > 0: # Filter out initial 0 readings
|
226
|
+
self.cpu_samples.append(cpu)
|
227
|
+
|
228
|
+
# Track peak memory
|
229
|
+
memory = self.process.memory_info().rss / 1024 / 1024 # MB
|
230
|
+
self.peak_memory = max(self.peak_memory, memory)
|
231
|
+
|
232
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
233
|
+
break
|
234
|
+
|
235
|
+
self._stop_monitoring.wait(self.sampling_interval)
|
236
|
+
|
237
|
+
def result(self) -> PerformanceMetrics:
|
238
|
+
"""Get collected metrics."""
|
239
|
+
metrics = PerformanceMetrics()
|
240
|
+
|
241
|
+
# Calculate duration
|
242
|
+
if self.start_time and self.end_time:
|
243
|
+
metrics.duration = self.end_time - self.start_time
|
244
|
+
|
245
|
+
if self.monitoring_enabled and self.process:
|
246
|
+
try:
|
247
|
+
# CPU usage (average of samples)
|
248
|
+
if self.cpu_samples:
|
249
|
+
metrics.cpu_percent = sum(self.cpu_samples) / len(self.cpu_samples)
|
250
|
+
|
251
|
+
# Memory metrics
|
252
|
+
metrics.memory_mb = self.peak_memory
|
253
|
+
if self.initial_memory:
|
254
|
+
current_memory = self.process.memory_info().rss / 1024 / 1024
|
255
|
+
metrics.memory_delta_mb = current_memory - self.initial_memory
|
256
|
+
|
257
|
+
# I/O metrics
|
258
|
+
if self.initial_io and hasattr(self.process, "io_counters"):
|
259
|
+
try:
|
260
|
+
current_io = self.process.io_counters()
|
261
|
+
metrics.io_read_bytes = (
|
262
|
+
current_io.read_bytes - self.initial_io.read_bytes
|
263
|
+
)
|
264
|
+
metrics.io_write_bytes = (
|
265
|
+
current_io.write_bytes - self.initial_io.write_bytes
|
266
|
+
)
|
267
|
+
metrics.io_read_count = (
|
268
|
+
current_io.read_count - self.initial_io.read_count
|
269
|
+
)
|
270
|
+
metrics.io_write_count = (
|
271
|
+
current_io.write_count - self.initial_io.write_count
|
272
|
+
)
|
273
|
+
except (psutil.AccessDenied, AttributeError):
|
274
|
+
pass
|
275
|
+
|
276
|
+
# Thread and context switch info
|
277
|
+
try:
|
278
|
+
metrics.thread_count = self.process.num_threads()
|
279
|
+
if hasattr(self.process, "num_ctx_switches"):
|
280
|
+
ctx = self.process.num_ctx_switches()
|
281
|
+
metrics.context_switches = ctx.voluntary + ctx.involuntary
|
282
|
+
except (psutil.AccessDenied, AttributeError):
|
283
|
+
pass
|
284
|
+
|
285
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
286
|
+
pass
|
287
|
+
|
288
|
+
return metrics
|
289
|
+
|
290
|
+
def add_custom_metric(self, name: str, value: Any):
|
291
|
+
"""Add a custom metric."""
|
292
|
+
if not hasattr(self, "_custom_metrics"):
|
293
|
+
self._custom_metrics = {}
|
294
|
+
self._custom_metrics[name] = value
|
295
|
+
|
296
|
+
def get_custom_metrics(self) -> Dict[str, Any]:
|
297
|
+
"""Get custom metrics."""
|
298
|
+
return getattr(self, "_custom_metrics", {})
|
299
|
+
|
300
|
+
|
301
|
+
# Global collector instance for convenience
|
302
|
+
default_collector = MetricsCollector()
|
303
|
+
|
304
|
+
|
305
|
+
def collect_metrics(func: Optional[Callable] = None, *, node_id: Optional[str] = None):
|
306
|
+
"""Decorator for collecting metrics on function execution.
|
307
|
+
|
308
|
+
Can be used as @collect_metrics or @collect_metrics(node_id="my_node")
|
309
|
+
|
310
|
+
Args:
|
311
|
+
func: Function to wrap
|
312
|
+
node_id: Optional node identifier
|
313
|
+
|
314
|
+
Returns:
|
315
|
+
Wrapped function that returns (result, metrics) tuple
|
316
|
+
"""
|
317
|
+
|
318
|
+
def decorator(f):
|
319
|
+
if asyncio.iscoroutinefunction(f):
|
320
|
+
|
321
|
+
async def async_wrapper(*args, **kwargs):
|
322
|
+
result, metrics = await default_collector.collect_async(
|
323
|
+
f(*args, **kwargs), node_id=node_id
|
324
|
+
)
|
325
|
+
return result, metrics
|
326
|
+
|
327
|
+
return async_wrapper
|
328
|
+
else:
|
329
|
+
|
330
|
+
def sync_wrapper(*args, **kwargs):
|
331
|
+
with default_collector.collect(node_id=node_id) as context:
|
332
|
+
result = f(*args, **kwargs)
|
333
|
+
return result, context.result()
|
334
|
+
|
335
|
+
return sync_wrapper
|
336
|
+
|
337
|
+
if func is None:
|
338
|
+
# Called with arguments: @collect_metrics(node_id="...")
|
339
|
+
return decorator
|
340
|
+
else:
|
341
|
+
# Called without arguments: @collect_metrics
|
342
|
+
return decorator(func)
|