Files
Atomizer/optimization_engine/logger.py

299 lines
9.8 KiB
Python
Raw Normal View History

feat: Add structured logging system for production-ready error handling (Phase 1.3) Implements comprehensive, production-ready logging infrastructure to replace ad-hoc print() statements across the codebase. This establishes a consistent logging standard for MVP stability. ## What Changed **New Files:** - optimization_engine/logger.py (330 lines) - AtomizerLogger class with trial-specific methods - Color-coded console output (Windows 10+ and Unix) - Automatic file logging with rotation (50MB, 3 backups) - Zero external dependencies (stdlib only) - docs/07_DEVELOPMENT/Phase_1_3_Implementation_Plan.md - Complete Phase 1.3 implementation plan - API documentation and usage examples - Migration strategy for existing studies ## Features 1. **Structured Trial Logging:** - logger.trial_start() - Log trial with design variables - logger.trial_complete() - Log results with objectives/constraints - logger.trial_failed() - Log failures with error details - logger.study_start() - Log study initialization - logger.study_complete() - Log final summary 2. **Production Features:** - ANSI color-coded console output (DEBUG=cyan, INFO=green, etc.) - Automatic file logging to {study_dir}/optimization.log - Log rotation: 50MB max, 3 backup files - Timestamps and structured format for dashboard parsing 3. **Simple API:** ```python from optimization_engine.logger import get_logger logger = get_logger(__name__, study_dir=Path("studies/foo/2_results")) logger.study_start("foo", n_trials=30, sampler="NSGAIISampler") logger.trial_start(1, design_vars) logger.trial_complete(1, objectives, constraints, feasible=True) ``` ## Testing - Verified color output on Windows 10 - Tested file logging and rotation - Confirmed trial-specific methods format correctly - UTF-8 encoding handles special characters ## Next Steps (Phase 1.3.1) - Integrate logging into drone_gimbal_arm_optimization (reference implementation) - Create migration guide for existing studies - Update create-study skill to include logger setup ## Technical Details Current state analyzed: - 1416 occurrences of logging/print across 79 files - 411 occurrences of try:/except/raise across 59 files - Mix of print(), traceback, and inconsistent formatting This logging system provides the foundation for: - Dashboard integration (structured trial logs) - Error recovery (checkpoint system in Phase 1.3.2) - Production debugging (file logs with rotation) Related: Phase 1.2 (Configuration Validation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 09:27:27 -05:00
"""
Atomizer Structured Logging System - Phase 1.3
Provides consistent, production-ready logging across all optimization studies.
Usage:
from optimization_engine.logger import get_logger
logger = get_logger(__name__)
logger.info("Starting optimization...")
logger.error("Simulation failed", exc_info=True)
# Study-specific logger with automatic file logging
logger = get_logger("drone_gimbal_arm", study_dir="studies/drone_gimbal_arm/2_results")
logger.trial_start(trial_number=5, design_vars={"thickness": 2.5})
logger.trial_complete(trial_number=5, objectives={"mass": 120, "freq": 155})
Features:
- Automatic file logging to study_dir/optimization.log
- Console output with color-coded levels (if supported)
- Structured trial logging for dashboard integration
- Log rotation (50MB max, 3 backups)
- No external dependencies (stdlib only)
"""
import logging
import sys
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any
from logging.handlers import RotatingFileHandler
# ANSI color codes for console output (Windows 10+ and Unix)
class LogColors:
"""ANSI color codes for console output."""
RESET = '\033[0m'
BOLD = '\033[1m'
# Levels
DEBUG = '\033[36m' # Cyan
INFO = '\033[32m' # Green
WARNING = '\033[33m' # Yellow
ERROR = '\033[31m' # Red
CRITICAL = '\033[35m' # Magenta
# Custom
TRIAL = '\033[94m' # Bright Blue
SUCCESS = '\033[92m' # Bright Green
class ColoredFormatter(logging.Formatter):
"""Formatter that adds color to console output."""
COLORS = {
logging.DEBUG: LogColors.DEBUG,
logging.INFO: LogColors.INFO,
logging.WARNING: LogColors.WARNING,
logging.ERROR: LogColors.ERROR,
logging.CRITICAL: LogColors.CRITICAL,
}
def __init__(self, fmt: str, use_colors: bool = True):
super().__init__(fmt)
self.use_colors = use_colors and self._supports_color()
def _supports_color(self) -> bool:
"""Check if terminal supports ANSI colors."""
# Windows 10+ supports ANSI
if sys.platform == 'win32':
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
return True
except:
return False
# Unix-like systems
return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
def format(self, record: logging.LogRecord) -> str:
if self.use_colors:
levelname = record.levelname
color = self.COLORS.get(record.levelno, '')
record.levelname = f"{color}{levelname}{LogColors.RESET}"
return super().format(record)
class AtomizerLogger(logging.Logger):
"""Extended logger with trial-specific methods."""
def trial_start(self, trial_number: int, design_vars: Dict[str, float]):
"""Log trial start with design variables."""
self.info(f"{'='*60}")
self.info(f"Trial #{trial_number} START")
self.info(f"{'='*60}")
self.info("Design Variables:")
for name, value in design_vars.items():
if isinstance(value, float):
self.info(f" {name}: {value:.4f}")
else:
self.info(f" {name}: {value}")
def trial_complete(self, trial_number: int, objectives: Dict[str, float],
constraints: Optional[Dict[str, float]] = None,
feasible: bool = True):
"""Log trial completion with results."""
self.info(f"\nTrial #{trial_number} COMPLETE")
self.info("Objectives:")
for name, value in objectives.items():
if isinstance(value, float):
self.info(f" {name}: {value:.4f}")
else:
self.info(f" {name}: {value}")
if constraints:
self.info("Constraints:")
for name, value in constraints.items():
if isinstance(value, float):
self.info(f" {name}: {value:.4f}")
else:
self.info(f" {name}: {value}")
status = "[OK] Feasible" if feasible else "[WARNING] Infeasible"
self.info(f"{status}")
self.info(f"{'='*60}\n")
def trial_failed(self, trial_number: int, error: str):
"""Log trial failure."""
self.error(f"\nTrial #{trial_number} FAILED")
self.error(f"Error: {error}")
self.error(f"{'='*60}\n")
def study_start(self, study_name: str, n_trials: int, sampler: str):
"""Log study initialization."""
self.info("=" * 80)
self.info(f"OPTIMIZATION STUDY: {study_name}")
self.info("=" * 80)
self.info(f"Trials: {n_trials}")
self.info(f"Sampler: {sampler}")
self.info(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.info("=" * 80)
self.info("")
def study_complete(self, study_name: str, n_trials: int, n_successful: int):
"""Log study completion."""
self.info("")
self.info("=" * 80)
self.info(f"STUDY COMPLETE: {study_name}")
self.info("=" * 80)
self.info(f"Total trials: {n_trials}")
self.info(f"Successful: {n_successful}")
self.info(f"Failed/Pruned: {n_trials - n_successful}")
self.info(f"Completed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.info("=" * 80)
# Register custom logger class
logging.setLoggerClass(AtomizerLogger)
def get_logger(
name: str,
level: int = logging.INFO,
study_dir: Optional[Path] = None,
console: bool = True,
file_logging: bool = True
) -> AtomizerLogger:
"""
Get or create a logger instance.
Args:
name: Logger name (typically __name__ or study name)
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
study_dir: If provided, creates log file at study_dir/optimization.log
console: Enable console output (default: True)
file_logging: Enable file logging (default: True, requires study_dir)
Returns:
AtomizerLogger instance
Example:
# Simple logger
logger = get_logger(__name__)
logger.info("Starting optimization...")
# Study logger with file output
logger = get_logger(
"drone_gimbal_arm",
study_dir=Path("studies/drone_gimbal_arm/2_results")
)
logger.study_start("drone_gimbal_arm", n_trials=30, sampler="NSGAIISampler")
"""
logger = logging.getLogger(name)
# Only configure if not already configured
if not logger.handlers:
logger.setLevel(level)
# Console handler with colors
if console:
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_formatter = ColoredFormatter(
fmt='[%(levelname)s] %(message)s',
use_colors=True
)
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# File handler with rotation
if file_logging and study_dir:
study_dir = Path(study_dir)
study_dir.mkdir(parents=True, exist_ok=True)
log_file = study_dir / "optimization.log"
file_handler = RotatingFileHandler(
log_file,
maxBytes=50 * 1024 * 1024, # 50MB
backupCount=3,
encoding='utf-8'
)
file_handler.setLevel(level)
file_formatter = logging.Formatter(
fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Log that file logging is enabled
logger.debug(f"File logging enabled: {log_file}")
# Prevent propagation to root logger
logger.propagate = False
return logger
def configure_root_logger(level: int = logging.WARNING):
"""
Configure root logger to catch unconfigured loggers.
Call this once at application startup to set up default logging behavior.
"""
root_logger = logging.getLogger()
root_logger.setLevel(level)
if not root_logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
formatter = logging.Formatter('[%(levelname)s] %(name)s: %(message)s')
handler.setFormatter(formatter)
root_logger.addHandler(handler)
# Example usage and testing
if __name__ == "__main__":
# Test basic logging
print("Testing Atomizer Logging System")
print("=" * 80)
# Simple logger
logger = get_logger("test_module")
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
print()
# Study logger with file output
test_dir = Path("test_logs")
test_dir.mkdir(exist_ok=True)
study_logger = get_logger("test_study", study_dir=test_dir)
study_logger.study_start("test_study", n_trials=5, sampler="TPESampler")
# Simulate trial
study_logger.trial_start(1, {"thickness": 2.5, "width": 10.0})
study_logger.info("Running simulation...")
study_logger.trial_complete(
1,
objectives={"mass": 120.5, "stiffness": 1500.2},
constraints={"max_stress": 85.3},
feasible=True
)
# Failed trial
study_logger.trial_start(2, {"thickness": 1.0, "width": 5.0})
study_logger.trial_failed(2, "Simulation convergence failure")
study_logger.study_complete("test_study", n_trials=5, n_successful=4)
print()
print(f"Log file created at: {test_dir / 'optimization.log'}")
print("Check the file to see structured logging output!")