File size: 8,097 Bytes
cacd4d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
"""
Core Logger Factory and Configuration.

This module provides the centralized logger factory that should be used
across all GEPA Optimizer modules. It ensures consistent logging behavior
and formatting throughout the application.

Design Principles:
- Single source of truth for logger configuration
- Lazy initialization (loggers created on first use)
- Thread-safe logger access
- Configurable log levels per module
"""

import logging
import sys
from enum import Enum
from typing import Optional, Dict, Any
from functools import lru_cache

from .formatters import GepaFormatter

# Root logger name for GEPA Optimizer
GEPA_LOGGER_NAME = "gepa_optimizer"

# Default log format
DEFAULT_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


class LogLevel(str, Enum):
    """Supported log levels with string representation."""
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"
    
    @classmethod
    def from_string(cls, level: str) -> "LogLevel":
        """Convert string to LogLevel enum."""
        try:
            return cls(level.upper())
        except ValueError:
            return cls.INFO


class LoggerConfig:
    """
    Configuration class for GEPA logging.
    
    This class holds all logging configuration and can be modified
    before calling configure_logging() to customize behavior.
    """
    
    # Default configuration
    level: LogLevel = LogLevel.INFO
    format: str = DEFAULT_FORMAT
    date_format: str = DEFAULT_DATE_FORMAT
    
    # Module-specific log levels (for fine-grained control)
    module_levels: Dict[str, LogLevel] = {}
    
    # Output configuration
    log_to_console: bool = True
    log_to_file: Optional[str] = None
    
    # Formatting options
    use_colors: bool = True
    include_emoji: bool = True  # For visual clarity in development
    
    @classmethod
    def reset(cls) -> None:
        """Reset configuration to defaults."""
        cls.level = LogLevel.INFO
        cls.format = DEFAULT_FORMAT
        cls.date_format = DEFAULT_DATE_FORMAT
        cls.module_levels = {}
        cls.log_to_console = True
        cls.log_to_file = None
        cls.use_colors = True
        cls.include_emoji = True


# Global flag to track if logging is configured
_logging_configured = False


def configure_logging(
    level: Optional[str] = None,
    log_file: Optional[str] = None,
    use_colors: bool = True,
    include_emoji: bool = True,
    format_string: Optional[str] = None,
    module_levels: Optional[Dict[str, str]] = None,
) -> None:
    """
    Configure the GEPA logging system.
    
    This should be called once at application startup. Subsequent calls
    will update the configuration.
    
    Args:
        level: Global log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
        log_file: Optional path to log file
        use_colors: Whether to use colored output in console
        include_emoji: Whether to include emoji prefixes for visual clarity
        format_string: Custom format string (optional)
        module_levels: Dict mapping module names to their specific log levels
        
    Example:
        configure_logging(
            level="DEBUG",
            log_file="optimization.log",
            module_levels={
                "gepa_optimizer.core.optimizer": "INFO",
                "gepa_optimizer.llms": "DEBUG"
            }
        )
    """
    global _logging_configured
    
    # Update configuration
    if level:
        LoggerConfig.level = LogLevel.from_string(level)
    if log_file:
        LoggerConfig.log_to_file = log_file
    LoggerConfig.use_colors = use_colors
    LoggerConfig.include_emoji = include_emoji
    if format_string:
        LoggerConfig.format = format_string
    if module_levels:
        LoggerConfig.module_levels = {
            k: LogLevel.from_string(v) for k, v in module_levels.items()
        }
    
    # Get or create root GEPA logger
    root_logger = logging.getLogger(GEPA_LOGGER_NAME)
    root_logger.setLevel(getattr(logging, LoggerConfig.level.value))
    
    # Remove existing handlers to avoid duplicates
    root_logger.handlers.clear()
    
    # Console handler
    if LoggerConfig.log_to_console:
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(getattr(logging, LoggerConfig.level.value))
        
        # Use custom formatter
        formatter = GepaFormatter(
            fmt=LoggerConfig.format,
            datefmt=LoggerConfig.date_format,
            use_colors=use_colors,
            include_emoji=include_emoji,
        )
        console_handler.setFormatter(formatter)
        root_logger.addHandler(console_handler)
    
    # File handler (if configured)
    if LoggerConfig.log_to_file:
        file_handler = logging.FileHandler(LoggerConfig.log_to_file)
        file_handler.setLevel(getattr(logging, LoggerConfig.level.value))
        
        # File logs don't use colors
        file_formatter = GepaFormatter(
            fmt=LoggerConfig.format,
            datefmt=LoggerConfig.date_format,
            use_colors=False,
            include_emoji=False,
        )
        file_handler.setFormatter(file_formatter)
        root_logger.addHandler(file_handler)
    
    # Apply module-specific levels
    for module_name, module_level in LoggerConfig.module_levels.items():
        module_logger = logging.getLogger(module_name)
        module_logger.setLevel(getattr(logging, module_level.value))
    
    _logging_configured = True
    
    # Log that configuration is complete
    root_logger.debug(
        f"Logging configured: level={LoggerConfig.level.value}, "
        f"file={LoggerConfig.log_to_file}"
    )


@lru_cache(maxsize=128)
def get_logger(name: str) -> logging.Logger:
    """
    Get a logger instance for the given module name.
    
    This is the primary factory function for obtaining loggers.
    All GEPA modules should use this instead of logging.getLogger().
    
    Args:
        name: Module name (typically __name__)
        
    Returns:
        Configured Logger instance
        
    Example:
        from gepa_optimizer.infrastructure.logging import get_logger
        
        logger = get_logger(__name__)
        logger.info("Starting process")
        logger.error("Failed to connect", exc_info=True)
    """
    global _logging_configured
    
    # Auto-configure with defaults if not yet configured
    if not _logging_configured:
        configure_logging()
    
    # Ensure name is under GEPA namespace for consistent handling
    if not name.startswith(GEPA_LOGGER_NAME) and name != GEPA_LOGGER_NAME:
        # External module - still use our formatting
        pass
    
    logger = logging.getLogger(name)
    
    # Apply module-specific level if configured
    if name in LoggerConfig.module_levels:
        logger.setLevel(getattr(logging, LoggerConfig.module_levels[name].value))
    
    return logger


def set_log_level(level: str, module: Optional[str] = None) -> None:
    """
    Dynamically change log level at runtime.
    
    Args:
        level: New log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
        module: Optional module name. If None, changes global level.
        
    Example:
        # Enable debug for specific module
        set_log_level("DEBUG", "gepa_optimizer.core.optimizer")
        
        # Change global level
        set_log_level("WARNING")
    """
    log_level = LogLevel.from_string(level)
    
    if module:
        # Set level for specific module
        logger = logging.getLogger(module)
        logger.setLevel(getattr(logging, log_level.value))
        LoggerConfig.module_levels[module] = log_level
    else:
        # Set global level
        LoggerConfig.level = log_level
        root_logger = logging.getLogger(GEPA_LOGGER_NAME)
        root_logger.setLevel(getattr(logging, log_level.value))
        
        # Update all handlers
        for handler in root_logger.handlers:
            handler.setLevel(getattr(logging, log_level.value))