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/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