summaryrefslogtreecommitdiff
path: root/src/logger.py
blob: 792346540d01ce249153dc537f0048542d68d5bc (plain)
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
"""
Centralized logging module for the experiment framework.
Provides configurable debug levels:
  - Level 1: CRITICAL only
  - Level 2: WARNING + CRITICAL
  - Level 3: INFO + WARNING + CRITICAL
  - Level 4: DEBUG + INFO + WARNING + CRITICAL (full API calls, reasoning, VFS)
"""
import logging
import os
import sys
from typing import Optional

# Custom level names
LEVEL_NAMES = {
    logging.CRITICAL: "CRITICAL",
    logging.WARNING: "WARN",
    logging.INFO: "INFO",
    logging.DEBUG: "DEBUG",
}

# Define DEBUG level (10) below INFO (20)
DEBUG_LEVEL = logging.DEBUG  # 10


def setup_logger(
    name: str = "experiment",
    level: int = 3,
    log_format: str = "[{level}] {message}",
    output: str = "console",
    log_file: Optional[str] = None
) -> logging.Logger:
    """
    Set up and configure a logger.

    Args:
        name: Logger name
        level: Debug level (1-4)
        log_format: Format string for log messages
        output: Output destination - "console", "file", or "both"
        log_file: Path to log file (required if output is "file" or "both")

    Returns:
        Configured logger instance
    """
    logger = logging.getLogger(name)

    # Clear any existing handlers
    logger.handlers.clear()

    # Determine actual logging level based on debug level
    # Level 1: CRITICAL only (50)
    # Level 2: WARNING (30) + CRITICAL (50)
    # Level 3: INFO (20) + WARNING (30) + CRITICAL (50)
    # Level 4: DEBUG (10) + INFO + WARNING + CRITICAL (full API calls, reasoning, VFS)
    if level >= 4:
        actual_level = logging.DEBUG
    elif level >= 3:
        actual_level = logging.INFO
    elif level >= 2:
        actual_level = logging.WARNING
    else:
        actual_level = logging.CRITICAL

    logger.setLevel(actual_level)

    # Create formatter
    class CustomFormatter(logging.Formatter):
        """Custom formatter that uses our format string."""

        def format(self, record):
            # Use our custom level names
            levelname = record.levelname
            if record.levelno == logging.CRITICAL:
                levelname = "CRITICAL"
            elif record.levelno == logging.WARNING:
                levelname = "WARN"
            elif record.levelno == logging.INFO:
                levelname = "INFO"
            elif record.levelno == logging.DEBUG:
                levelname = "DEBUG"

            msg = record.getMessage()
            return log_format.format(level=levelname, message=msg)

    formatter = CustomFormatter()

    # For level 4 (DEBUG), automatically enable both console and file output
    if level >= 4 and output == "console":
        output = "both"
        if not log_file:
            log_file = "logs/experiment.log"

    # Console handler
    if output in ("console", "both"):
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setFormatter(formatter)
        logger.addHandler(console_handler)

    # File handler
    if output in ("file", "both"):
        if not log_file:
            raise ValueError("log_file must be specified when output is 'file' or 'both'")

        # Ensure directory exists
        log_dir = os.path.dirname(log_file)
        if log_dir:
            os.makedirs(log_dir, exist_ok=True)

        file_handler = logging.FileHandler(log_file)
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)

    return logger


def get_logger(name: str = "experiment") -> logging.Logger:
    """
    Get an existing logger by name.

    Args:
        name: Logger name

    Returns:
        Logger instance
    """
    return logging.getLogger(name)