1919"""
2020Module providing a customized logging configuration for the ContextGem framework.
2121
22- This module configures a Loguru-based logging system with environment variable controls
23- for log level and enabling/disabling logging. It includes a dedicated stream wrapper
24- for consistent log formatting.
22+ This module configures a standard library logging system with environment variable controls
23+ for log level and enabling/disabling logging. It uses a namespaced logger ('contextgem').
2524"""
2625
2726from __future__ import annotations
2827
28+ import logging
2929import os
3030import sys
31+ import threading
32+ from typing import Protocol
3133
32- from loguru import logger
34+ import colorlog
3335
3436from contextgem .internal .suppressions import _install_litellm_noise_filters
3537
3638
39+ class _LoggerProtocol (Protocol ):
40+ """
41+ Protocol defining the logger interface with custom methods.
42+
43+ This Protocol is used purely for type checking to inform type checkers
44+ (e.g. Pyright) about the custom .trace() and .success() methods that
45+ are dynamically added to logging.Logger at runtime.
46+ """
47+
48+ propagate : bool
49+ handlers : list [logging .Handler ]
50+
51+ def trace (self , message : str , * args , ** kwargs ) -> None :
52+ """
53+ Log with TRACE level (below DEBUG).
54+ """
55+ ...
56+
57+ def debug (self , message : str , * args , ** kwargs ) -> None :
58+ """
59+ Log with DEBUG level.
60+ """
61+ ...
62+
63+ def info (self , message : str , * args , ** kwargs ) -> None :
64+ """
65+ Log with INFO level.
66+ """
67+ ...
68+
69+ def success (self , message : str , * args , ** kwargs ) -> None :
70+ """
71+ Log with SUCCESS level (between INFO and WARNING).
72+ """
73+ ...
74+
75+ def warning (self , message : str , * args , ** kwargs ) -> None :
76+ """
77+ Log with WARNING level.
78+ """
79+ ...
80+
81+ def error (self , message : str , * args , ** kwargs ) -> None :
82+ """
83+ Log with ERROR level.
84+ """
85+ ...
86+
87+ def critical (self , message : str , * args , ** kwargs ) -> None :
88+ """
89+ Log with CRITICAL level.
90+ """
91+ ...
92+
93+ def addHandler (self , handler : logging .Handler ) -> None : # noqa: N802
94+ """
95+ Adds a handler to the logger.
96+ """
97+ ...
98+
99+ def removeHandler (self , handler : logging .Handler ) -> None : # noqa: N802
100+ """
101+ Removes a handler from the logger.
102+ """
103+ ...
104+
105+ def setLevel (self , level : int ) -> None : # noqa: N802
106+ """
107+ Sets the logging level.
108+ """
109+ ...
110+
111+
37112DEFAULT_LOGGER_LEVEL = "INFO"
38113
39114# Dynamically control logging state with env vars
40115LOGGER_LEVEL_ENV_VAR_NAME = "CONTEXTGEM_LOGGER_LEVEL"
41116
117+ # Add custom levels
118+ TRACE_LEVEL_NUM = 5 # Below DEBUG (10)
119+ SUCCESS_LEVEL_NUM = 25 # Between INFO (20) and WARNING (30)
120+ logging .addLevelName (TRACE_LEVEL_NUM , "TRACE" )
121+ logging .addLevelName (SUCCESS_LEVEL_NUM , "SUCCESS" )
122+
42123
43- class _DedicatedStream :
124+ def _trace ( self , message , * args , ** kwargs ) :
44125 """
45- A dedicated stream wrapper for formatting and directing messages to
46- a base stream.
126+ Logs a message with severity 'TRACE' on this logger.
127+
128+ This is a custom level below DEBUG.
47129 """
130+ if self .isEnabledFor (TRACE_LEVEL_NUM ):
131+ self ._log (TRACE_LEVEL_NUM , message , args , ** kwargs )
48132
49- def __init__ (self , base ):
50- self .base = base
51133
52- def write (self , message ):
53- """
54- Writes a message to the base stream with contextgem prefix .
134+ def _success (self , message , * args , ** kwargs ):
135+ """
136+ Logs a message with severity 'SUCCESS' on this logger .
55137
56- :param message: The message to write to the stream.
57- :type message: str
58- """
59- # You can add a prefix or other formatting if you wish
60- self .base .write (f"[contextgem] { message } " )
138+ This is a custom level between INFO and WARNING.
139+ """
140+ if self .isEnabledFor (SUCCESS_LEVEL_NUM ):
141+ self ._log (SUCCESS_LEVEL_NUM , message , args , ** kwargs )
61142
62- def flush (self ):
63- """
64- Flushes the base stream to ensure all output is written.
65- """
66- self .base .flush ()
67143
144+ # Add custom methods to Logger class
145+ logging .Logger .trace = _trace # type: ignore[attr-defined]
146+ logging .Logger .success = _success # type: ignore[attr-defined]
147+
148+ # Create a namespaced logger for ContextGem
149+ logger : _LoggerProtocol = logging .getLogger ("contextgem" ) # type: ignore[assignment]
150+
151+ # Add NullHandler by default
152+ logger .addHandler (logging .NullHandler ())
68153
69- dedicated_stream = _DedicatedStream (sys .stdout )
154+ # Track our handler to avoid duplicates
155+ _contextgem_handler : logging .Handler | None = None
156+ _handler_lock = threading .Lock ()
70157
71158
72- # Helper to read environment config at import time
73159def _read_env_vars () -> tuple [bool , str ]:
74160 """
75161 Returns the (disabled_status, level) read from environment variables.
162+
163+ :return: Tuple of (should_disable, level_string)
164+ :rtype: tuple[bool, str]
76165 """
166+
77167 # Default to DEFAULT_LOGGER_LEVEL if no variable is set or invalid
78168 level_str = os .getenv (LOGGER_LEVEL_ENV_VAR_NAME , DEFAULT_LOGGER_LEVEL ).upper ()
79169 valid_levels = [
@@ -94,55 +184,87 @@ def _read_env_vars() -> tuple[bool, str]:
94184 return disable_logger , level_str
95185
96186
97- def _apply_color_scheme () :
187+ def _get_colored_formatter () -> logging . Formatter :
98188 """
99- Defines custom colors for each log level (mimicking colorlog style)
189+ Creates a colored formatter using colorlog with millisecond precision.
190+
191+ :return: A logging formatter with color support and milliseconds
192+ :rtype: logging.Formatter
100193 """
101- logger .level ("DEBUG" , color = "<cyan>" )
102- logger .level ("INFO" , color = "<blue>" )
103- logger .level ("SUCCESS" , color = "<green>" )
104- logger .level ("WARNING" , color = "<yellow>" )
105- logger .level ("ERROR" , color = "<red>" )
106- logger .level ("CRITICAL" , color = "<red><bold>" )
194+
195+ # Use colorlog for colored output with milliseconds
196+ return colorlog .ColoredFormatter (
197+ "[contextgem] %(log_color)s%(asctime)s.%(msecs)03d%(reset)s | "
198+ "%(log_color)s%(levelname)-7s%(reset)s | %(message)s" ,
199+ datefmt = "%Y-%m-%d %H:%M:%S" ,
200+ reset = True ,
201+ log_colors = {
202+ "TRACE" : "dim" ,
203+ "DEBUG" : "cyan" ,
204+ "INFO" : "blue" ,
205+ "SUCCESS" : "green" ,
206+ "WARNING" : "yellow" ,
207+ "ERROR" : "red" ,
208+ "CRITICAL" : "red,bold" ,
209+ },
210+ style = "%" ,
211+ )
107212
108213
109- # Main configuration function
110214def _configure_logger_from_env ():
111215 """
112- Configures the Loguru logger based on environment variables.
216+ Configures the contextgem logger based on environment variables.
113217 This can be called at import time (once) or re-called any time.
114218
115- (Loguru does not require `getLogger(name)`; we just import `logger` and use it.)
116- """
117- disable_logger , level_str = _read_env_vars ()
118-
119- # Remove default handlers
120- logger .remove ()
121-
122- # If logging is disabled (OFF level), just disable and don't add any handlers
123- if disable_logger :
124- logger .disable ("" )
125- return
126-
127- # Enable logging and add handler
128- logger .enable ("" )
129-
130- # Apply custom level color scheme
131- _apply_color_scheme ()
132-
133- logger .add (
134- dedicated_stream ,
135- level = level_str ,
136- colorize = True ,
137- format = (
138- "<white>{time:YYYY-MM-DD HH:mm:ss.SSS}</white> | "
139- "<level>{level: <7}</level> | "
140- "{message}"
141- ),
142- )
143-
144- # Install filters to suppress noisy third-party dependency logs
145- _install_litellm_noise_filters ()
146-
219+ This function only affects the 'contextgem' logger and is thread-safe.
147220
221+ :return: None
222+ :rtype: None
223+ """
224+ global _contextgem_handler
225+
226+ with _handler_lock :
227+ disable_logger , level_str = _read_env_vars ()
228+
229+ # Remove our previous handler if it exists
230+ if _contextgem_handler is not None :
231+ logger .removeHandler (_contextgem_handler )
232+ _contextgem_handler = None
233+
234+ # If logging is disabled (OFF level), remove all handlers except NullHandler
235+ if disable_logger :
236+ # Remove all non-NullHandler handlers
237+ for handler in logger .handlers [:]:
238+ if not isinstance (handler , logging .NullHandler ):
239+ logger .removeHandler (handler )
240+ # Set level high to ensure nothing gets through
241+ logger .setLevel (logging .CRITICAL + 1 )
242+ # Don't propagate to avoid any output
243+ logger .propagate = False
244+ return
245+
246+ # Enable logging and add handler
247+ # Handle custom levels specially
248+ if level_str == "TRACE" :
249+ logger .setLevel (TRACE_LEVEL_NUM )
250+ elif level_str == "SUCCESS" :
251+ logger .setLevel (SUCCESS_LEVEL_NUM )
252+ else :
253+ logger .setLevel (getattr (logging , level_str ))
254+
255+ # Don't propagate - we manage our own output
256+ # This prevents duplicate logs if the root logger also has handlers
257+ logger .propagate = False
258+
259+ # Create and configure handler
260+ handler = logging .StreamHandler (sys .stdout )
261+ handler .setFormatter (_get_colored_formatter ())
262+ logger .addHandler (handler )
263+ _contextgem_handler = handler
264+
265+ # Install filters to suppress noisy third-party dependency logs
266+ _install_litellm_noise_filters ()
267+
268+
269+ # Configure on import
148270_configure_logger_from_env ()
0 commit comments