Coverage for core/src/version_finder/logger.py: 45%
205 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
1"""
2logger.py
3====================================
4Logging configuration for version_finder.
5This module provides a centralized logging configuration.
6"""
7from datetime import datetime
8import os
9import logging
10import sys
11import tempfile
12from typing import Optional, Tuple
13from pathlib import Path
16# Environment variable to override default log directory
17LOG_DIR_ENV_VAR = "VERSION_FINDER_LOG_DIR"
20class ColoredFormatter(logging.Formatter):
21 """Formatter that adds color to console log messages based on their level."""
22 COLOR_CODES = {
23 logging.DEBUG: "\033[36m", # Cyan
24 logging.INFO: "\033[32m", # Green
25 logging.WARNING: "\033[33m", # Yellow
26 logging.ERROR: "\033[31m", # Red
27 logging.CRITICAL: "\033[41m", # Red background
28 }
29 RESET_CODE = "\033[0m"
31 def format(self, record):
32 color = self.COLOR_CODES.get(record.levelno, self.RESET_CODE)
33 message = super().format(record)
34 return f"{color}{message}{self.RESET_CODE}"
37def get_default_log_dir() -> Path:
38 """
39 Get the default log directory based on the operating system.
40 Respects VERSION_FINDER_LOG_DIR environment variable if set.
42 Returns:
43 Path object for the default log directory
44 """
45 # First check environment variable
46 env_log_dir = os.environ.get(LOG_DIR_ENV_VAR)
47 if env_log_dir:
48 return Path(env_log_dir)
50 app_name = "version_finder"
52 try:
53 if sys.platform.startswith('win'):
54 # Windows: %LOCALAPPDATA%\version_finder\Logs
55 base_dir = os.environ.get('LOCALAPPDATA')
56 if not base_dir or not os.path.exists(base_dir):
57 base_dir = os.path.expanduser('~\\AppData\\Local')
58 return Path(base_dir) / app_name / "Logs"
59 elif sys.platform.startswith('darwin'):
60 # macOS: ~/Library/Logs/version_finder
61 return Path.home() / "Library" / "Logs" / app_name
62 else:
63 # Linux/Unix: ~/.local/share/version_finder/logs
64 xdg_data_home = os.environ.get('XDG_DATA_HOME')
65 if xdg_data_home:
66 return Path(xdg_data_home) / app_name / "logs"
67 return Path.home() / ".local" / "share" / app_name / "logs"
68 except Exception:
69 # Fallback to temp directory if something goes wrong
70 return Path(tempfile.gettempdir()) / app_name / "logs"
72def _ensure_log_directory(custom_dir: Optional[Path] = None) -> Tuple[str, bool]:
73 """
74 Ensure the log directory exists and is writable.
75 Falls back to temporary directory if the primary location fails.
77 Args:
78 custom_dir: Optional custom directory path
80 Returns:
81 tuple: (log_dir_path, success)
82 """
83 # Try the primary path first
84 primary_paths = []
86 # Add custom directory if provided
87 if custom_dir:
88 primary_paths.append(Path(custom_dir))
90 # Then try the default path
91 primary_paths.append(get_default_log_dir())
93 # Try each location
94 for log_dir_path in primary_paths:
95 try:
96 # Create the directory if it doesn't exist
97 log_dir_path.mkdir(parents=True, exist_ok=True)
99 # Test if we can write to the directory
100 test_file = log_dir_path / "test_write.tmp"
102 try:
103 test_file.write_text("test")
104 test_file.unlink() # Remove the file
105 return str(log_dir_path), True
106 except Exception:
107 # Can't write to this directory, try the next one
108 continue
109 except Exception:
110 # Can't create this directory, try the next one
111 continue
113 # If all primary paths fail, try using the system temp directory
114 try:
115 temp_dir = Path(tempfile.gettempdir()) / "version_finder" / "logs"
116 temp_dir.mkdir(parents=True, exist_ok=True)
118 # Test if we can write to the temp directory
119 test_file = temp_dir / "test_write.tmp"
120 try:
121 test_file.write_text("test")
122 test_file.unlink() # Remove the file
123 return str(temp_dir), True
124 except Exception:
125 # Even temp directory isn't writable, give up
126 pass
127 except Exception:
128 # Can't create temp directory either
129 pass
131 # None of the paths worked
132 return str(primary_paths[0] if primary_paths else "unknown"), False
134def get_logger(name: str = "version_finder") -> logging.Logger:
135 """
136 Get a logger with the specified name. If the logger already exists, return it.
138 Args:
139 name: The name of the logger
140 verbose: Whether to enable verbose logging
142 Returns:
143 logging.Logger: The configured logger
144 """
146 # Create a new logger
147 logger = logging.getLogger(name)
149 # Only configure the logger if it hasn't been configured yet
150 if not logger.handlers:
151 # Set the base level to DEBUG so handlers can filter from there
152 logger.setLevel(logging.DEBUG)
154 # Configure console handler
155 console_handler = logging.StreamHandler(sys.stdout)
156 console_formatter = ColoredFormatter('%(message)s')
157 console_handler.setFormatter(console_formatter)
158 console_handler.setLevel(logging.INFO)
159 logger.addHandler(console_handler)
161 # Configure file handler
162 log_dir, dir_writable = _ensure_log_directory()
164 if dir_writable:
165 # Primary log file in the standard location
166 log_file_name = f"{name}-{datetime.now().strftime('%Y-%m-%d')}.log"
167 log_file_path = Path(log_dir) / log_file_name
169 try:
170 # Try to write directly to file first as a test
171 log_file_path.write_text(f"Log initialized: {datetime.now().isoformat()}\n")
173 if log_file_path.exists():
174 file_size = log_file_path.stat().st_size
175 else:
176 # Try to diagnose the issue
177 if sys.platform.startswith('win'):
178 print(f"Windows path length: {len(str(log_file_path))} characters")
179 if len(str(log_file_path)) > 260:
180 print("Path exceeds Windows 260 character limit")
182 # Now set up the logging handler
183 file_handler = logging.FileHandler(str(log_file_path))
184 file_handler.setLevel(logging.DEBUG)
185 file_formatter = logging.Formatter('%(asctime)s - %(message)s')
186 file_handler.setFormatter(file_formatter)
187 logger.addHandler(file_handler)
188 logger.debug(f"Log file created at: {str(log_file_path)}")
189 file_handler.flush()
191 except Exception as e:
192 import traceback
193 print(f"Warning: Failed to create log file at {str(log_file_path)}: {str(e)}")
194 print(f"Exception type: {type(e).__name__}")
195 print(f"Exception details: {traceback.format_exc()}")
196 dir_writable = False
197 log_file_path = None
199 if not dir_writable:
200 # Fallback to current directory
201 fallback_dir = Path.cwd()
202 fallback_path = fallback_dir / f"{name}.log"
204 # List directory contents before creating fallback log
205 if fallback_dir.exists():
206 for file_path in fallback_dir.iterdir():
207 if file_path.name.endswith('.log'): # Only show log files to avoid cluttering output
208 file_size = file_path.stat().st_size
209 print(f" - {file_path.name} ({file_size} bytes)")
210 else:
211 print(" Directory does not exist!")
213 try:
214 # Try to write directly to file first as a test
215 fallback_path.write_text(f"Fallback log initialized: {datetime.now().isoformat()}\n")
217 if fallback_path.exists():
218 print(f"Verified fallback log file exists at: {str(fallback_path)}")
219 file_size = fallback_path.stat().st_size
220 print(f"Fallback log file size: {file_size} bytes")
221 else:
222 print(f"ERROR: Fallback file was written but doesn't exist at: {str(fallback_path)}")
224 # Now set up the logging handler
225 file_handler = logging.FileHandler(str(fallback_path))
226 file_handler.setLevel(logging.DEBUG)
227 file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
228 file_handler.setFormatter(file_formatter)
229 logger.addHandler(file_handler)
230 logger.debug(f"Using fallback log file at: {str(fallback_path)}")
231 log_file_path = str(fallback_path)
232 file_handler.flush()
234 except Exception as e:
235 import traceback
236 print(f"Warning: Failed to create fallback log file: {str(e)}")
237 print(f"Exception type: {type(e).__name__}")
238 print(f"Exception details: {traceback.format_exc()}")
239 log_file_path = None
240 # Continue without file logging
242 # Test the log file was created
243 if log_file_path and Path(log_file_path).exists():
244 logger.info(f"Logging to file: {str(log_file_path)}")
245 else:
246 logger.warning("File logging not available")
248 return logger
251def configure_logging(verbose: bool = False, log_file_path: Optional[Path] = None) -> None:
252 """
253 Configure global logging settings.
255 Args:
256 verbose: Whether to enable verbose logging
257 log_file_path: Optional custom log file path
258 """
259 logger = get_logger()
260 logger.setLevel(logging.DEBUG if verbose else logging.INFO)
262 if log_file_path and isinstance(log_file_path, Path):
263 log_dir = str(log_file_path.parent) if log_file_path.parent != Path() else None
264 if log_dir:
265 log_dir, dir_writable = _ensure_log_directory(log_dir)
266 if dir_writable:
267 try:
268 # List directory contents before creating custom log
269 log_dir_path = Path(log_dir)
270 print(f"Contents of {str(log_dir_path)} before creating custom log:")
271 if log_dir_path.exists():
272 for file_path in log_dir_path.iterdir():
273 file_size = file_path.stat().st_size
274 print(f" - {file_path.name} ({file_size} bytes)")
275 else:
276 print(" Directory does not exist!")
278 # Try to write directly to file first as a test
279 log_file_path.write_text(f"Custom log initialized: {datetime.now().isoformat()}\n")
281 if log_file_path.exists():
282 print(f"Verified custom log file exists at: {str(log_file_path)}")
283 file_size = log_file_path.stat().st_size
284 print(f"Custom log file size: {file_size} bytes")
285 else:
286 print(f"ERROR: Custom file was written but doesn't exist at: {str(log_file_path)}")
288 # Now set up the logging handler
289 file_handler = logging.FileHandler(str(log_file_path))
290 file_handler.setLevel(logging.DEBUG)
291 file_formatter = logging.Formatter('%(asctime)s - %(message)s')
292 file_handler.setFormatter(file_formatter)
293 logger.addHandler(file_handler)
294 logger.info(f"Log file created at: {str(log_file_path)}")
295 file_handler.flush()
297 # List directory contents after creating custom log
298 print(f"Contents of {str(log_dir_path)} after creating custom log:")
299 if log_dir_path.exists():
300 for file_path in log_dir_path.iterdir():
301 file_size = file_path.stat().st_size
302 print(f" - {file_path.name} ({file_size} bytes)")
303 else:
304 print(" Directory does not exist!")
305 except Exception as e:
306 import traceback
307 print(f"Warning: Failed to create log file at {str(log_file_path)}: {str(e)}")
308 print(f"Exception type: {type(e).__name__}")
309 print(f"Exception details: {traceback.format_exc()}")
310 else:
311 print(f"Warning: Log directory {str(log_dir)} is not writable")
313def get_current_log_file_path(name: str = "version_finder") -> Optional[str]:
314 """
315 Get the path to the current log file for the given logger name.
317 Args:
318 name: Logger name
320 Returns:
321 Path to the current log file or None if no file handler is found
322 """
323 try:
324 logger = logging.getLogger(name)
326 # Search for file handlers in the logger
327 for handler in logger.handlers:
328 if isinstance(handler, logging.FileHandler):
329 # Return the path of the first file handler found
330 if os.path.exists(handler.baseFilename):
331 return handler.baseFilename
333 # If no file handler exists or the file doesn't exist, try to find a log file in standard locations
334 # First check for current day's log in the default log directory
335 log_dir, dir_writable = _ensure_log_directory()
337 if dir_writable:
338 # Primary log file in the standard location
339 log_file_name = f"{name}-{datetime.now().strftime('%Y-%m-%d')}.log"
340 log_file_path = Path(log_dir) / log_file_name
342 if log_file_path.exists():
343 return str(log_file_path)
345 # If today's log doesn't exist, look for the most recent log file
346 try:
347 log_dir_path = Path(log_dir)
348 if log_dir_path.exists() and log_dir_path.is_dir():
349 log_files = list(log_dir_path.glob(f"{name}-*.log"))
350 if log_files:
351 # Sort by modification time (most recent first)
352 log_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
353 return str(log_files[0])
354 except Exception:
355 pass
357 # Try fallback location
358 fallback_path = Path.cwd() / f"{name}.log"
359 if fallback_path.exists():
360 return str(fallback_path)
362 # Try temp directory
363 temp_path = Path(tempfile.gettempdir()) / "version_finder" / "logs" / f"{name}.log"
364 if temp_path.exists():
365 return str(temp_path)
367 except Exception:
368 # If anything goes wrong, return None
369 pass
371 return None