@@ -103,20 +103,59 @@ def format(self, record: logging.LogRecord) -> str:
103103
104104
105105class ThirdPartyJSONFormatter (logging .Formatter ):
106- """Custom formatter that converts third-party logging to JSON format"""
106+ """Custom formatter that converts third-party logging to JSON format.
107+
108+ If the message is already valid JSON (e.g. from structlog), it is passed through as-is.
109+ """
107110
108111 def format (self , record : logging .LogRecord ) -> str :
109- return json .dumps ({
110- "timestamp" : datetime .fromtimestamp (record .created , tz = UTC ).isoformat (),
111- "logger" : record .name ,
112- "level" : record .levelname .lower (),
113- "event" : record .getMessage (),
114- "filename" : record .pathname .split ("/" )[- 1 ] if record .pathname else "" ,
115- "lineno" : str (record .lineno ),
116- })
112+ msg = record .getMessage ()
113+ try :
114+ json .loads (msg )
115+ return msg
116+ except json .JSONDecodeError , TypeError :
117+ return json .dumps ({
118+ "timestamp" : datetime .fromtimestamp (record .created , tz = UTC ).isoformat (),
119+ "logger" : record .name ,
120+ "level" : record .levelname .lower (),
121+ "event" : msg ,
122+ "filename" : record .pathname .split ("/" )[- 1 ] if record .pathname else "" ,
123+ "lineno" : str (record .lineno ),
124+ })
117125
118126
119127_initialized = False
128+ _human_readable = False
129+ _original_add_handler = logging .Logger .addHandler
130+
131+
132+ def set_human_readable (enabled : bool ) -> None :
133+ """Set log output format and re-initialize structlog if already configured."""
134+ global _human_readable , _initialized
135+ _human_readable = enabled
136+ _initialized = False
137+ _initialize ()
138+
139+
140+ _LEVEL_COLORS = {
141+ "DEBUG" : _FG_CODES ["cyan" ],
142+ "INFO" : _FG_CODES ["green" ],
143+ "WARNING" : _FG_CODES ["yellow" ],
144+ "ERROR" : _FG_CODES ["red" ],
145+ "CRITICAL" : _FG_CODES ["red" ] + _BG_CODES ["white" ],
146+ }
147+
148+
149+ class ThirdPartyHumanReadableFormatter (logging .Formatter ):
150+ """Formatter for third-party logs in human-readable mode with colors and source location."""
151+
152+ def format (self , record : logging .LogRecord ) -> str :
153+ timestamp = datetime .fromtimestamp (record .created , tz = UTC ).isoformat ()
154+ color = _LEVEL_COLORS .get (record .levelname , "" )
155+ reset = _RESET if color else ""
156+ filename = record .pathname .rsplit ("/" , 1 )[- 1 ] if record .pathname else ""
157+ msg = record .getMessage ()
158+ return f"{ timestamp } { record .name } { color } { record .levelname } { reset } { msg } ({ filename } :{ record .lineno } )"
120159
121160
122161def _initialize () -> None :
@@ -125,36 +164,51 @@ def _initialize() -> None:
125164 if _initialized :
126165 return
127166
128- structlog .configure (
129- processors = [
167+ if _human_readable :
168+ processors = [
169+ structlog .processors .KeyValueRenderer (key_order = ["event" ]),
170+ ]
171+ else :
172+ processors = [
130173 structlog .stdlib .add_logger_name ,
131174 structlog .stdlib .add_log_level ,
132175 structlog .processors .TimeStamper (fmt = "ISO" , utc = True ),
133176 structlog .processors .JSONRenderer (),
134- ],
177+ ]
178+
179+ structlog .configure (
180+ processors = processors ,
135181 wrapper_class = structlog .stdlib .BoundLogger ,
136182 logger_factory = structlog .stdlib .LoggerFactory (),
137- cache_logger_on_first_use = True ,
183+ cache_logger_on_first_use = False ,
138184 )
139185
140- # Patch addHandler so new loggers get JSON formatting
141- original_add_handler = logging .Logger .addHandler
186+ third_party_formatter = ThirdPartyHumanReadableFormatter () if _human_readable else ThirdPartyJSONFormatter ()
187+
188+ # Formatters that should not be replaced:
189+ # - JSONOnlyFormatter: used by StructlogWrapper (structlog already formats the message)
190+ # - WrapperLogFormatter: used by setup_logging's QueueHandlers (only protected in HR mode)
191+ _skip_formatters : tuple [type , ...] = (JSONOnlyFormatter ,)
192+ if _human_readable :
193+ _skip_formatters = (* _skip_formatters , WrapperLogFormatter )
142194
195+ # Patch addHandler so new loggers (e.g. from ocp_resources/simple_logger)
196+ # get the correct formatter on their handlers.
143197 def patched_add_handler (self : logging .Logger , hdlr : logging .Handler ) -> None :
144- if not isinstance (hdlr .formatter , ( ThirdPartyJSONFormatter , JSONOnlyFormatter ) ):
145- hdlr .setFormatter (fmt = ThirdPartyJSONFormatter () )
146- original_add_handler (self , hdlr ) # noqa: FCN001
198+ if not isinstance (hdlr .formatter , _skip_formatters ):
199+ hdlr .setFormatter (fmt = third_party_formatter )
200+ _original_add_handler (self , hdlr ) # noqa: FCN001
147201
148202 logging .Logger .addHandler = patched_add_handler # type: ignore[method-assign]
149203
150- # Apply JSON formatter to all existing handlers on all loggers
204+ # Apply formatter to all existing handlers on all loggers
151205 all_loggers = [logging .getLogger ()] + [
152206 logger for logger in logging .root .manager .loggerDict .values () if isinstance (logger , logging .Logger )
153207 ]
154208 for logger in all_loggers :
155209 for handler in logger .handlers :
156- if isinstance (handler .formatter , ( logging . Formatter , type ( None )) ):
157- handler .setFormatter (fmt = ThirdPartyJSONFormatter () )
210+ if not isinstance (handler .formatter , _skip_formatters ):
211+ handler .setFormatter (fmt = third_party_formatter )
158212
159213 _initialized = True
160214
@@ -177,8 +231,14 @@ def _log(self, level: str, msg: Any, *args: Any, **kwargs: Any) -> None:
177231 if args :
178232 msg_str = msg_str % args
179233
180- log_method = getattr (self ._logger , level .lower ())
181- log_method (event = msg_str , ** kwargs )
234+ if _human_readable :
235+ std_logger = logging .getLogger (self .name )
236+ log_method = getattr (std_logger , level .lower ())
237+ extra_str = " " .join (f"{ k } ={ v } " for k , v in kwargs .items ()) if kwargs else ""
238+ log_method (f"{ msg_str } { extra_str } " if extra_str else msg_str , stacklevel = 3 ) # noqa: FCN001
239+ else :
240+ log_method = getattr (self ._logger , level .lower ())
241+ log_method (event = msg_str , ** kwargs )
182242
183243 def info (self , msg : Any , * args : Any , ** kwargs : Any ) -> None :
184244 self ._log ("info" , msg , * args , ** kwargs ) # noqa: FCN001
0 commit comments