File size: 8,226 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
"""
Custom Log Formatters for GEPA Optimizer.

Provides formatters for:
- Console output with colors and emoji
- JSON structured logging for production
- Plain text for file logging
"""

import json
import logging
from datetime import datetime
from typing import Any, Dict, Optional


# ANSI color codes for terminal output
class Colors:
    """ANSI color codes for terminal coloring."""
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    
    # Log level colors
    DEBUG = "\033[36m"      # Cyan
    INFO = "\033[32m"       # Green
    WARNING = "\033[33m"    # Yellow
    ERROR = "\033[31m"      # Red
    CRITICAL = "\033[35m"   # Magenta
    
    # Semantic colors
    TIMESTAMP = "\033[90m"  # Gray
    MODULE = "\033[34m"     # Blue
    MESSAGE = "\033[0m"     # Default


# Emoji prefixes for visual log scanning
LEVEL_EMOJI = {
    logging.DEBUG: "🔍",
    logging.INFO: "ℹ️ ",
    logging.WARNING: "⚠️ ",
    logging.ERROR: "❌",
    logging.CRITICAL: "🚨",
}

# Level colors mapping
LEVEL_COLORS = {
    logging.DEBUG: Colors.DEBUG,
    logging.INFO: Colors.INFO,
    logging.WARNING: Colors.WARNING,
    logging.ERROR: Colors.ERROR,
    logging.CRITICAL: Colors.CRITICAL,
}


class GepaFormatter(logging.Formatter):
    """
    Custom formatter for GEPA Optimizer logs.
    
    Features:
    - Optional color output for console
    - Optional emoji prefixes for visual scanning
    - Structured extra fields support
    - Clean, readable format
    
    Example output:
        2024-01-15 10:30:45 | INFO     | ℹ️  gepa_optimizer.core.optimizer | Starting optimization iteration=5
    """
    
    def __init__(
        self,
        fmt: Optional[str] = None,
        datefmt: Optional[str] = None,
        use_colors: bool = True,
        include_emoji: bool = True,
    ):
        """
        Initialize the formatter.
        
        Args:
            fmt: Format string (uses default if not provided)
            datefmt: Date format string
            use_colors: Whether to use ANSI colors
            include_emoji: Whether to include emoji prefixes
        """
        super().__init__(fmt=fmt, datefmt=datefmt)
        self.use_colors = use_colors
        self.include_emoji = include_emoji
    
    def format(self, record: logging.LogRecord) -> str:
        """Format a log record with colors and emoji."""
        # Store original values
        original_msg = record.msg
        original_levelname = record.levelname
        
        try:
            # Add emoji prefix if enabled
            if self.include_emoji:
                emoji = LEVEL_EMOJI.get(record.levelno, "")
                record.levelname = f"{emoji} {record.levelname}"
            
            # Add colors if enabled
            if self.use_colors:
                color = LEVEL_COLORS.get(record.levelno, Colors.RESET)
                record.levelname = f"{color}{record.levelname}{Colors.RESET}"
                record.name = f"{Colors.MODULE}{record.name}{Colors.RESET}"
            
            # Format extra fields if present
            extra_str = self._format_extra(record)
            if extra_str:
                record.msg = f"{record.msg} | {extra_str}"
            
            # Call parent formatter
            formatted = super().format(record)
            
            return formatted
            
        finally:
            # Restore original values
            record.msg = original_msg
            record.levelname = original_levelname
    
    def _format_extra(self, record: logging.LogRecord) -> str:
        """
        Format extra fields from the log record.
        
        Extra fields are passed via the 'extra' parameter to logging calls:
            logger.info("Message", extra={"key": "value"})
        """
        # Standard LogRecord attributes to exclude
        standard_attrs = {
            'name', 'msg', 'args', 'created', 'filename', 'funcName',
            'levelname', 'levelno', 'lineno', 'module', 'msecs',
            'pathname', 'process', 'processName', 'relativeCreated',
            'stack_info', 'exc_info', 'exc_text', 'thread', 'threadName',
            'taskName', 'message'
        }
        
        # Collect extra fields
        extra_fields = {
            k: v for k, v in record.__dict__.items()
            if k not in standard_attrs and not k.startswith('_')
        }
        
        if not extra_fields:
            return ""
        
        # Format as key=value pairs
        parts = []
        for key, value in extra_fields.items():
            if isinstance(value, str):
                parts.append(f"{key}={value}")
            elif isinstance(value, (int, float)):
                parts.append(f"{key}={value}")
            elif isinstance(value, bool):
                parts.append(f"{key}={str(value).lower()}")
            else:
                parts.append(f"{key}={repr(value)}")
        
        return " ".join(parts)


class JsonFormatter(logging.Formatter):
    """
    JSON formatter for structured logging.
    
    Outputs each log record as a single JSON line, suitable for:
    - Log aggregation systems (ELK, Splunk)
    - Cloud logging (CloudWatch, Stackdriver)
    - Log parsing and analysis
    
    Example output:
        {"timestamp": "2024-01-15T10:30:45.123Z", "level": "INFO", "logger": "gepa_optimizer.core", "message": "Starting optimization", "iteration": 5}
    """
    
    def __init__(
        self,
        include_timestamp: bool = True,
        include_location: bool = False,
    ):
        """
        Initialize JSON formatter.
        
        Args:
            include_timestamp: Include ISO timestamp
            include_location: Include file/line information
        """
        super().__init__()
        self.include_timestamp = include_timestamp
        self.include_location = include_location
    
    def format(self, record: logging.LogRecord) -> str:
        """Format record as JSON string."""
        log_dict: Dict[str, Any] = {}
        
        # Timestamp
        if self.include_timestamp:
            log_dict["timestamp"] = datetime.utcfromtimestamp(
                record.created
            ).isoformat() + "Z"
        
        # Core fields
        log_dict["level"] = record.levelname
        log_dict["logger"] = record.name
        log_dict["message"] = record.getMessage()
        
        # Location info
        if self.include_location:
            log_dict["file"] = record.filename
            log_dict["line"] = record.lineno
            log_dict["function"] = record.funcName
        
        # Exception info
        if record.exc_info:
            log_dict["exception"] = self.formatException(record.exc_info)
        
        # Extra fields
        standard_attrs = {
            'name', 'msg', 'args', 'created', 'filename', 'funcName',
            'levelname', 'levelno', 'lineno', 'module', 'msecs',
            'pathname', 'process', 'processName', 'relativeCreated',
            'stack_info', 'exc_info', 'exc_text', 'thread', 'threadName',
            'taskName', 'message'
        }
        
        for key, value in record.__dict__.items():
            if key not in standard_attrs and not key.startswith('_'):
                try:
                    # Ensure value is JSON serializable
                    json.dumps(value)
                    log_dict[key] = value
                except (TypeError, ValueError):
                    log_dict[key] = str(value)
        
        return json.dumps(log_dict, default=str)


class CompactFormatter(logging.Formatter):
    """
    Compact formatter for minimal log output.
    
    Useful for:
    - CI/CD pipelines
    - Reduced log verbosity
    - Quick debugging
    
    Example output:
        10:30:45 INFO optimizer: Starting optimization
    """
    
    def format(self, record: logging.LogRecord) -> str:
        """Format record in compact form."""
        # Short timestamp (time only)
        time_str = datetime.fromtimestamp(record.created).strftime("%H:%M:%S")
        
        # Short module name (last part only)
        short_name = record.name.split(".")[-1]
        
        return f"{time_str} {record.levelname:5s} {short_name}: {record.getMessage()}"