mcli-framework 7.0.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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import FrameType
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
15
|
+
|
|
16
|
+
import psutil
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class McliLogger:
|
|
20
|
+
"""
|
|
21
|
+
Central logger for mcli that logs only to file, not to console.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_instance = None
|
|
25
|
+
_runtime_tracing_enabled = False
|
|
26
|
+
_system_tracing_enabled = False
|
|
27
|
+
_system_trace_interval = 5 # Default to check every 5 seconds
|
|
28
|
+
_system_trace_process_ids = set() # Process IDs to monitor
|
|
29
|
+
_system_trace_thread = None
|
|
30
|
+
_excluded_modules: Set[str] = set()
|
|
31
|
+
_trace_level = 0 # 0=off, 1=function calls, 2=line by line, 3=verbose
|
|
32
|
+
_system_trace_level = 0 # 0=off, 1=basic, 2=detailed
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_logger(cls, name="mcli.out"):
|
|
36
|
+
"""Get or create the singleton logger instance."""
|
|
37
|
+
if cls._instance is None:
|
|
38
|
+
cls._instance = cls(name)
|
|
39
|
+
return cls._instance.logger
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_trace_logger(cls):
|
|
43
|
+
"""Get the trace logger instance for interpreter trace events."""
|
|
44
|
+
if cls._instance is None:
|
|
45
|
+
cls._instance = cls("mcli.out")
|
|
46
|
+
return cls._instance.trace_logger
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_system_trace_logger(cls):
|
|
50
|
+
"""Get the system trace logger instance for OS process monitoring."""
|
|
51
|
+
if cls._instance is None:
|
|
52
|
+
cls._instance = cls("mcli.out")
|
|
53
|
+
return cls._instance.system_trace_logger
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def enable_runtime_tracing(cls, level: int = 1, excluded_modules: Optional[List[str]] = None):
|
|
57
|
+
"""
|
|
58
|
+
Enable Python interpreter runtime tracing.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
level: Tracing detail level (0=off, 1=function calls only, 2=line by line, 3=verbose)
|
|
62
|
+
excluded_modules: List of module prefixes to exclude from tracing
|
|
63
|
+
"""
|
|
64
|
+
if cls._instance is None:
|
|
65
|
+
cls._instance = cls("mcli.out")
|
|
66
|
+
|
|
67
|
+
cls._trace_level = max(0, min(level, 3)) # Clamp to 0-3
|
|
68
|
+
|
|
69
|
+
if excluded_modules:
|
|
70
|
+
cls._excluded_modules = set(excluded_modules)
|
|
71
|
+
else:
|
|
72
|
+
# Default exclusions to avoid excessive logging
|
|
73
|
+
cls._excluded_modules = {
|
|
74
|
+
"logging",
|
|
75
|
+
"importlib",
|
|
76
|
+
"typing",
|
|
77
|
+
"abc",
|
|
78
|
+
"inspect",
|
|
79
|
+
"pkg_resources",
|
|
80
|
+
"encodings",
|
|
81
|
+
"_weakrefset",
|
|
82
|
+
"weakref",
|
|
83
|
+
"sre_",
|
|
84
|
+
"re",
|
|
85
|
+
"functools",
|
|
86
|
+
"threading",
|
|
87
|
+
"copyreg",
|
|
88
|
+
"collections",
|
|
89
|
+
"_collections_abc",
|
|
90
|
+
"enum",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if cls._trace_level > 0 and not cls._runtime_tracing_enabled:
|
|
94
|
+
# Enable tracing
|
|
95
|
+
sys.settrace(cls._instance._trace_callback)
|
|
96
|
+
threading.settrace(cls._instance._trace_callback)
|
|
97
|
+
cls._runtime_tracing_enabled = True
|
|
98
|
+
cls._instance.trace_logger.info(
|
|
99
|
+
f"Python interpreter tracing enabled (level={cls._trace_level})"
|
|
100
|
+
)
|
|
101
|
+
elif cls._trace_level == 0 and cls._runtime_tracing_enabled:
|
|
102
|
+
# Disable tracing
|
|
103
|
+
sys.settrace(None)
|
|
104
|
+
threading.settrace(None)
|
|
105
|
+
cls._runtime_tracing_enabled = False
|
|
106
|
+
cls._instance.trace_logger.info("Python interpreter tracing disabled")
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def enable_system_tracing(cls, level: int = 1, interval: int = 5):
|
|
110
|
+
"""
|
|
111
|
+
Enable OS-level system tracing for process monitoring.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
level: System trace level (0=off, 1=basic info, 2=detailed)
|
|
115
|
+
interval: Monitoring interval in seconds
|
|
116
|
+
"""
|
|
117
|
+
if cls._instance is None:
|
|
118
|
+
cls._instance = cls("mcli.out")
|
|
119
|
+
|
|
120
|
+
# Add current process to monitoring
|
|
121
|
+
cls._system_trace_process_ids.add(os.getpid())
|
|
122
|
+
cls._system_trace_interval = max(1, interval) # Minimum 1 second
|
|
123
|
+
cls._system_trace_level = max(0, min(level, 2)) # Clamp to 0-2
|
|
124
|
+
|
|
125
|
+
# Start monitoring thread if not already running
|
|
126
|
+
if cls._system_trace_level > 0 and not cls._system_tracing_enabled:
|
|
127
|
+
if cls._system_trace_thread is None or not cls._system_trace_thread.is_alive():
|
|
128
|
+
cls._system_tracing_enabled = True
|
|
129
|
+
cls._system_trace_thread = threading.Thread(
|
|
130
|
+
target=cls._instance._system_trace_worker, daemon=True
|
|
131
|
+
)
|
|
132
|
+
cls._system_trace_thread.start()
|
|
133
|
+
cls._instance.system_trace_logger.info(
|
|
134
|
+
f"System process tracing enabled (level={cls._system_trace_level}, interval={cls._system_trace_interval}s)"
|
|
135
|
+
)
|
|
136
|
+
elif cls._system_trace_level == 0 and cls._system_tracing_enabled:
|
|
137
|
+
# Disable tracing
|
|
138
|
+
cls._system_tracing_enabled = False
|
|
139
|
+
if cls._system_trace_thread and cls._system_trace_thread.is_alive():
|
|
140
|
+
# Thread will terminate on its own when _system_tracing_enabled is False
|
|
141
|
+
cls._instance.system_trace_logger.info("System process tracing disabled")
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def disable_system_tracing(cls):
|
|
145
|
+
"""Disable OS-level system tracing."""
|
|
146
|
+
cls.enable_system_tracing(level=0)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def register_process(cls, pid: int):
|
|
150
|
+
"""Register a process for monitoring."""
|
|
151
|
+
if cls._instance is None:
|
|
152
|
+
cls._instance = cls("mcli.out")
|
|
153
|
+
|
|
154
|
+
if pid > 0:
|
|
155
|
+
cls._system_trace_process_ids.add(pid)
|
|
156
|
+
cls._instance.system_trace_logger.info(f"Registered process ID {pid} for monitoring")
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def register_subprocess(cls, proc: subprocess.Popen) -> int:
|
|
162
|
+
"""
|
|
163
|
+
Register a subprocess.Popen object for monitoring.
|
|
164
|
+
Returns the process ID if successful, 0 otherwise.
|
|
165
|
+
"""
|
|
166
|
+
if proc and proc.pid:
|
|
167
|
+
if cls.register_process(proc.pid):
|
|
168
|
+
return proc.pid
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def unregister_process(cls, pid: int):
|
|
173
|
+
"""Remove a process from monitoring."""
|
|
174
|
+
if cls._instance is None:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
if pid in cls._system_trace_process_ids:
|
|
178
|
+
cls._system_trace_process_ids.remove(pid)
|
|
179
|
+
cls._instance.system_trace_logger.info(f"Unregistered process ID {pid} from monitoring")
|
|
180
|
+
|
|
181
|
+
@classmethod
|
|
182
|
+
def disable_runtime_tracing(cls):
|
|
183
|
+
"""Disable Python interpreter runtime tracing."""
|
|
184
|
+
cls.enable_runtime_tracing(level=0)
|
|
185
|
+
|
|
186
|
+
def __init__(self, name="mcli.out"):
|
|
187
|
+
self.name = name
|
|
188
|
+
self.logger = logging.getLogger(name)
|
|
189
|
+
self.trace_logger = logging.getLogger(f"{name}.trace")
|
|
190
|
+
self.system_trace_logger = logging.getLogger(f"{name}.system")
|
|
191
|
+
|
|
192
|
+
# Set to DEBUG to capture all levels in the log file
|
|
193
|
+
self.logger.setLevel(logging.DEBUG)
|
|
194
|
+
self.trace_logger.setLevel(logging.DEBUG)
|
|
195
|
+
self.system_trace_logger.setLevel(logging.DEBUG)
|
|
196
|
+
|
|
197
|
+
self.logger.propagate = False
|
|
198
|
+
self.trace_logger.propagate = False
|
|
199
|
+
self.system_trace_logger.propagate = False
|
|
200
|
+
|
|
201
|
+
# Clear any existing handlers
|
|
202
|
+
if self.logger.handlers:
|
|
203
|
+
self.logger.handlers.clear()
|
|
204
|
+
if self.trace_logger.handlers:
|
|
205
|
+
self.trace_logger.handlers.clear()
|
|
206
|
+
if self.system_trace_logger.handlers:
|
|
207
|
+
self.system_trace_logger.handlers.clear()
|
|
208
|
+
|
|
209
|
+
# Set up file handler with path resolution
|
|
210
|
+
try:
|
|
211
|
+
# Find the project root directory (where logs dir should be)
|
|
212
|
+
# Start with the current file and navigate up the directory structure
|
|
213
|
+
current_file = Path(__file__)
|
|
214
|
+
# Go up 5 levels: file -> logger -> lib -> mcli -> src -> repo_root
|
|
215
|
+
project_root = current_file.parents[4]
|
|
216
|
+
log_dir = project_root / "logs"
|
|
217
|
+
log_dir.mkdir(exist_ok=True)
|
|
218
|
+
|
|
219
|
+
# Create daily log file
|
|
220
|
+
timestamp = datetime.now().strftime("%Y%m%d")
|
|
221
|
+
log_file = log_dir / f"mcli_{timestamp}.log"
|
|
222
|
+
trace_log_file = log_dir / f"mcli_trace_{timestamp}.log"
|
|
223
|
+
system_trace_log_file = log_dir / f"mcli_system_{timestamp}.log"
|
|
224
|
+
|
|
225
|
+
# Configure regular file handler
|
|
226
|
+
file_handler = logging.FileHandler(log_file)
|
|
227
|
+
file_handler.setLevel(logging.DEBUG) # Capture all levels in the file
|
|
228
|
+
file_formatter = logging.Formatter("%(asctime)s [%(levelname)s] [%(name)s] %(message)s")
|
|
229
|
+
file_handler.setFormatter(file_formatter)
|
|
230
|
+
self.logger.addHandler(file_handler)
|
|
231
|
+
|
|
232
|
+
# Configure trace file handler
|
|
233
|
+
trace_handler = logging.FileHandler(trace_log_file)
|
|
234
|
+
trace_handler.setLevel(logging.DEBUG)
|
|
235
|
+
trace_formatter = logging.Formatter("%(asctime)s [TRACE] %(message)s")
|
|
236
|
+
trace_handler.setFormatter(trace_formatter)
|
|
237
|
+
self.trace_logger.addHandler(trace_handler)
|
|
238
|
+
|
|
239
|
+
# Configure system trace file handler
|
|
240
|
+
system_trace_handler = logging.FileHandler(system_trace_log_file)
|
|
241
|
+
system_trace_handler.setLevel(logging.DEBUG)
|
|
242
|
+
system_trace_formatter = logging.Formatter("%(asctime)s [SYSTEM] %(message)s")
|
|
243
|
+
system_trace_handler.setFormatter(system_trace_formatter)
|
|
244
|
+
self.system_trace_logger.addHandler(system_trace_handler)
|
|
245
|
+
|
|
246
|
+
# Log the path to help with debugging
|
|
247
|
+
self.logger.debug(f"Logging to: {log_file}")
|
|
248
|
+
self.trace_logger.debug(f"Trace logging to: {trace_log_file}")
|
|
249
|
+
self.system_trace_logger.debug(f"System trace logging to: {system_trace_log_file}")
|
|
250
|
+
except Exception as e:
|
|
251
|
+
# If we can't set up file logging, fall back to stderr
|
|
252
|
+
# This should only happen during development or in unusual environments
|
|
253
|
+
fallback_handler = logging.StreamHandler(sys.stderr)
|
|
254
|
+
fallback_handler.setLevel(logging.ERROR)
|
|
255
|
+
fallback_formatter = logging.Formatter(
|
|
256
|
+
"[FALLBACK] %(asctime)s [%(levelname)s] %(message)s"
|
|
257
|
+
)
|
|
258
|
+
fallback_handler.setFormatter(fallback_formatter)
|
|
259
|
+
self.logger.addHandler(fallback_handler)
|
|
260
|
+
self.trace_logger.addHandler(fallback_handler)
|
|
261
|
+
self.system_trace_logger.addHandler(fallback_handler)
|
|
262
|
+
self.logger.error(f"Failed to set up file logging: {e}. Using stderr fallback.")
|
|
263
|
+
|
|
264
|
+
def _should_trace(self, filename: str) -> bool:
|
|
265
|
+
"""Determine if a file should be traced based on exclusion rules."""
|
|
266
|
+
# Skip files in standard library
|
|
267
|
+
if filename.startswith(sys.prefix) or "<" in filename:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
# Get module name from filename
|
|
271
|
+
module_name = os.path.basename(filename)
|
|
272
|
+
if module_name.endswith(".py"):
|
|
273
|
+
module_name = module_name[:-3]
|
|
274
|
+
|
|
275
|
+
# Skip excluded modules
|
|
276
|
+
for excluded in self._excluded_modules:
|
|
277
|
+
if module_name.startswith(excluded):
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def _get_process_info(self, pid: int, detailed: bool = False) -> Dict[str, Any]:
|
|
283
|
+
"""Get information about a process given its PID."""
|
|
284
|
+
try:
|
|
285
|
+
# Check if process exists
|
|
286
|
+
if not psutil.pid_exists(pid):
|
|
287
|
+
return {"pid": pid, "status": "NOT_FOUND"}
|
|
288
|
+
|
|
289
|
+
# Get process info
|
|
290
|
+
proc = psutil.Process(pid)
|
|
291
|
+
basic_info = {
|
|
292
|
+
"pid": pid,
|
|
293
|
+
"name": proc.name(),
|
|
294
|
+
"status": proc.status(),
|
|
295
|
+
"cpu_percent": proc.cpu_percent(interval=0.1), # Quick sampling
|
|
296
|
+
"memory_percent": proc.memory_percent(),
|
|
297
|
+
"create_time": datetime.fromtimestamp(proc.create_time()).strftime(
|
|
298
|
+
"%Y-%m-%d %H:%M:%S"
|
|
299
|
+
),
|
|
300
|
+
"running_time": time.time() - proc.create_time(),
|
|
301
|
+
"num_threads": proc.num_threads(),
|
|
302
|
+
"username": proc.username(),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Add more detailed information if requested
|
|
306
|
+
if detailed:
|
|
307
|
+
try:
|
|
308
|
+
# These can sometimes raise exceptions depending on permissions
|
|
309
|
+
children = proc.children(recursive=True)
|
|
310
|
+
io_counters = proc.io_counters()
|
|
311
|
+
connections = proc.connections()
|
|
312
|
+
open_files = proc.open_files()
|
|
313
|
+
|
|
314
|
+
detailed_info = {
|
|
315
|
+
"cmdline": " ".join(proc.cmdline()),
|
|
316
|
+
"exe": proc.exe(),
|
|
317
|
+
"cwd": proc.cwd(),
|
|
318
|
+
"nice": proc.nice(),
|
|
319
|
+
"io_counters": {
|
|
320
|
+
"read_bytes": io_counters.read_bytes,
|
|
321
|
+
"write_bytes": io_counters.write_bytes,
|
|
322
|
+
},
|
|
323
|
+
"num_fds": proc.num_fds() if hasattr(proc, "num_fds") else None,
|
|
324
|
+
"num_connections": len(connections),
|
|
325
|
+
"num_open_files": len(open_files),
|
|
326
|
+
"open_files": [f.path for f in open_files[:5]], # First 5 files
|
|
327
|
+
"children": [
|
|
328
|
+
{"pid": child.pid, "name": child.name()} for child in children[:5]
|
|
329
|
+
], # First 5 children
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
basic_info.update(detailed_info)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
# Add error info but don't fail
|
|
335
|
+
basic_info["detailed_error"] = str(e)
|
|
336
|
+
|
|
337
|
+
return basic_info
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
return {"pid": pid, "status": "ERROR", "error": str(e)}
|
|
341
|
+
|
|
342
|
+
def _format_process_info(self, info: Dict[str, Any]) -> str:
|
|
343
|
+
"""Format process information for logging."""
|
|
344
|
+
if info.get("status") == "NOT_FOUND":
|
|
345
|
+
return f"Process {info['pid']} no longer exists"
|
|
346
|
+
|
|
347
|
+
if info.get("status") == "ERROR":
|
|
348
|
+
return f"Error getting info for process {info['pid']}: {info.get('error', 'Unknown error')}"
|
|
349
|
+
|
|
350
|
+
# Format basic info
|
|
351
|
+
lines = [
|
|
352
|
+
f"Process {info['pid']} ({info['name']}):",
|
|
353
|
+
f" Status: {info['status']}",
|
|
354
|
+
f" CPU: {info['cpu_percent']:.1f}%, Memory: {info['memory_percent']:.1f}%",
|
|
355
|
+
f" Running time: {info['running_time']:.1f} seconds",
|
|
356
|
+
f" Threads: {info['num_threads']}",
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
# Add detailed info if available
|
|
360
|
+
if "cmdline" in info:
|
|
361
|
+
lines.extend(
|
|
362
|
+
[
|
|
363
|
+
f" Command: {info['cmdline'][:100]}{'...' if len(info['cmdline']) > 100 else ''}",
|
|
364
|
+
f" Working directory: {info['cwd']}",
|
|
365
|
+
f" IO: read={info['io_counters']['read_bytes']/1024:.1f} KB, write={info['io_counters']['write_bytes']/1024:.1f} KB",
|
|
366
|
+
f" Open files: {info['num_open_files']} (sample: {', '.join(info['open_files'][:3]) if info['open_files'] else 'none'})",
|
|
367
|
+
f" Child processes: {len(info['children'])} (sample: child info available)",
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return "\n".join(lines)
|
|
372
|
+
|
|
373
|
+
def _system_trace_worker(self):
|
|
374
|
+
"""Worker thread that periodically collects and logs process information."""
|
|
375
|
+
while self._system_tracing_enabled:
|
|
376
|
+
try:
|
|
377
|
+
# Check all registered processes
|
|
378
|
+
current_pids = set(
|
|
379
|
+
self._system_trace_process_ids
|
|
380
|
+
) # Make a copy to avoid modification during iteration
|
|
381
|
+
|
|
382
|
+
for pid in current_pids:
|
|
383
|
+
try:
|
|
384
|
+
detailed = self._system_trace_level >= 2
|
|
385
|
+
process_info = self._get_process_info(pid, detailed)
|
|
386
|
+
|
|
387
|
+
# Format and log the information
|
|
388
|
+
if process_info.get("status") != "NOT_FOUND":
|
|
389
|
+
formatted_info = self._format_process_info(process_info)
|
|
390
|
+
self.system_trace_logger.info(formatted_info)
|
|
391
|
+
else:
|
|
392
|
+
# Process no longer exists
|
|
393
|
+
self.system_trace_logger.info(
|
|
394
|
+
f"Process {pid} no longer exists, removing from monitoring"
|
|
395
|
+
)
|
|
396
|
+
self._system_trace_process_ids.discard(pid)
|
|
397
|
+
|
|
398
|
+
# Look for child processes if detailed tracing is enabled
|
|
399
|
+
if detailed and process_info.get("status") not in ["NOT_FOUND", "ERROR"]:
|
|
400
|
+
try:
|
|
401
|
+
proc = psutil.Process(pid)
|
|
402
|
+
children = proc.children(recursive=False)
|
|
403
|
+
|
|
404
|
+
for child in children:
|
|
405
|
+
# If we find a child not already being monitored, add it
|
|
406
|
+
if child.pid not in self._system_trace_process_ids:
|
|
407
|
+
self._system_trace_process_ids.add(child.pid)
|
|
408
|
+
self.system_trace_logger.info(
|
|
409
|
+
f"Added child process {child.pid} ({child.name()}) to monitoring"
|
|
410
|
+
)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
self.system_trace_logger.error(
|
|
413
|
+
f"Error getting child processes for {pid}: {e}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
except Exception as e:
|
|
417
|
+
self.system_trace_logger.error(
|
|
418
|
+
f"Error in system trace for process {pid}: {e}"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Add a separator between trace cycles for readability
|
|
422
|
+
if current_pids:
|
|
423
|
+
self.system_trace_logger.info("-" * 50)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.system_trace_logger.error(f"Error in system trace worker: {e}")
|
|
427
|
+
|
|
428
|
+
# Sleep until next collection cycle
|
|
429
|
+
time.sleep(self._system_trace_interval)
|
|
430
|
+
|
|
431
|
+
def _trace_callback(self, frame: FrameType, event: str, arg: Any) -> Callable:
|
|
432
|
+
"""Trace callback function for sys.settrace()."""
|
|
433
|
+
if self._trace_level == 0:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
filename = frame.f_code.co_filename
|
|
438
|
+
lineno = frame.f_lineno
|
|
439
|
+
function = frame.f_code.co_name
|
|
440
|
+
|
|
441
|
+
# Skip if we should not trace this file
|
|
442
|
+
if not self._should_trace(filename):
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
# Log based on trace level and event type
|
|
446
|
+
if event == "call" and self._trace_level >= 1:
|
|
447
|
+
module = os.path.basename(filename)
|
|
448
|
+
if module.endswith(".py"):
|
|
449
|
+
module = module[:-3]
|
|
450
|
+
|
|
451
|
+
# Get function arguments for detailed tracing
|
|
452
|
+
if self._trace_level >= 3:
|
|
453
|
+
args = inspect.getargvalues(frame)
|
|
454
|
+
args_str = []
|
|
455
|
+
for arg_name in args.args:
|
|
456
|
+
if arg_name in args.locals:
|
|
457
|
+
# Safely get string representation with limits
|
|
458
|
+
try:
|
|
459
|
+
arg_val = str(args.locals[arg_name])
|
|
460
|
+
# Truncate long values
|
|
461
|
+
if len(arg_val) > 100:
|
|
462
|
+
arg_val = arg_val[:97] + "..."
|
|
463
|
+
args_str.append(f"{arg_name}={arg_val}")
|
|
464
|
+
except:
|
|
465
|
+
args_str.append(f"{arg_name}=<error>")
|
|
466
|
+
args_repr = ", ".join(args_str)
|
|
467
|
+
self.trace_logger.debug(
|
|
468
|
+
f"CALL {module}.{function}({args_repr}) at {filename}:{lineno}"
|
|
469
|
+
)
|
|
470
|
+
else:
|
|
471
|
+
self.trace_logger.debug(f"CALL {module}.{function}() at {filename}:{lineno}")
|
|
472
|
+
|
|
473
|
+
elif event == "line" and self._trace_level >= 2:
|
|
474
|
+
# For line-by-line tracing (high volume)
|
|
475
|
+
if self._trace_level >= 3:
|
|
476
|
+
# Include source line in verbose mode
|
|
477
|
+
try:
|
|
478
|
+
with open(filename, "r") as f:
|
|
479
|
+
lines = f.readlines()
|
|
480
|
+
source = (
|
|
481
|
+
lines[lineno - 1].strip()
|
|
482
|
+
if lineno <= len(lines)
|
|
483
|
+
else "<source not available>"
|
|
484
|
+
)
|
|
485
|
+
self.trace_logger.debug(f"LINE {filename}:{lineno} -> {source}")
|
|
486
|
+
except:
|
|
487
|
+
self.trace_logger.debug(f"LINE {filename}:{lineno}")
|
|
488
|
+
else:
|
|
489
|
+
self.trace_logger.debug(f"LINE {filename}:{lineno}")
|
|
490
|
+
|
|
491
|
+
elif event == "return" and self._trace_level >= 2:
|
|
492
|
+
if self._trace_level >= 3:
|
|
493
|
+
# Include return value in verbose mode
|
|
494
|
+
ret_val = str(arg)
|
|
495
|
+
if len(ret_val) > 100:
|
|
496
|
+
ret_val = ret_val[:97] + "..."
|
|
497
|
+
self.trace_logger.debug(f"RETURN from {function} -> {ret_val}")
|
|
498
|
+
else:
|
|
499
|
+
self.trace_logger.debug(f"RETURN from {function}")
|
|
500
|
+
|
|
501
|
+
elif event == "exception" and self._trace_level >= 1:
|
|
502
|
+
exc_type, exc_value, exc_tb = arg
|
|
503
|
+
self.trace_logger.debug(
|
|
504
|
+
f"EXCEPTION in {filename}:{lineno} -> {exc_type.__name__}: {exc_value}"
|
|
505
|
+
)
|
|
506
|
+
if self._trace_level >= 3:
|
|
507
|
+
# Include traceback in verbose mode
|
|
508
|
+
tb_str = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
509
|
+
self.trace_logger.debug(f"Traceback:\n{tb_str}")
|
|
510
|
+
except Exception as e:
|
|
511
|
+
# Never let tracing errors crash the program
|
|
512
|
+
try:
|
|
513
|
+
self.trace_logger.error(f"Error in trace callback: {e}")
|
|
514
|
+
except:
|
|
515
|
+
pass
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
# Continue tracing this thread if tracing is enabled
|
|
519
|
+
return self._trace_callback if self._trace_level > 0 else None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# Singleton instance accessor function
|
|
523
|
+
def get_logger(name="mcli.out"):
|
|
524
|
+
"""
|
|
525
|
+
Get the mcli logger instance.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
name: Optional logger name. Defaults to "mcli.out".
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
A configured Logger instance that logs only to file.
|
|
532
|
+
"""
|
|
533
|
+
return McliLogger.get_logger(name)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def get_system_trace_logger():
|
|
537
|
+
"""
|
|
538
|
+
Get the system trace logger instance.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
The system trace logger for OS-level process tracing.
|
|
542
|
+
"""
|
|
543
|
+
return McliLogger.get_system_trace_logger()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def enable_runtime_tracing(level: int = 1, excluded_modules: Optional[List[str]] = None):
|
|
547
|
+
"""
|
|
548
|
+
Enable Python interpreter runtime tracing.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
level: Tracing detail level (0=off, 1=function calls only, 2=line by line, 3=verbose)
|
|
552
|
+
excluded_modules: List of module prefixes to exclude from tracing
|
|
553
|
+
"""
|
|
554
|
+
McliLogger.enable_runtime_tracing(level, excluded_modules)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def disable_runtime_tracing():
|
|
558
|
+
"""Disable Python interpreter runtime tracing."""
|
|
559
|
+
McliLogger.disable_runtime_tracing()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def enable_system_tracing(level: int = 1, interval: int = 5):
|
|
563
|
+
"""
|
|
564
|
+
Enable OS-level system tracing for process monitoring.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
level: System trace level (0=off, 1=basic info, 2=detailed)
|
|
568
|
+
interval: Monitoring interval in seconds (minimum 1 second)
|
|
569
|
+
"""
|
|
570
|
+
McliLogger.enable_system_tracing(level, interval)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def disable_system_tracing():
|
|
574
|
+
"""Disable OS-level system tracing."""
|
|
575
|
+
McliLogger.disable_system_tracing()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def register_process(pid: int) -> bool:
|
|
579
|
+
"""
|
|
580
|
+
Register a process for monitoring.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
pid: Process ID to monitor
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
True if successfully registered, False otherwise
|
|
587
|
+
"""
|
|
588
|
+
return McliLogger.register_process(pid)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def register_subprocess(proc: subprocess.Popen) -> int:
|
|
592
|
+
"""
|
|
593
|
+
Register a subprocess.Popen object for monitoring.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
proc: A subprocess.Popen object to monitor
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
The process ID if successfully registered, 0 otherwise
|
|
600
|
+
"""
|
|
601
|
+
return McliLogger.register_subprocess(proc)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def unregister_process(pid: int):
|
|
605
|
+
"""
|
|
606
|
+
Remove a process from monitoring.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
pid: Process ID to stop monitoring
|
|
610
|
+
"""
|
|
611
|
+
McliLogger.unregister_process(pid)
|