levelapp 0.1.15__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.
Files changed (70) hide show
  1. levelapp/__init__.py +0 -0
  2. levelapp/aspects/__init__.py +8 -0
  3. levelapp/aspects/loader.py +253 -0
  4. levelapp/aspects/logger.py +59 -0
  5. levelapp/aspects/monitor.py +617 -0
  6. levelapp/aspects/sanitizer.py +168 -0
  7. levelapp/clients/__init__.py +122 -0
  8. levelapp/clients/anthropic.py +112 -0
  9. levelapp/clients/gemini.py +130 -0
  10. levelapp/clients/groq.py +101 -0
  11. levelapp/clients/huggingface.py +162 -0
  12. levelapp/clients/ionos.py +126 -0
  13. levelapp/clients/mistral.py +106 -0
  14. levelapp/clients/openai.py +116 -0
  15. levelapp/comparator/__init__.py +5 -0
  16. levelapp/comparator/comparator.py +232 -0
  17. levelapp/comparator/extractor.py +108 -0
  18. levelapp/comparator/schemas.py +61 -0
  19. levelapp/comparator/scorer.py +269 -0
  20. levelapp/comparator/utils.py +136 -0
  21. levelapp/config/__init__.py +5 -0
  22. levelapp/config/endpoint.py +199 -0
  23. levelapp/config/prompts.py +57 -0
  24. levelapp/core/__init__.py +0 -0
  25. levelapp/core/base.py +386 -0
  26. levelapp/core/schemas.py +24 -0
  27. levelapp/core/session.py +336 -0
  28. levelapp/endpoint/__init__.py +0 -0
  29. levelapp/endpoint/client.py +188 -0
  30. levelapp/endpoint/client_test.py +41 -0
  31. levelapp/endpoint/manager.py +114 -0
  32. levelapp/endpoint/parsers.py +119 -0
  33. levelapp/endpoint/schemas.py +38 -0
  34. levelapp/endpoint/tester.py +52 -0
  35. levelapp/evaluator/__init__.py +3 -0
  36. levelapp/evaluator/evaluator.py +307 -0
  37. levelapp/metrics/__init__.py +63 -0
  38. levelapp/metrics/embedding.py +56 -0
  39. levelapp/metrics/embeddings/__init__.py +0 -0
  40. levelapp/metrics/embeddings/sentence_transformer.py +30 -0
  41. levelapp/metrics/embeddings/torch_based.py +56 -0
  42. levelapp/metrics/exact.py +182 -0
  43. levelapp/metrics/fuzzy.py +80 -0
  44. levelapp/metrics/token.py +103 -0
  45. levelapp/plugins/__init__.py +0 -0
  46. levelapp/repository/__init__.py +3 -0
  47. levelapp/repository/filesystem.py +203 -0
  48. levelapp/repository/firestore.py +291 -0
  49. levelapp/simulator/__init__.py +3 -0
  50. levelapp/simulator/schemas.py +116 -0
  51. levelapp/simulator/simulator.py +531 -0
  52. levelapp/simulator/utils.py +134 -0
  53. levelapp/visualization/__init__.py +7 -0
  54. levelapp/visualization/charts.py +358 -0
  55. levelapp/visualization/dashboard.py +240 -0
  56. levelapp/visualization/exporter.py +167 -0
  57. levelapp/visualization/templates/base.html +158 -0
  58. levelapp/visualization/templates/comparator_dashboard.html +57 -0
  59. levelapp/visualization/templates/simulator_dashboard.html +111 -0
  60. levelapp/workflow/__init__.py +6 -0
  61. levelapp/workflow/base.py +192 -0
  62. levelapp/workflow/config.py +96 -0
  63. levelapp/workflow/context.py +64 -0
  64. levelapp/workflow/factory.py +42 -0
  65. levelapp/workflow/registration.py +6 -0
  66. levelapp/workflow/runtime.py +19 -0
  67. levelapp-0.1.15.dist-info/METADATA +571 -0
  68. levelapp-0.1.15.dist-info/RECORD +70 -0
  69. levelapp-0.1.15.dist-info/WHEEL +4 -0
  70. levelapp-0.1.15.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,617 @@
1
+ """levelapp/aspects.monitor.py"""
2
+ import threading
3
+ import tracemalloc
4
+ from contextlib import contextmanager
5
+
6
+ from enum import Enum
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass, fields
9
+ from typing import List, Dict, Callable, Any, Union, ParamSpec, TypeVar, runtime_checkable, Protocol, Type
10
+
11
+ from threading import RLock
12
+ from functools import wraps
13
+ from datetime import datetime, timedelta
14
+ from humanize import precisedelta, naturalsize
15
+
16
+ from levelapp.aspects import logger
17
+
18
+
19
+ P = ParamSpec('P')
20
+ T = TypeVar('T')
21
+
22
+
23
+ class MetricType(Enum):
24
+ """Types of metrics that can be collected."""
25
+ SETUP = "setup"
26
+ DATA_LOADING = "data_loading"
27
+ EXECUTION = "execution"
28
+ RESULTS_COLLECTION = "results_collection"
29
+
30
+ API_CALL = "api_call"
31
+ SCORING = "scoring"
32
+ CUSTOM = "custom"
33
+
34
+
35
+ @dataclass
36
+ class ExecutionMetrics:
37
+ """Comprehensive metrics for a function execution."""
38
+ procedure: str
39
+ category: MetricType = MetricType.CUSTOM
40
+ start_time: datetime | None = None
41
+ end_time: datetime | None = None
42
+ duration: float | None = None
43
+ total_api_calls: int = 0
44
+ memory_before: int | None = None
45
+ memory_after: int | None = None
46
+ memory_peak: int | None = None
47
+ cache_hit: bool = False
48
+ error: str | None = None
49
+
50
+ def finalize(self) -> None:
51
+ """Finalize metrics calculation."""
52
+ if self.end_time and self.start_time:
53
+ self.duration = (self.end_time - self.start_time).total_seconds()
54
+
55
+ def update_duration(self, value: float) -> None:
56
+ """Update duration with explicit value."""
57
+ if value < 0:
58
+ raise ValueError("Duration value cannot be negative.")
59
+ self.duration = value
60
+
61
+ def to_dict(self) -> dict:
62
+ """Returns the content of the ExecutionMetrics as a structured dictionary."""
63
+ metrics_dict = {}
64
+ for field_info in fields(self):
65
+ value = getattr(self, field_info.name)
66
+
67
+ # Special handling for enum types to convert them to their value
68
+ if isinstance(value, Enum):
69
+ metrics_dict[field_info.name] = value.name
70
+ elif isinstance(value, datetime):
71
+ metrics_dict[field_info.name] = value.isoformat()
72
+ else:
73
+ metrics_dict[field_info.name] = value
74
+
75
+ return metrics_dict
76
+
77
+
78
+ @dataclass
79
+ class AggregatedStats:
80
+ """Aggregated metrics for monitored functions."""
81
+ total_calls: int = 0
82
+ total_duration: float = 0.0
83
+ min_duration: float = float('inf')
84
+ max_duration: float = 0.0
85
+ error_count: int = 0
86
+ cache_hits: int = 0
87
+ memory_peak: int = 0
88
+ recent_call: datetime | None = None
89
+
90
+ def update(self, metrics: ExecutionMetrics) -> None:
91
+ """Update aggregated metrics with new execution metrics."""
92
+ self.recent_call = datetime.now()
93
+ self.total_calls += 1
94
+
95
+ if metrics.duration is not None:
96
+ self.total_duration += metrics.duration
97
+
98
+ if self.min_duration == float('inf'):
99
+ self.min_duration = metrics.duration
100
+ else:
101
+ self.min_duration = min(self.min_duration, metrics.duration)
102
+
103
+ self.max_duration = max(self.max_duration, metrics.duration)
104
+
105
+ if metrics.error:
106
+ self.error_count += 1
107
+
108
+ if metrics.cache_hit:
109
+ self.cache_hits += 1
110
+
111
+ if metrics.memory_peak:
112
+ self.memory_peak = max(self.memory_peak, metrics.memory_peak)
113
+
114
+ @property
115
+ def average_duration(self) -> float:
116
+ """Average execution duration."""
117
+ return (self.total_duration / self.total_calls) if self.total_calls > 0 else 0.0
118
+
119
+ @property
120
+ def cache_hit_rate(self) -> float:
121
+ """Cache hit rate as a percentage."""
122
+ return (self.cache_hits / self.total_calls * 100) if self.total_calls > 0 else 0.0
123
+
124
+ @property
125
+ def error_rate(self) -> float:
126
+ """Error rate as a percentage."""
127
+ return (self.error_count / self.total_calls * 100) if self.total_calls > 0 else 0.0
128
+
129
+
130
+ @runtime_checkable
131
+ class MetricsCollector(Protocol):
132
+ """Protocol for custom metrics collectors."""
133
+
134
+ def collect_before(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
135
+ """Collect metrics before function execution."""
136
+ ...
137
+
138
+ def collect_after(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
139
+ """Collect metrics after function execution."""
140
+ ...
141
+
142
+
143
+ class MemoryTracker(MetricsCollector):
144
+ """Memory usage metrics collector."""
145
+ def __init__(self):
146
+ self._tracking = False
147
+ self._lock = threading.Lock()
148
+
149
+ @contextmanager
150
+ def _ensure_tracking(self):
151
+ """Context manager to ensure tracemalloc is properly managed."""
152
+ with self._lock:
153
+ if not self._tracking:
154
+ tracemalloc.start()
155
+ self._tracking = True
156
+ try:
157
+ yield
158
+ finally:
159
+ pass
160
+
161
+ def collect_before(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
162
+ with self._ensure_tracking():
163
+ try:
164
+ current, _ = tracemalloc.get_traced_memory()
165
+ collected_metrics.memory_before = current
166
+
167
+ except Exception as e:
168
+ logger.warning(f"[MemoryTracker] Memory tracking failed: {e}")
169
+
170
+ return collected_metrics
171
+
172
+ def collect_after(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
173
+ if self._tracking:
174
+ try:
175
+ current, peak = tracemalloc.get_traced_memory()
176
+ collected_metrics.memory_after = current
177
+ collected_metrics.memory_peak = peak
178
+ return collected_metrics
179
+
180
+ except Exception as e:
181
+ logger.warning(f"Memory tracking failed: {e}")
182
+ return collected_metrics
183
+
184
+ def cleanup(self):
185
+ """Explicit cleanup method."""
186
+ with self._lock:
187
+ if self._tracking:
188
+ try:
189
+ tracemalloc.stop()
190
+ except Exception as e:
191
+ logger.warning(f"Failed to stop tracemalloc: {e}")
192
+ finally:
193
+ self._tracking = False
194
+
195
+
196
+ class APICallTracker(MetricsCollector):
197
+ """API call metrics collector for LLM clients."""
198
+
199
+ def __init__(self):
200
+ self._api_calls = defaultdict(int)
201
+ self._lock = threading.Lock()
202
+
203
+ def collect_before(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
204
+ return collected_metrics
205
+
206
+ def collect_after(self, collected_metrics: ExecutionMetrics) -> ExecutionMetrics:
207
+ with self._lock:
208
+ if collected_metrics.category == MetricType.API_CALL:
209
+ self._api_calls[collected_metrics.procedure] += 1
210
+ collected_metrics.total_api_calls = self._api_calls[collected_metrics.procedure]
211
+
212
+ return collected_metrics
213
+
214
+
215
+ class FunctionMonitor:
216
+ """Core function monitoring system."""
217
+
218
+ def __init__(self, max_history: int = 1000):
219
+ self._lock = RLock()
220
+ self._max_history = max_history
221
+ self._monitored_procedures: Dict[str, Callable[..., Any]] = {}
222
+ self._execution_history: Dict[str, List[ExecutionMetrics]] = defaultdict(list)
223
+ self._aggregated_stats: Dict[str, AggregatedStats] = defaultdict(AggregatedStats)
224
+
225
+ self._collectors: List[MetricsCollector] = []
226
+
227
+ self.add_collector(MemoryTracker())
228
+ self.add_collector(APICallTracker())
229
+
230
+ def update_procedure_duration(self, name: str, value: float) -> None:
231
+ """
232
+ Update the duration of a monitored procedure by name.
233
+
234
+ Args:
235
+ name: The name of the procedure to retrieve.
236
+ value: The value to retrieve for the procedure.
237
+ """
238
+ with self._lock:
239
+ history = self._execution_history.get(name, [])
240
+ if not history:
241
+ logger.warning(f"[FunctionMonitor] ne execution history found for proc: {name}")
242
+ return
243
+
244
+ latest_entry = history[-1]
245
+ latest_entry.update_duration(value=value)
246
+ self._aggregated_stats[name] = AggregatedStats()
247
+
248
+ for entry in history:
249
+ self._aggregated_stats[name].update(metrics=entry)
250
+
251
+ def add_collector(self, collector: MetricsCollector) -> None:
252
+ """
253
+ Add a custom metrics collector to the monitor.
254
+
255
+ Args:
256
+ collector: An instance of a class implementing MetricsCollector protocol.
257
+ """
258
+ with self._lock:
259
+ self._collectors.append(collector)
260
+
261
+ def remove_collector(self, collector: MetricsCollector) -> None:
262
+ """
263
+ Remove a custom metrics collector from the monitor.
264
+
265
+ Args:
266
+ collector: An instance of a class implementing MetricsCollector protocol.
267
+ """
268
+ with self._lock:
269
+ if collector in self._collectors:
270
+ self._collectors.remove(collector)
271
+
272
+ def _collect_metrics_before(self, execution_metrics: ExecutionMetrics) -> ExecutionMetrics:
273
+ """Collect metrics before function execution using registered collectors."""
274
+ result_metrics = execution_metrics
275
+ for collector in self._collectors:
276
+ try:
277
+ result_metrics = collector.collect_before(result_metrics)
278
+ except Exception as e:
279
+ logger.warning(f"Metrics collector {type(collector).__name__} failed: {e}")
280
+
281
+ return result_metrics
282
+
283
+ def _collect_metrics_after(self, execution_metrics: ExecutionMetrics) -> ExecutionMetrics:
284
+ """Collect metrics after function execution using registered collectors."""
285
+ result_metrics = execution_metrics
286
+ for collector in self._collectors:
287
+ try:
288
+ result_metrics = collector.collect_after(result_metrics)
289
+ except Exception as e:
290
+ logger.warning(f"Metrics collector {type(collector).__name__} failed: {e}")
291
+ return result_metrics
292
+
293
+ @staticmethod
294
+ def _apply_caching(func: Callable[P, T], maxsize: int | None) -> Callable[P, T]:
295
+ if maxsize is None:
296
+ return func
297
+
298
+ def make_args_hashable(args, kwargs):
299
+ hashable_args = tuple(_make_hashable(a) for a in args)
300
+ hashable_kwargs = tuple(sorted((k, _make_hashable(v)) for k, v in kwargs.items()))
301
+ return hashable_args, hashable_kwargs
302
+
303
+ cache = {}
304
+ cache_info = {"hits": 0, "misses": 0}
305
+
306
+ @wraps(func)
307
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
308
+ nonlocal cache_info
309
+ cache_key = make_args_hashable(args, kwargs)
310
+
311
+ if cache_key in cache:
312
+ cache_info["hits"] += 1
313
+ return cache[cache_key]
314
+
315
+ cache_info["misses"] += 1
316
+ result = func(*args, **kwargs)
317
+ cache[cache_key] = result
318
+
319
+ # Enforce maxsize
320
+ if len(cache) > maxsize:
321
+ cache.pop(next(iter(cache)))
322
+ return result
323
+
324
+ # Add cache management methods
325
+ def get_cache_info():
326
+ return dict(cache_info)
327
+
328
+ def clear_cache():
329
+ nonlocal cache, cache_info
330
+ cache.clear()
331
+ cache_info = {"hits": 0, "misses": 0}
332
+
333
+ wrapper.cache_info = get_cache_info
334
+ wrapper.cache_clear = clear_cache
335
+ wrapper._cache = cache # For debugging/inspection
336
+
337
+ return wrapper
338
+
339
+ def _wrap_execution(
340
+ self,
341
+ func: Callable[P, T],
342
+ name: str,
343
+ category: MetricType,
344
+ enable_timing: bool,
345
+ track_memory: bool,
346
+ verbose=False
347
+ ) -> Callable[P, T]:
348
+ """
349
+ Wrap function execution with timing and error handling.
350
+
351
+ Args:
352
+ func: Function to be wrapped
353
+ name: Unique identifier for the function
354
+ enable_timing: Enable execution time logging
355
+ track_memory: Enable memory tracking
356
+ verbose: Enable verbose logging
357
+
358
+ Returns:
359
+ Wrapped function
360
+ """
361
+ @wraps(func)
362
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
363
+ metrics = ExecutionMetrics(
364
+ procedure=name,
365
+ category=category,
366
+ )
367
+
368
+ if enable_timing:
369
+ metrics.start_time = datetime.now()
370
+
371
+ # Collect pre-execution metrics
372
+ if track_memory:
373
+ self._collect_metrics_before(execution_metrics=metrics)
374
+
375
+ try:
376
+ result = func(*args, **kwargs)
377
+
378
+ cache_hit_info = getattr(func, 'cache_hit_info', None)
379
+ if hasattr(func, 'cache_info') and cache_hit_info is not None:
380
+ metrics.cache_hit = getattr(cache_hit_info, 'is_hit', False)
381
+
382
+ return result
383
+
384
+ except Exception as e:
385
+ metrics.error = str(e)
386
+ logger.error(f"Error in '{name}': {str(e)}", exc_info=True)
387
+ raise
388
+
389
+ finally:
390
+ if enable_timing:
391
+ metrics.end_time = datetime.now()
392
+ metrics.finalize()
393
+
394
+ if track_memory:
395
+ metrics = self._collect_metrics_after(execution_metrics=metrics)
396
+
397
+ # store metrics
398
+ with self._lock:
399
+ history = self._execution_history[name]
400
+ history.append(metrics)
401
+
402
+ if len(history) > self._max_history:
403
+ history.pop(0)
404
+
405
+ self._aggregated_stats[name].update(metrics=metrics)
406
+
407
+ if verbose and enable_timing and metrics.duration is not None:
408
+ log_message = f"[FunctionMonitor] Executed '{name}' in {metrics.duration:.4f}s"
409
+ if metrics.cache_hit:
410
+ log_message += " (cache hit)"
411
+ if metrics.memory_peak:
412
+ log_message += f" (memory peak: {metrics.memory_peak / 1024 / 1024:.2f} MB)"
413
+ logger.info(log_message)
414
+
415
+ return wrapped
416
+
417
+ def monitor(
418
+ self,
419
+ name: str,
420
+ category: MetricType = MetricType.CUSTOM,
421
+ cached: bool = False,
422
+ maxsize: int | None = 128,
423
+ enable_timing: bool = True,
424
+ track_memory: bool = True,
425
+ collectors: List[Type[MetricsCollector]] | None = None,
426
+ verbose: bool = False
427
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
428
+ """
429
+ Decorator factory for monitoring functions.
430
+
431
+ Args:
432
+ name: Unique identifier for the function
433
+ category: Category of the metric (e.g., API_CALL, SCORING)
434
+ cached: Enable LRU caching
435
+ maxsize: Maximum cache size
436
+ enable_timing: Record execution time
437
+ track_memory: Track memory usage
438
+ collectors: Optional list of custom metrics collectors
439
+
440
+ Returns:
441
+ Callable[[Callable[P, T]], Callable[P, T]]: Decorator function
442
+ """
443
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
444
+ if collectors:
445
+ for collector in collectors:
446
+ self.add_collector(collector)
447
+
448
+ if cached:
449
+ func = self._apply_caching(func=func, maxsize=maxsize)
450
+
451
+ monitored_func = self._wrap_execution(
452
+ func=func,
453
+ name=name,
454
+ category=category,
455
+ enable_timing=enable_timing,
456
+ track_memory=track_memory,
457
+ )
458
+
459
+ with self._lock:
460
+ if name in self._monitored_procedures and verbose:
461
+ raise ValueError(f"Function '{name}' is already registered.")
462
+
463
+ self._monitored_procedures[name] = monitored_func
464
+
465
+ return monitored_func
466
+
467
+ return decorator
468
+
469
+ def list_monitored_functions(self) -> Dict[str, Callable[..., Any]]:
470
+ """
471
+ List all registered monitored functions.
472
+
473
+ Returns:
474
+ List[str]: Names of all registered functions
475
+ """
476
+ with self._lock:
477
+ return dict(self._monitored_procedures)
478
+
479
+ def get_stats(self, name: str) -> Dict[str, Any] | None:
480
+ """
481
+ Get comprehensive statistics for a monitored function.
482
+
483
+ Args:
484
+ name (str): Name of the monitored function.
485
+
486
+ Returns:
487
+ Dict[str, Any] | None: Dictionary containing function statistics or None if not found.
488
+ """
489
+ with self._lock:
490
+ if name not in self._monitored_procedures:
491
+ return None
492
+
493
+ func = self._monitored_procedures[name]
494
+ stats = self._aggregated_stats[name]
495
+
496
+ min_duration = stats.min_duration if stats.min_duration != float('inf') else 0.0
497
+
498
+ return {
499
+ 'name': name,
500
+ 'total_calls': stats.total_calls,
501
+ 'avg_duration': precisedelta(
502
+ timedelta(seconds=stats.average_duration),
503
+ suppress=['minutes'],
504
+ format='%.4f'
505
+ ) if stats.average_duration > 0 else "0.000s",
506
+ 'min_duration': precisedelta(
507
+ timedelta(seconds=min_duration),
508
+ suppress=['minutes'],
509
+ format='%.4f'
510
+ ),
511
+ 'max_duration': precisedelta(
512
+ timedelta(seconds=stats.max_duration),
513
+ suppress=['minutes'],
514
+ format='%.4f'
515
+ ),
516
+ 'error_rate': f"{stats.error_rate:.2f}%",
517
+ 'cache_hit_rate': f"{stats.cache_hit_rate}%",
518
+ 'memory_peak_mb': naturalsize(stats.memory_peak) if stats.memory_peak > 0 else "0 B",
519
+ 'last_called': stats.recent_call.isoformat() if stats.recent_call else None,
520
+ 'recent_execution': stats.recent_call.isoformat() if stats.recent_call else None,
521
+ 'is_cached': hasattr(func, 'cache_info'),
522
+ 'cache_info': func.cache_info() if hasattr(func, 'cache_info') else None
523
+ }
524
+
525
+ def get_all_stats(self) -> Dict[str, Dict[str, Any]]:
526
+ """Get statistics for all monitored functions."""
527
+ with self._lock:
528
+ return {
529
+ name: self.get_stats(name)
530
+ for name in self._monitored_procedures.keys()
531
+ }
532
+
533
+ def get_execution_history(
534
+ self,
535
+ name: str | None = None,
536
+ category: MetricType | None = None,
537
+ limit: int | None = None
538
+ ) -> list[ExecutionMetrics]:
539
+ """Get execution history filtered by procedure name or category."""
540
+ with self._lock:
541
+ if name:
542
+ history = self._execution_history.get(name, [])
543
+ else:
544
+ history = [m for h in self._execution_history.values() for m in h]
545
+
546
+ if category:
547
+ history = [m for m in history if m.category == category]
548
+
549
+ history.sort(key=lambda m: m.start_time or 0)
550
+ return history[-limit:] if limit else history
551
+
552
+ def clear_history(self, procedure: str | None = None) -> None:
553
+ """Clear execution history."""
554
+ with self._lock:
555
+ if procedure:
556
+ if procedure in self._execution_history:
557
+ self._execution_history[procedure].clear()
558
+ if procedure in self._aggregated_stats:
559
+ self._aggregated_stats[procedure] = AggregatedStats()
560
+ else:
561
+ self._execution_history.clear()
562
+ self._aggregated_stats.clear()
563
+
564
+ def export_metrics(self, output_format: str = 'dict') -> Union[Dict[str, Any], str]:
565
+ """
566
+ Export all metrics in various formats.
567
+
568
+ Args:
569
+ output_format (str): Format for exporting metrics ('dict' or 'json').
570
+
571
+ Returns:
572
+ Union[Dict[str, Any], str]: Exported metrics in the specified format.
573
+ """
574
+ with self._lock:
575
+ data = {
576
+ 'timestamp': datetime.now().isoformat(),
577
+ 'functions': self.get_all_stats(),
578
+ 'total_executions': sum(
579
+ len(history) for history in self._execution_history.values()
580
+ ),
581
+ 'collectors': [type(c).__name__ for c in self._collectors]
582
+ }
583
+
584
+ if output_format == 'dict':
585
+ return data
586
+ elif output_format == 'json':
587
+ import json
588
+ return json.dumps(data, indent=2, default=str)
589
+ else:
590
+ raise ValueError(f"Unsupported format: {output_format}")
591
+
592
+ def cleanup(self):
593
+ """Cleanup resources."""
594
+ with self._lock:
595
+ for collector in self._collectors:
596
+ if hasattr(collector, 'cleanup'):
597
+ try:
598
+ collector.cleanup()
599
+ except Exception as e:
600
+ logger.warning(f"Collector cleanup failed: {e}")
601
+
602
+
603
+ MonitoringAspect = FunctionMonitor()
604
+
605
+
606
+ def _make_hashable(obj):
607
+ """Convert potentially unhashable objects to a hashable representation."""
608
+ if isinstance(obj, defaultdict):
609
+ return 'defaultdict', _make_hashable(dict(obj))
610
+
611
+ elif isinstance(obj, dict):
612
+ return tuple(sorted((k, _make_hashable(v)) for k, v in obj.items()))
613
+
614
+ elif isinstance(obj, (list, set, tuple)):
615
+ return tuple(_make_hashable(v) for v in obj)
616
+
617
+ return obj