File size: 7,994 Bytes
0a372e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""
Centralized logging configuration for the Executive Education RAG Chatbot.
"""
from threading import Lock
import logging, os, sys, warnings, copy
from pathlib import Path
from typing import Optional

import colorama
from colorama import Fore, Style

# Initialize colorama for cross-platform color support
colorama.init()

class CodeBlockFormatter(logging.Formatter):
    ALIASES = {
        'DEBUG':    'DEBUG',
        'INFO':     'INFO ',
        'WARNING':  'WARN ',
        'ERROR':    'ERROR',
        'CRITICAL': 'CRITC'
    }

    def format(self, record):
        if hasattr(record, 'levelname') and record.levelname in self.ALIASES:
            record.levelname = self.ALIASES[record.levelname]

        if hasattr(record, 'name'):
            rname = record.name 
            if len(rname) <= 17: rname += ' '*(17-len(rname))
            else: rname = rname[14:] + '...'
            record.name = rname
        
        return super().format(record)


class ColoredFormatter(logging.Formatter):
    """Custom formatter with color support for console output."""
    
    COLORS = {
        'DEBUG':    Fore.CYAN,
        'INFO':     Fore.GREEN,
        'WARNING':  Fore.YELLOW,
        'ERROR':    Fore.RED,
        'CRITICAL': Fore.MAGENTA + Style.BRIGHT,
    }
    ALIASES = {
        'DEBUG':    'DEBUG',
        'INFO':     'INFO ',
        'WARNING':  'WARN ',
        'ERROR':    'ERROR',
        'CRITICAL': 'CRITC'
    }
    
    def format(self, record):
        # Add color to the level name
        if hasattr(record, 'levelname') and record.levelname in self.COLORS:
            lname = record.levelname
            if hasattr(record, 'message') and lname == 'ERROR':
                record.message = f"{self.COLORS[lname]}{record.message}{Style.RESET_ALL}"

            record.levelname = f"{self.COLORS[lname]}{self.ALIASES[lname]}{Style.RESET_ALL}"
            

        if hasattr(record, 'name'):
            rname = record.name if len(record.name) <= 17 else record.name[:14] + '...'
            record.name = f"{Fore.CYAN}{rname}{Style.RESET_ALL}"

        return super().format(record)


class CachedLogHandler(logging.Handler):
    def __init__(self, level = 0) -> None:
        super().__init__(level)
        self._cache = []
        self._max_lines = 50
        self._lock = Lock()


    def emit(self, record):
        record_copy = copy.copy(record)
        msg = self.format(record_copy)
        with self._lock:
            self._cache.append(msg)

    def get_logs(self):
        with self._lock:
            return '\n'.join(self._cache)


cached_log_handler: CachedLogHandler = CachedLogHandler()

def setup_logging(
    level: str = "INFO",
    log_file: Optional[str] = None,
    interactive_mode: bool = False,
    module_name: Optional[str] = None
) -> logging.Logger:
    """
    Set up centralized logging configuration.
    
    Args:
        level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
        log_file: Optional path to log file. If None, uses default location.
        interactive_mode: If True, logs only to file in interactive mode
        module_name: Name of the module requesting the logger
    
    Returns:
        Configured logger instance
    """
    global cached_log_handler

    # Convert string level to logging constant
    numeric_level = getattr(logging, level.upper(), logging.INFO)
    
    # Create logger
    logger = logging.getLogger()
    
    # Avoid duplicate handlers if logger already configured
    if logger.handlers:
        logger.handlers.clear()
    
    logger.setLevel(numeric_level)
    
    # Create formatters
    detailed_formatter = logging.Formatter(
        "(%(asctime)s) %(name)s\t %(levelname)s: %(message)s",
        datefmt="%Y.%m.%d %H:%M:%S"
    )
    
    colored_formatter = ColoredFormatter(
        "(%(asctime)s) %(name)s\t %(levelname)s: %(message)s",
        datefmt="%Y.%m.%d %H:%M:%S"
    )

    codeblock_formatter = CodeBlockFormatter(
       "%(levelname)s %(name)s\t : %(message)s", 
    )
    
    # Set up file logging
    if log_file or interactive_mode:
        if not log_file:
            # Default log file location
            log_dir = Path("logs")
            log_dir.mkdir(exist_ok=True)
            log_file = log_dir / "rag_chatbot.log"
        
        file_handler = logging.FileHandler(log_file, encoding='utf-8')
        file_handler.setLevel(numeric_level)
        file_handler.setFormatter(detailed_formatter)
        logger.addHandler(file_handler)
    
    # Set up the cached log handler for gradio Code block output
    cached_log_handler.setLevel(numeric_level)
    cached_log_handler.setFormatter(codeblock_formatter)

    # Set up console logging (unless in interactive mode)
    if not interactive_mode:
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(numeric_level)
        
        # Use colored formatter if terminal supports it
        if _supports_color():
            console_handler.setFormatter(colored_formatter)
        else:
            console_handler.setFormatter(detailed_formatter)
        
        logger.addHandler(cached_log_handler)
        logger.addHandler(console_handler)
    
    return logger


def get_logger(module_name: str) -> logging.Logger:
    """
    Get a logger for a specific module.
    
    Args:
        module_name: Name of the module requesting the logger
    
    Returns:
        Logger instance
    """
    logger = logging.getLogger(module_name)
    logger.propagate = True       
    return logger


def _supports_color() -> bool:
    """
    Check if the terminal supports color output.
    
    Returns:
        True if color is supported, False otherwise
    """
    # Check if we're in a terminal
    if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty():
        return False
    
    # Check environment variables
    if os.getenv('NO_COLOR'):
        return False
    
    if os.getenv('FORCE_COLOR'):
        return True
    
    # Check terminal type
    term = os.getenv('TERM', '').lower()
    if 'color' in term or term in ['xterm', 'xterm-256color', 'screen']:
        return True
    
    return False


def detect_interactive_mode() -> bool:
    """
    Detect if the application is running in interactive mode.
    
    Returns:
        True if in interactive mode, False otherwise
    """
    # Check if no command line arguments were provided (except script name)
    if len(sys.argv) == 1:
        return True
    
    # Check if only the script name and no other arguments
    # This indicates the chatbot is running in default interactive mode
    return False


def configure_external_loggers(level: str = "WARNING") -> None:
    """
    Configure logging for external libraries to reduce noise.
    
    Args:
        level: Logging level for external libraries
    """
    external_loggers = [
        'selenium',
        'urllib3',
        'requests',
        'chromadb',
        'docling',
        'weaviate',
        'langchain',
        'langgraph',
        'openai',
        'httpx'
    ]
    
    numeric_level = getattr(logging, level.upper(), logging.WARNING)
    
    for logger_name in external_loggers:
        logging.getLogger(logger_name).setLevel(numeric_level)


# Global configuration function
def init_logging(
    level: str = "INFO",
    log_file: Optional[str] = None,
    interactive_mode: Optional[bool] = None
) -> None:
    """
    Initialize the global logging configuration.
    
    Args:
        level: Logging level
        log_file: Optional log file path
        interactive_mode: If None, auto-detect interactive mode
    """
    if interactive_mode is None:
        interactive_mode = detect_interactive_mode()
    
    warnings.filterwarnings("ignore")

    # Set up root logger
    setup_logging(
        level=level,
        log_file=log_file,
        interactive_mode=interactive_mode
    )
    
    # Configure external library loggers
    configure_external_loggers()