ic-code 1.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.
- ic/__init__.py +29 -0
- ic/cli.py +769 -0
- ic/commands/__init__.py +10 -0
- ic/commands/config.py +699 -0
- ic/compat/__init__.py +241 -0
- ic/compat/cli.py +289 -0
- ic/compat/common.py +243 -0
- ic/config/__init__.py +10 -0
- ic/config/cleanup.py +382 -0
- ic/config/docs_organizer.py +587 -0
- ic/config/external.py +456 -0
- ic/config/manager.py +898 -0
- ic/config/migration.py +628 -0
- ic/config/schema.py +595 -0
- ic/config/secrets.py +437 -0
- ic/config/security.py +462 -0
- ic/core/__init__.py +16 -0
- ic/core/logging.py +311 -0
- ic/core/mcp_manager.py +856 -0
- ic/core/session.py +392 -0
- ic/core/silence_logging.py +67 -0
- ic_code-1.0.0.dist-info/METADATA +354 -0
- ic_code-1.0.0.dist-info/RECORD +27 -0
- ic_code-1.0.0.dist-info/WHEEL +5 -0
- ic_code-1.0.0.dist-info/entry_points.txt +2 -0
- ic_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- ic_code-1.0.0.dist-info/top_level.txt +1 -0
ic/core/logging.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced logging system with security-aware dual-level logging.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Console logging for ERROR/CRITICAL only
|
|
6
|
+
- Comprehensive file logging with rotation
|
|
7
|
+
- Sensitive data masking in all log outputs
|
|
8
|
+
- Rich console formatting
|
|
9
|
+
- Structured log file management
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import logging.handlers
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, Optional, Union
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.logging import RichHandler
|
|
23
|
+
RICH_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
RICH_AVAILABLE = False
|
|
26
|
+
|
|
27
|
+
from ..config.security import SecurityManager
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ICLogger:
|
|
31
|
+
"""Enhanced logger with dual-level logging and security features."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the IC logger with configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Configuration dictionary with logging settings
|
|
39
|
+
"""
|
|
40
|
+
self.config = config or {}
|
|
41
|
+
self.logging_config = self.config.get('logging', {})
|
|
42
|
+
|
|
43
|
+
# Initialize security manager for sensitive data masking
|
|
44
|
+
self.security_manager = SecurityManager(self.config)
|
|
45
|
+
|
|
46
|
+
# Console and file log levels
|
|
47
|
+
self.console_level = getattr(logging, self.logging_config.get('console_level', 'ERROR'))
|
|
48
|
+
self.file_level = getattr(logging, self.logging_config.get('file_level', 'INFO'))
|
|
49
|
+
|
|
50
|
+
# Log file configuration
|
|
51
|
+
self.log_file_path = self._get_log_file_path()
|
|
52
|
+
self.max_files = self.logging_config.get('max_files', 30)
|
|
53
|
+
self.log_format = self.logging_config.get('format',
|
|
54
|
+
'%(asctime)s [%(levelname)s] - %(message)s')
|
|
55
|
+
|
|
56
|
+
# Initialize console for Rich output
|
|
57
|
+
self.console = Console() if RICH_AVAILABLE else None
|
|
58
|
+
|
|
59
|
+
# Setup loggers
|
|
60
|
+
self.logger = self._setup_logger()
|
|
61
|
+
|
|
62
|
+
# Set global logging level to suppress console output for non-ERROR messages
|
|
63
|
+
root_logger = logging.getLogger()
|
|
64
|
+
root_logger.setLevel(logging.DEBUG) # Allow all levels for file logging
|
|
65
|
+
|
|
66
|
+
# Remove any existing console handlers from root logger
|
|
67
|
+
for handler in root_logger.handlers[:]:
|
|
68
|
+
if isinstance(handler, (logging.StreamHandler, RichHandler if RICH_AVAILABLE else type(None))):
|
|
69
|
+
root_logger.removeHandler(handler)
|
|
70
|
+
|
|
71
|
+
def _get_log_file_path(self) -> str:
|
|
72
|
+
"""Generate log file path with date using fixed path logic."""
|
|
73
|
+
log_path_template = self.logging_config.get('file_path', '~/.ic/logs/ic_{date}.log')
|
|
74
|
+
date_str = datetime.now().strftime('%Y%m%d')
|
|
75
|
+
log_path = log_path_template.format(date=date_str)
|
|
76
|
+
|
|
77
|
+
# Expand user path and resolve
|
|
78
|
+
log_path = Path(log_path).expanduser().resolve()
|
|
79
|
+
|
|
80
|
+
# Ensure log directory exists with fallback logic
|
|
81
|
+
log_dir = log_path.parent
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
except (PermissionError, OSError) as e:
|
|
86
|
+
# Fallback to temp directory if home directory is not writable
|
|
87
|
+
fallback_dir = Path("/tmp/ic/logs")
|
|
88
|
+
try:
|
|
89
|
+
fallback_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
log_path = fallback_dir / f"ic_{date_str}.log"
|
|
91
|
+
print(f"Warning: Using fallback log path {log_path} due to: {e}")
|
|
92
|
+
except Exception as fallback_error:
|
|
93
|
+
# Last resort: current directory
|
|
94
|
+
log_path = Path(f"ic_{date_str}.log")
|
|
95
|
+
print(f"Warning: Using current directory for logs due to: {fallback_error}")
|
|
96
|
+
|
|
97
|
+
return str(log_path)
|
|
98
|
+
|
|
99
|
+
def _setup_logger(self) -> logging.Logger:
|
|
100
|
+
"""Setup dual-level logger with console and file handlers."""
|
|
101
|
+
logger = logging.getLogger('ic')
|
|
102
|
+
logger.setLevel(logging.DEBUG) # Set to lowest level, handlers will filter
|
|
103
|
+
|
|
104
|
+
# Clear existing handlers to avoid duplicates
|
|
105
|
+
logger.handlers.clear()
|
|
106
|
+
|
|
107
|
+
# Console handler - ERROR and CRITICAL only
|
|
108
|
+
if RICH_AVAILABLE:
|
|
109
|
+
console_handler = RichHandler(
|
|
110
|
+
console=self.console,
|
|
111
|
+
show_time=False,
|
|
112
|
+
show_path=False,
|
|
113
|
+
rich_tracebacks=True
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
console_handler = logging.StreamHandler()
|
|
117
|
+
|
|
118
|
+
console_handler.setLevel(self.console_level)
|
|
119
|
+
console_formatter = logging.Formatter('%(message)s')
|
|
120
|
+
console_handler.setFormatter(console_formatter)
|
|
121
|
+
|
|
122
|
+
# Add filter to suppress non-ERROR messages on console
|
|
123
|
+
def error_only_filter(record):
|
|
124
|
+
return record.levelno >= logging.ERROR
|
|
125
|
+
|
|
126
|
+
console_handler.addFilter(error_only_filter)
|
|
127
|
+
logger.addHandler(console_handler)
|
|
128
|
+
|
|
129
|
+
# File handler - comprehensive logging with rotation
|
|
130
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
131
|
+
filename=self.log_file_path,
|
|
132
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
133
|
+
backupCount=self.max_files,
|
|
134
|
+
encoding='utf-8'
|
|
135
|
+
)
|
|
136
|
+
file_handler.setLevel(self.file_level)
|
|
137
|
+
file_formatter = logging.Formatter(self.log_format)
|
|
138
|
+
file_handler.setFormatter(file_formatter)
|
|
139
|
+
logger.addHandler(file_handler)
|
|
140
|
+
|
|
141
|
+
return logger
|
|
142
|
+
|
|
143
|
+
def _mask_message(self, message: str) -> str:
|
|
144
|
+
"""Mask sensitive data in log messages."""
|
|
145
|
+
if not self.logging_config.get('mask_sensitive', True):
|
|
146
|
+
return message
|
|
147
|
+
|
|
148
|
+
# Use security manager to mask sensitive data
|
|
149
|
+
return self.security_manager.mask_sensitive_in_text(message)
|
|
150
|
+
|
|
151
|
+
def log_args(self, args: Union[Dict[str, Any], object]) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Display command arguments on console and log to file.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
args: Arguments dictionary or argparse Namespace object
|
|
157
|
+
"""
|
|
158
|
+
# Convert args to dictionary if it's an object
|
|
159
|
+
if hasattr(args, '__dict__'):
|
|
160
|
+
args_dict = {k: v for k, v in vars(args).items()
|
|
161
|
+
if not k.startswith('_') and k != 'func'}
|
|
162
|
+
else:
|
|
163
|
+
args_dict = dict(args) if isinstance(args, dict) else {}
|
|
164
|
+
|
|
165
|
+
# Format arguments for display
|
|
166
|
+
pretty_args = {k: (v if v is not None else "default")
|
|
167
|
+
for k, v in args_dict.items()}
|
|
168
|
+
args_str = ", ".join(f"{k}={v}" for k, v in pretty_args.items())
|
|
169
|
+
|
|
170
|
+
# Console output for args (always shown regardless of log level)
|
|
171
|
+
if self.console and RICH_AVAILABLE:
|
|
172
|
+
self.console.print(f"[bold cyan]Args:[/bold cyan] {args_str}")
|
|
173
|
+
else:
|
|
174
|
+
print(f"Args: {args_str}")
|
|
175
|
+
|
|
176
|
+
# File logging with masking
|
|
177
|
+
masked_args_str = self._mask_message(f"Args: {args_str}")
|
|
178
|
+
self.logger.info(masked_args_str)
|
|
179
|
+
|
|
180
|
+
def log_info_file_only(self, message: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Log INFO message to file only (not console).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
message: Message to log to file
|
|
186
|
+
"""
|
|
187
|
+
masked_message = self._mask_message(message)
|
|
188
|
+
self.logger.info(masked_message)
|
|
189
|
+
|
|
190
|
+
def log_error(self, message: str) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Log ERROR message to both console and file.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
message: Error message to log
|
|
196
|
+
"""
|
|
197
|
+
masked_message = self._mask_message(message)
|
|
198
|
+
self.logger.error(masked_message)
|
|
199
|
+
|
|
200
|
+
# Also display on console with Rich formatting if available
|
|
201
|
+
if self.console and RICH_AVAILABLE:
|
|
202
|
+
self.console.print(f"[bold red]ERROR:[/bold red] {message}")
|
|
203
|
+
else:
|
|
204
|
+
print(f"ERROR: {message}")
|
|
205
|
+
|
|
206
|
+
def log_critical(self, message: str) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Log CRITICAL message to both console and file.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
message: Critical message to log
|
|
212
|
+
"""
|
|
213
|
+
masked_message = self._mask_message(message)
|
|
214
|
+
self.logger.critical(masked_message)
|
|
215
|
+
|
|
216
|
+
# Also display on console with Rich formatting if available
|
|
217
|
+
if self.console and RICH_AVAILABLE:
|
|
218
|
+
self.console.print(f"[bold red]CRITICAL:[/bold red] {message}")
|
|
219
|
+
else:
|
|
220
|
+
print(f"CRITICAL: {message}")
|
|
221
|
+
|
|
222
|
+
def log_warning(self, message: str) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Log WARNING message to file only.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
message: Warning message to log
|
|
228
|
+
"""
|
|
229
|
+
masked_message = self._mask_message(message)
|
|
230
|
+
self.logger.warning(masked_message)
|
|
231
|
+
|
|
232
|
+
def log_debug(self, message: str) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Log DEBUG message to file only.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
message: Debug message to log
|
|
238
|
+
"""
|
|
239
|
+
masked_message = self._mask_message(message)
|
|
240
|
+
self.logger.debug(masked_message)
|
|
241
|
+
|
|
242
|
+
def cleanup_old_logs(self) -> None:
|
|
243
|
+
"""Clean up old log files beyond the retention limit."""
|
|
244
|
+
try:
|
|
245
|
+
log_dir = Path(self.log_file_path).parent
|
|
246
|
+
if not log_dir.exists():
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Find all IC log files
|
|
250
|
+
log_files = list(log_dir.glob('ic_*.log*'))
|
|
251
|
+
|
|
252
|
+
# Sort by modification time (oldest first)
|
|
253
|
+
log_files.sort(key=lambda f: f.stat().st_mtime)
|
|
254
|
+
|
|
255
|
+
# Remove files beyond retention limit
|
|
256
|
+
if len(log_files) > self.max_files:
|
|
257
|
+
files_to_remove = log_files[:-self.max_files]
|
|
258
|
+
for log_file in files_to_remove:
|
|
259
|
+
try:
|
|
260
|
+
log_file.unlink()
|
|
261
|
+
self.log_debug(f"Removed old log file: {log_file}")
|
|
262
|
+
except OSError as e:
|
|
263
|
+
self.log_warning(f"Failed to remove log file {log_file}: {e}")
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.log_warning(f"Failed to cleanup old logs: {e}")
|
|
267
|
+
|
|
268
|
+
def get_log_file_path(self) -> str:
|
|
269
|
+
"""Get current log file path."""
|
|
270
|
+
return self.log_file_path
|
|
271
|
+
|
|
272
|
+
def get_logger(self) -> logging.Logger:
|
|
273
|
+
"""Get the underlying logger instance."""
|
|
274
|
+
return self.logger
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Global logger instance
|
|
278
|
+
_global_logger: Optional[ICLogger] = None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_logger(config: Optional[Dict[str, Any]] = None) -> ICLogger:
|
|
282
|
+
"""
|
|
283
|
+
Get or create global logger instance.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
config: Configuration dictionary for logger initialization
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
ICLogger instance
|
|
290
|
+
"""
|
|
291
|
+
global _global_logger
|
|
292
|
+
|
|
293
|
+
if _global_logger is None or config is not None:
|
|
294
|
+
_global_logger = ICLogger(config)
|
|
295
|
+
|
|
296
|
+
return _global_logger
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def init_logger(config: Dict[str, Any]) -> ICLogger:
|
|
300
|
+
"""
|
|
301
|
+
Initialize global logger with configuration.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
config: Configuration dictionary
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Initialized ICLogger instance
|
|
308
|
+
"""
|
|
309
|
+
global _global_logger
|
|
310
|
+
_global_logger = ICLogger(config)
|
|
311
|
+
return _global_logger
|