log9er.logger
1import os 2import logging 3from logging.handlers import RotatingFileHandler, SysLogHandler 4 5 6class Logger(object): 7 """A basic logger supporting stderr, syslog, and rotating local file logging. 8 9 Attributes: 10 debug (bool): If True, traceback is logged when `exception=True`. 11 logger (logging.Logger | None): The underlying logger instance, if already provided 12 or created in `__init__`. 13 """ 14 15 debug : bool = False 16 logger : logging.Logger | None = None 17 18 # Default rotating file handler settings 19 max_bytes : int = 10485760 20 backup_count : int = 5 21 22 level_map : dict[str, int] = { 23 "DEBUG": logging.DEBUG, 24 "INFO": logging.INFO, 25 "WARNING": logging.WARNING, 26 "ERROR": logging.ERROR, 27 "CRITICAL": logging.CRITICAL, 28 } 29 30 def __init__( 31 self, 32 name : str = "", 33 log : str = "syslog", 34 level : int | str = logging.INFO, 35 debug : bool = False, 36 propagate : bool = False, 37 max_bytes : int | None = None, 38 backup_count : int | None = None, 39 logger : logging.Logger | None = None 40 ): 41 """Initializes the Logger. 42 43 The `Logger` class provides a flexible interface for logging messages to various 44 backends (such as syslog, file-based logging, or stderr) with customizable 45 log levels. If an existing `logging.Logger` instance is provided, all other 46 initialization parameters (except `debug`) are ignored. 47 48 Args: 49 name (str): The name to assign to the logger instance. Required unless an 50 external `logger` is provided. 51 log (str): The log output destination. For example, `"syslog"` or a file path. 52 If `"stderr"`, logs go to standard output. 53 level (int | str): The default logging level. Can be a string 54 (e.g., `"INFO"`) or an integer constant from the `logging` module 55 (e.g., `logging.INFO`). 56 debug (bool): If True, enable traceback logging when `exception=True`. 57 propagate (bool): Whether log records should propagate to the root logger. 58 If True, the root logger also processes these messages (default: False). 59 max_bytes (int | None): If provided, and if `log` is a file path, use as the 60 `maxBytes` for the `RotatingFileHandler` (default: 10485760). 61 backup_count (int | None): If provided, and if `log` is a file path, use as 62 the `backup_count` for the `RotatingFileHandler` (default: 5). 63 logger (logging.Logger | None): If provided, use this logger instance directly 64 and ignore other arguments (except `debug`). 65 66 Raises: 67 TypeError: If `logger` is provided but is not an instance of `logging.Logger`. 68 ValueError: If `level` is an invalid log level string. 69 FileNotFoundError: If `log` is a file path and its directory does not exist. 70 OSError: If `log="syslog"` but `/dev/log` is not available on this system. 71 """ 72 73 self.debug = debug 74 75 # Logger provided? Then ignore other arguments 76 if logger is not None: 77 if not isinstance(logger, logging.Logger): 78 raise TypeError( 79 f"'logger' must be a 'logging.Logger' instance, not {type(logger)}" 80 ) 81 82 self.logger = logger 83 return 84 85 # No logger provided, so 'name' is required 86 if not name: 87 raise ValueError("'name' is required unless an external 'logger' is provided") 88 89 # Otherwise create a new logger from args 90 if isinstance(max_bytes, int): 91 self.max_bytes = max_bytes 92 if isinstance(backup_count, int): 93 self.backup_count = backup_count 94 95 self._init_logger(name, log, level, propagate) 96 97 def _init_logger( 98 self, 99 name: str, 100 log: str, 101 level: int | str, 102 propagate: bool 103 ) -> None: 104 """ 105 Internal helper to create and configure a new logger. 106 Raises FileNotFoundError, OSError, or ValueError if invalid arguments. 107 """ 108 109 # If log is a file path, ensure the directory exists 110 if log not in ["stderr", "syslog"]: 111 dirpath = os.path.dirname(log) 112 if not os.path.isdir(dirpath): 113 raise FileNotFoundError(f"Log file directory not found: {dirpath}") 114 115 # Check syslog support 116 if log == "syslog" and not os.path.exists("/dev/log"): 117 raise OSError("This system does not support 'syslog' logging.") 118 119 # If level is a string, map it to a logging.* constant 120 if isinstance(level, str): 121 level = level.upper() 122 if level not in self.level_map: 123 raise ValueError(f"Invalid log level '{level}'") 124 level = self.level_map[level] 125 126 # Logger already has handlers? Then let sleeping dogs lie 127 logger = logging.getLogger(name) 128 if logger.hasHandlers(): 129 self.logger = logger 130 return 131 132 # OK safe to make the logger, now... 133 logger.propagate = propagate 134 logger.setLevel(level) 135 136 datefmt = "%b %d %H:%M:%S" 137 msgfmt = "%(asctime)s %(name)s[%(process)s]: [%(levelname)s] %(message)s" 138 139 # Select a formatter 140 if log == "stderr": 141 formatter = ErrorColorFormatter(fmt=msgfmt, datefmt=datefmt) 142 else: 143 formatter = logging.Formatter(msgfmt, datefmt=datefmt) 144 145 # Select a handler 146 if log == "stderr": 147 handler = logging.StreamHandler() 148 elif log == "syslog": 149 handler = SysLogHandler(address="/dev/log") 150 else: 151 # Rotating file 152 handler = RotatingFileHandler( 153 log, maxBytes=self.max_bytes, backupCount=self.max_bytes 154 ) 155 156 handler.setFormatter(formatter) 157 logger.addHandler(handler) 158 159 self.logger = logger 160 161 def log( 162 self, 163 *msgs : str, 164 level : int | str = logging.INFO, 165 exception : bool = False, 166 prefix : str | None = None 167 ) -> None: 168 """Logs one or more messages at the specified level. 169 170 If multiple messages are passed, they are joined with a newline+tab ("\n\t"). 171 If `exception=True`, the messages are prefixed with "Exception:" and a traceback 172 is logged if `debug=True`. If a `prefix` string is provided, it is inserted in 173 square brackets before the message text. 174 175 Args: 176 *msgs (str): One or more strings to log, e.g. `"Hello"`, `"World"`. 177 level (int | str): The log level (e.g., `logging.INFO` or `"INFO"`). Defaults 178 to `logging.INFO`. 179 exception (bool): If True, prefix with "Exception:" and log a traceback if 180 `self.debug` is also True. 181 prefix (str | None): Optional prefix label. If provided, the log message is 182 prefixed with e.g. `"[myfunction] message"`. 183 184 Returns: 185 None 186 """ 187 188 # Convert and join all msgs 189 msg = "\n\t".join([str(m) for m in msgs]) 190 191 if exception: 192 msg = f"Exception: {msg}" 193 194 if prefix: 195 msg = f"[{prefix}] {msg}" 196 197 # If level is a string, map it to a logging.* constant 198 if not isinstance(level, int) and not isinstance(level, str): 199 raise ValueError(f"Invalid log level '{level}'. Must be `logging` level constant or string.") 200 201 if isinstance(level, str): 202 level = level.upper() 203 if level not in self.level_map: 204 raise ValueError(f"Invalid log level '{level}'") 205 level = self.level_map[level] 206 207 # If we're dealing with an exception and debug is enabled, log with traceback 208 if exception and self.debug: 209 # logger.exception() always logs at ERROR level + traceback 210 self.logger.exception(msg) 211 return 212 213 # Otherwise, if exception=True but debug=False, just log as ERROR without traceback 214 if exception: 215 level = logging.ERROR 216 217 # Log normally at the resolved level 218 if level == logging.DEBUG: 219 self.logger.debug(msg) 220 elif level == logging.INFO: 221 self.logger.info(msg) 222 elif level == logging.WARNING: 223 self.logger.warning(msg) 224 elif level == logging.ERROR: 225 self.logger.error(msg) 226 elif level == logging.FATAL: 227 self.logger.fatal(msg) 228 else: 229 self.logger.log(level, msg) 230 231 232class ErrorColorFormatter(logging.Formatter): 233 """A custom formatter that applies bold red color for ERROR or CRITICAL messages. 234 235 Attributes: 236 BOLD_RED (str): The ANSI escape sequence for bold red text. 237 RESET (str): The ANSI escape sequence to reset color. 238 """ 239 240 BOLD_RED = "\x1b[1;31m" 241 RESET = "\x1b[0m" 242 243 def format(self, record: logging.LogRecord) -> str: 244 """Formats a log record. 245 246 If the level is ERROR or CRITICAL, the message is colored in bold red. 247 248 Args: 249 record (logging.LogRecord): The record to format. 250 251 Returns: 252 str: The formatted log message, colored if needed. 253 """ 254 255 log_msg = super().format(record) 256 if record.levelno >= logging.ERROR: 257 return f"{self.BOLD_RED}{log_msg}{self.RESET}" 258 return log_msg
7class Logger(object): 8 """A basic logger supporting stderr, syslog, and rotating local file logging. 9 10 Attributes: 11 debug (bool): If True, traceback is logged when `exception=True`. 12 logger (logging.Logger | None): The underlying logger instance, if already provided 13 or created in `__init__`. 14 """ 15 16 debug : bool = False 17 logger : logging.Logger | None = None 18 19 # Default rotating file handler settings 20 max_bytes : int = 10485760 21 backup_count : int = 5 22 23 level_map : dict[str, int] = { 24 "DEBUG": logging.DEBUG, 25 "INFO": logging.INFO, 26 "WARNING": logging.WARNING, 27 "ERROR": logging.ERROR, 28 "CRITICAL": logging.CRITICAL, 29 } 30 31 def __init__( 32 self, 33 name : str = "", 34 log : str = "syslog", 35 level : int | str = logging.INFO, 36 debug : bool = False, 37 propagate : bool = False, 38 max_bytes : int | None = None, 39 backup_count : int | None = None, 40 logger : logging.Logger | None = None 41 ): 42 """Initializes the Logger. 43 44 The `Logger` class provides a flexible interface for logging messages to various 45 backends (such as syslog, file-based logging, or stderr) with customizable 46 log levels. If an existing `logging.Logger` instance is provided, all other 47 initialization parameters (except `debug`) are ignored. 48 49 Args: 50 name (str): The name to assign to the logger instance. Required unless an 51 external `logger` is provided. 52 log (str): The log output destination. For example, `"syslog"` or a file path. 53 If `"stderr"`, logs go to standard output. 54 level (int | str): The default logging level. Can be a string 55 (e.g., `"INFO"`) or an integer constant from the `logging` module 56 (e.g., `logging.INFO`). 57 debug (bool): If True, enable traceback logging when `exception=True`. 58 propagate (bool): Whether log records should propagate to the root logger. 59 If True, the root logger also processes these messages (default: False). 60 max_bytes (int | None): If provided, and if `log` is a file path, use as the 61 `maxBytes` for the `RotatingFileHandler` (default: 10485760). 62 backup_count (int | None): If provided, and if `log` is a file path, use as 63 the `backup_count` for the `RotatingFileHandler` (default: 5). 64 logger (logging.Logger | None): If provided, use this logger instance directly 65 and ignore other arguments (except `debug`). 66 67 Raises: 68 TypeError: If `logger` is provided but is not an instance of `logging.Logger`. 69 ValueError: If `level` is an invalid log level string. 70 FileNotFoundError: If `log` is a file path and its directory does not exist. 71 OSError: If `log="syslog"` but `/dev/log` is not available on this system. 72 """ 73 74 self.debug = debug 75 76 # Logger provided? Then ignore other arguments 77 if logger is not None: 78 if not isinstance(logger, logging.Logger): 79 raise TypeError( 80 f"'logger' must be a 'logging.Logger' instance, not {type(logger)}" 81 ) 82 83 self.logger = logger 84 return 85 86 # No logger provided, so 'name' is required 87 if not name: 88 raise ValueError("'name' is required unless an external 'logger' is provided") 89 90 # Otherwise create a new logger from args 91 if isinstance(max_bytes, int): 92 self.max_bytes = max_bytes 93 if isinstance(backup_count, int): 94 self.backup_count = backup_count 95 96 self._init_logger(name, log, level, propagate) 97 98 def _init_logger( 99 self, 100 name: str, 101 log: str, 102 level: int | str, 103 propagate: bool 104 ) -> None: 105 """ 106 Internal helper to create and configure a new logger. 107 Raises FileNotFoundError, OSError, or ValueError if invalid arguments. 108 """ 109 110 # If log is a file path, ensure the directory exists 111 if log not in ["stderr", "syslog"]: 112 dirpath = os.path.dirname(log) 113 if not os.path.isdir(dirpath): 114 raise FileNotFoundError(f"Log file directory not found: {dirpath}") 115 116 # Check syslog support 117 if log == "syslog" and not os.path.exists("/dev/log"): 118 raise OSError("This system does not support 'syslog' logging.") 119 120 # If level is a string, map it to a logging.* constant 121 if isinstance(level, str): 122 level = level.upper() 123 if level not in self.level_map: 124 raise ValueError(f"Invalid log level '{level}'") 125 level = self.level_map[level] 126 127 # Logger already has handlers? Then let sleeping dogs lie 128 logger = logging.getLogger(name) 129 if logger.hasHandlers(): 130 self.logger = logger 131 return 132 133 # OK safe to make the logger, now... 134 logger.propagate = propagate 135 logger.setLevel(level) 136 137 datefmt = "%b %d %H:%M:%S" 138 msgfmt = "%(asctime)s %(name)s[%(process)s]: [%(levelname)s] %(message)s" 139 140 # Select a formatter 141 if log == "stderr": 142 formatter = ErrorColorFormatter(fmt=msgfmt, datefmt=datefmt) 143 else: 144 formatter = logging.Formatter(msgfmt, datefmt=datefmt) 145 146 # Select a handler 147 if log == "stderr": 148 handler = logging.StreamHandler() 149 elif log == "syslog": 150 handler = SysLogHandler(address="/dev/log") 151 else: 152 # Rotating file 153 handler = RotatingFileHandler( 154 log, maxBytes=self.max_bytes, backupCount=self.max_bytes 155 ) 156 157 handler.setFormatter(formatter) 158 logger.addHandler(handler) 159 160 self.logger = logger 161 162 def log( 163 self, 164 *msgs : str, 165 level : int | str = logging.INFO, 166 exception : bool = False, 167 prefix : str | None = None 168 ) -> None: 169 """Logs one or more messages at the specified level. 170 171 If multiple messages are passed, they are joined with a newline+tab ("\n\t"). 172 If `exception=True`, the messages are prefixed with "Exception:" and a traceback 173 is logged if `debug=True`. If a `prefix` string is provided, it is inserted in 174 square brackets before the message text. 175 176 Args: 177 *msgs (str): One or more strings to log, e.g. `"Hello"`, `"World"`. 178 level (int | str): The log level (e.g., `logging.INFO` or `"INFO"`). Defaults 179 to `logging.INFO`. 180 exception (bool): If True, prefix with "Exception:" and log a traceback if 181 `self.debug` is also True. 182 prefix (str | None): Optional prefix label. If provided, the log message is 183 prefixed with e.g. `"[myfunction] message"`. 184 185 Returns: 186 None 187 """ 188 189 # Convert and join all msgs 190 msg = "\n\t".join([str(m) for m in msgs]) 191 192 if exception: 193 msg = f"Exception: {msg}" 194 195 if prefix: 196 msg = f"[{prefix}] {msg}" 197 198 # If level is a string, map it to a logging.* constant 199 if not isinstance(level, int) and not isinstance(level, str): 200 raise ValueError(f"Invalid log level '{level}'. Must be `logging` level constant or string.") 201 202 if isinstance(level, str): 203 level = level.upper() 204 if level not in self.level_map: 205 raise ValueError(f"Invalid log level '{level}'") 206 level = self.level_map[level] 207 208 # If we're dealing with an exception and debug is enabled, log with traceback 209 if exception and self.debug: 210 # logger.exception() always logs at ERROR level + traceback 211 self.logger.exception(msg) 212 return 213 214 # Otherwise, if exception=True but debug=False, just log as ERROR without traceback 215 if exception: 216 level = logging.ERROR 217 218 # Log normally at the resolved level 219 if level == logging.DEBUG: 220 self.logger.debug(msg) 221 elif level == logging.INFO: 222 self.logger.info(msg) 223 elif level == logging.WARNING: 224 self.logger.warning(msg) 225 elif level == logging.ERROR: 226 self.logger.error(msg) 227 elif level == logging.FATAL: 228 self.logger.fatal(msg) 229 else: 230 self.logger.log(level, msg)
A basic logger supporting stderr, syslog, and rotating local file logging.
Attributes:
- debug (bool): If True, traceback is logged when
exception=True
. - logger (logging.Logger | None): The underlying logger instance, if already provided
or created in
__init__
.
31 def __init__( 32 self, 33 name : str = "", 34 log : str = "syslog", 35 level : int | str = logging.INFO, 36 debug : bool = False, 37 propagate : bool = False, 38 max_bytes : int | None = None, 39 backup_count : int | None = None, 40 logger : logging.Logger | None = None 41 ): 42 """Initializes the Logger. 43 44 The `Logger` class provides a flexible interface for logging messages to various 45 backends (such as syslog, file-based logging, or stderr) with customizable 46 log levels. If an existing `logging.Logger` instance is provided, all other 47 initialization parameters (except `debug`) are ignored. 48 49 Args: 50 name (str): The name to assign to the logger instance. Required unless an 51 external `logger` is provided. 52 log (str): The log output destination. For example, `"syslog"` or a file path. 53 If `"stderr"`, logs go to standard output. 54 level (int | str): The default logging level. Can be a string 55 (e.g., `"INFO"`) or an integer constant from the `logging` module 56 (e.g., `logging.INFO`). 57 debug (bool): If True, enable traceback logging when `exception=True`. 58 propagate (bool): Whether log records should propagate to the root logger. 59 If True, the root logger also processes these messages (default: False). 60 max_bytes (int | None): If provided, and if `log` is a file path, use as the 61 `maxBytes` for the `RotatingFileHandler` (default: 10485760). 62 backup_count (int | None): If provided, and if `log` is a file path, use as 63 the `backup_count` for the `RotatingFileHandler` (default: 5). 64 logger (logging.Logger | None): If provided, use this logger instance directly 65 and ignore other arguments (except `debug`). 66 67 Raises: 68 TypeError: If `logger` is provided but is not an instance of `logging.Logger`. 69 ValueError: If `level` is an invalid log level string. 70 FileNotFoundError: If `log` is a file path and its directory does not exist. 71 OSError: If `log="syslog"` but `/dev/log` is not available on this system. 72 """ 73 74 self.debug = debug 75 76 # Logger provided? Then ignore other arguments 77 if logger is not None: 78 if not isinstance(logger, logging.Logger): 79 raise TypeError( 80 f"'logger' must be a 'logging.Logger' instance, not {type(logger)}" 81 ) 82 83 self.logger = logger 84 return 85 86 # No logger provided, so 'name' is required 87 if not name: 88 raise ValueError("'name' is required unless an external 'logger' is provided") 89 90 # Otherwise create a new logger from args 91 if isinstance(max_bytes, int): 92 self.max_bytes = max_bytes 93 if isinstance(backup_count, int): 94 self.backup_count = backup_count 95 96 self._init_logger(name, log, level, propagate)
Initializes the Logger.
The Logger
class provides a flexible interface for logging messages to various
backends (such as syslog, file-based logging, or stderr) with customizable
log levels. If an existing logging.Logger
instance is provided, all other
initialization parameters (except debug
) are ignored.
Arguments:
- name (str): The name to assign to the logger instance. Required unless an
external
logger
is provided. - log (str): The log output destination. For example,
"syslog"
or a file path. If"stderr"
, logs go to standard output. - level (int | str): The default logging level. Can be a string
(e.g.,
"INFO"
) or an integer constant from thelogging
module (e.g.,logging.INFO
). - debug (bool): If True, enable traceback logging when
exception=True
. - propagate (bool): Whether log records should propagate to the root logger. If True, the root logger also processes these messages (default: False).
- max_bytes (int | None): If provided, and if
log
is a file path, use as themaxBytes
for theRotatingFileHandler
(default: 10485760). - backup_count (int | None): If provided, and if
log
is a file path, use as thebackup_count
for theRotatingFileHandler
(default: 5). - logger (logging.Logger | None): If provided, use this logger instance directly
and ignore other arguments (except
debug
).
Raises:
162 def log( 163 self, 164 *msgs : str, 165 level : int | str = logging.INFO, 166 exception : bool = False, 167 prefix : str | None = None 168 ) -> None: 169 """Logs one or more messages at the specified level. 170 171 If multiple messages are passed, they are joined with a newline+tab ("\n\t"). 172 If `exception=True`, the messages are prefixed with "Exception:" and a traceback 173 is logged if `debug=True`. If a `prefix` string is provided, it is inserted in 174 square brackets before the message text. 175 176 Args: 177 *msgs (str): One or more strings to log, e.g. `"Hello"`, `"World"`. 178 level (int | str): The log level (e.g., `logging.INFO` or `"INFO"`). Defaults 179 to `logging.INFO`. 180 exception (bool): If True, prefix with "Exception:" and log a traceback if 181 `self.debug` is also True. 182 prefix (str | None): Optional prefix label. If provided, the log message is 183 prefixed with e.g. `"[myfunction] message"`. 184 185 Returns: 186 None 187 """ 188 189 # Convert and join all msgs 190 msg = "\n\t".join([str(m) for m in msgs]) 191 192 if exception: 193 msg = f"Exception: {msg}" 194 195 if prefix: 196 msg = f"[{prefix}] {msg}" 197 198 # If level is a string, map it to a logging.* constant 199 if not isinstance(level, int) and not isinstance(level, str): 200 raise ValueError(f"Invalid log level '{level}'. Must be `logging` level constant or string.") 201 202 if isinstance(level, str): 203 level = level.upper() 204 if level not in self.level_map: 205 raise ValueError(f"Invalid log level '{level}'") 206 level = self.level_map[level] 207 208 # If we're dealing with an exception and debug is enabled, log with traceback 209 if exception and self.debug: 210 # logger.exception() always logs at ERROR level + traceback 211 self.logger.exception(msg) 212 return 213 214 # Otherwise, if exception=True but debug=False, just log as ERROR without traceback 215 if exception: 216 level = logging.ERROR 217 218 # Log normally at the resolved level 219 if level == logging.DEBUG: 220 self.logger.debug(msg) 221 elif level == logging.INFO: 222 self.logger.info(msg) 223 elif level == logging.WARNING: 224 self.logger.warning(msg) 225 elif level == logging.ERROR: 226 self.logger.error(msg) 227 elif level == logging.FATAL: 228 self.logger.fatal(msg) 229 else: 230 self.logger.log(level, msg)
Logs one or more messages at the specified level.
If multiple messages are passed, they are joined with a newline+tab ("
").
If exception=True
, the messages are prefixed with "Exception:" and a traceback
is logged if debug=True
. If a prefix
string is provided, it is inserted in
square brackets before the message text.
Arguments:
- *msgs (str): One or more strings to log, e.g.
"Hello"
,"World"
. - level (int | str): The log level (e.g.,
logging.INFO
or"INFO"
). Defaults tologging.INFO
. - exception (bool): If True, prefix with "Exception:" and log a traceback if
self.debug
is also True. - prefix (str | None): Optional prefix label. If provided, the log message is
prefixed with e.g.
"[myfunction] message"
.
Returns:
None
233class ErrorColorFormatter(logging.Formatter): 234 """A custom formatter that applies bold red color for ERROR or CRITICAL messages. 235 236 Attributes: 237 BOLD_RED (str): The ANSI escape sequence for bold red text. 238 RESET (str): The ANSI escape sequence to reset color. 239 """ 240 241 BOLD_RED = "\x1b[1;31m" 242 RESET = "\x1b[0m" 243 244 def format(self, record: logging.LogRecord) -> str: 245 """Formats a log record. 246 247 If the level is ERROR or CRITICAL, the message is colored in bold red. 248 249 Args: 250 record (logging.LogRecord): The record to format. 251 252 Returns: 253 str: The formatted log message, colored if needed. 254 """ 255 256 log_msg = super().format(record) 257 if record.levelno >= logging.ERROR: 258 return f"{self.BOLD_RED}{log_msg}{self.RESET}" 259 return log_msg
A custom formatter that applies bold red color for ERROR or CRITICAL messages.
Attributes:
- BOLD_RED (str): The ANSI escape sequence for bold red text.
- RESET (str): The ANSI escape sequence to reset color.
244 def format(self, record: logging.LogRecord) -> str: 245 """Formats a log record. 246 247 If the level is ERROR or CRITICAL, the message is colored in bold red. 248 249 Args: 250 record (logging.LogRecord): The record to format. 251 252 Returns: 253 str: The formatted log message, colored if needed. 254 """ 255 256 log_msg = super().format(record) 257 if record.levelno >= logging.ERROR: 258 return f"{self.BOLD_RED}{log_msg}{self.RESET}" 259 return log_msg
Formats a log record.
If the level is ERROR or CRITICAL, the message is colored in bold red.
Arguments:
- record (logging.LogRecord): The record to format.
Returns:
str: The formatted log message, colored if needed.