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

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 

14 

15 

16# Environment variable to override default log directory 

17LOG_DIR_ENV_VAR = "VERSION_FINDER_LOG_DIR" 

18 

19 

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" 

30 

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}" 

35 

36 

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. 

41  

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) 

49 

50 app_name = "version_finder" 

51 

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" 

71 

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. 

76 

77 Args: 

78 custom_dir: Optional custom directory path 

79 

80 Returns: 

81 tuple: (log_dir_path, success) 

82 """ 

83 # Try the primary path first 

84 primary_paths = [] 

85 

86 # Add custom directory if provided 

87 if custom_dir: 

88 primary_paths.append(Path(custom_dir)) 

89 

90 # Then try the default path 

91 primary_paths.append(get_default_log_dir()) 

92 

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) 

98 

99 # Test if we can write to the directory 

100 test_file = log_dir_path / "test_write.tmp" 

101 

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 

112 

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) 

117 

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 

130 

131 # None of the paths worked 

132 return str(primary_paths[0] if primary_paths else "unknown"), False 

133 

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. 

137 

138 Args: 

139 name: The name of the logger 

140 verbose: Whether to enable verbose logging 

141 

142 Returns: 

143 logging.Logger: The configured logger 

144 """ 

145 

146 # Create a new logger 

147 logger = logging.getLogger(name) 

148 

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) 

153 

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) 

160 

161 # Configure file handler 

162 log_dir, dir_writable = _ensure_log_directory() 

163 

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 

168 

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") 

172 

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") 

181 

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() 

190 

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 

198 

199 if not dir_writable: 

200 # Fallback to current directory 

201 fallback_dir = Path.cwd() 

202 fallback_path = fallback_dir / f"{name}.log" 

203 

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!") 

212 

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") 

216 

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)}") 

223 

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() 

233 

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 

241 

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") 

247 

248 return logger 

249 

250 

251def configure_logging(verbose: bool = False, log_file_path: Optional[Path] = None) -> None: 

252 """ 

253 Configure global logging settings. 

254 

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) 

261 

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!") 

277 

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") 

280 

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)}") 

287 

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() 

296 

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") 

312 

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. 

316  

317 Args: 

318 name: Logger name 

319  

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) 

325 

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 

332 

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() 

336 

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 

341 

342 if log_file_path.exists(): 

343 return str(log_file_path) 

344 

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 

356 

357 # Try fallback location 

358 fallback_path = Path.cwd() / f"{name}.log" 

359 if fallback_path.exists(): 

360 return str(fallback_path) 

361 

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) 

366 

367 except Exception: 

368 # If anything goes wrong, return None 

369 pass 

370 

371 return None