Source code for core.logger

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2019-2025 by Murray Altheim. All rights reserved. This file is part
# of the MR01 Robot Operating System (MROS) project, released under the MIT
# License. Please see the LICENSE file included as part of this package.
#
# author:   Murray Altheim
# created:  2020-01-14
# modified: 2025-06-09
#
# This is a subset of the full Logger class, without support for writing to
# a log file, statistics or fancy headings. This is just used for testing,
# the full logger may be found in the KRZOS or MROS projects.
#

import logging
from threading import Lock
from datetime import datetime as dt
from enum import Enum
from colorama import init, Fore, Style
init()

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[docs] class Level(Enum): DEBUG = ( logging.DEBUG, 'DEBUG' ) # 10 INFO = ( logging.INFO, 'INFO' ) # 20 WARN = ( logging.WARN, 'WARN' ) # 30 ERROR = ( logging.ERROR, 'ERROR' ) # 40 CRITICAL = ( logging.CRITICAL, 'CRITICAL' ) # 50 # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ def __new__(cls, *args, **kwds): obj = object.__new__(cls) obj._value_ = args[0] return obj # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ # ignore the first param since it's already set by __new__ def __init__(self, num, label): self._label = label # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] @staticmethod def from_string(label): if label.upper() == 'DEBUG': return Level.DEBUG elif label.upper() == 'INFO': return Level.INFO elif label.upper() == 'WARN': return Level.WARN elif label.upper() == 'ERROR': return Level.ERROR elif label.upper() == 'CRITICAL': return Level.CRITICAL else: raise NotImplementedError
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[docs] class Logger: __suppress = False __color_debug = Fore.BLUE + Style.DIM __color_info = Fore.CYAN + Style.NORMAL __color_notice = Fore.CYAN + Style.BRIGHT __color_warning = Fore.YELLOW + Style.NORMAL __color_error = Fore.RED + Style.NORMAL __color_critical = Fore.WHITE + Style.NORMAL __color_reset = Style.RESET_ALL def __init__(self, name=None, level=None): ''' Writes to a console log with the provided level. :param name: the name identified with the log output :param level: the log level ''' if not name: raise ValueError('no log name specified.') if not level: raise ValueError('no log level specified.') # configuration ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ _strip_ansi_codes = True # used only with file output, to strip ANSI characters from log data self._include_timestamp = True self._date_format = '%Y-%m-%dT%H:%M:%S' # self._date_format = '%Y-%m-%dT%H:%M:%S.%f' # self._date_format = '%H:%M:%S' # i18n? self.__DEBUG_TOKEN = 'DEBUG' self.__INFO_TOKEN = 'INFO ' self.__WARN_TOKEN = 'WARN ' self.__ERROR_TOKEN = 'ERROR' self.__FATAL_TOKEN = 'FATAL' self._mf = '{}{} : {}{}' _1st_col_width = 14 # create logger ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ self.__mutex = Lock() self.__log = logging.getLogger(name) self.__log.propagate = False self._name = name self._sh = None # stream handler if not self.__log.handlers: self._sh = logging.StreamHandler() if self._include_timestamp: self._sh.setFormatter(logging.Formatter(Fore.BLUE + Style.DIM + '%(asctime)s.%(msecs)3fZ\t:' \ + Fore.RESET + ' %(name)s ' + ( ' '*(_1st_col_width-len(name)) ) + ' : %(message)s', datefmt=self._date_format)) else: self._sh.setFormatter(logging.Formatter('%(name)s ' + ( ' '*(_1st_col_width-len(name)) ) + ' : %(message)s')) self.__log.addHandler(self._sh) self.level = level # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ @property def name(self): ''' Return the name of this Logger. ''' return self._name # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def close(self): ''' Closes down logging, and informs the logging system to perform an orderly shutdown by flushing and closing all handlers. This should be called at application exit and no further use of the logging system should be made after this call. ''' # self.suppress() logging.shutdown()
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def suppress(self): ''' Suppresses all log messages except critical errors and log-to-file messages. This is global across all Loggers. ''' type(self).__suppress = True
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def release(self): ''' Releases (un-suppresses) all log messages except critical errors and log-to-file messages. This is global across all Loggers. ''' type(self).__suppress = False
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ @property def level(self): ''' Return the level of this logger. ''' return self._level # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ @level.setter def level(self, level): ''' Set the level of this logger to the argument. ''' self._level = level self.__log.setLevel(self._level.value) if self._sh: self._sh.setLevel(level.value) _level = self._sh.level # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def is_at_least(self, level): ''' Returns True if the current log level is less than or equals the argument. E.g., if self._log.is_at_least(Level.WARN): # returns True for WARN or ERROR or CRITICAL ''' return self._level.value >= level.value
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ @property def suppressed(self): ''' Return True if this logger has been suppressed. ''' return type(self).__suppress # ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def debug(self, message): ''' Prints a debug message. The optional 'end' argument is for special circumstances where a different end-of-line is desired. ''' if not self.suppressed: with self.__mutex: self.__log.debug(self._mf.format(Logger.__color_debug, self.__DEBUG_TOKEN, message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def info(self, message): ''' Prints an informational message. The optional 'end' argument is for special circumstances where a different end-of-line is desired. ''' if not self.suppressed: with self.__mutex: self.__log.info(self._mf.format(Logger.__color_info, self.__INFO_TOKEN, message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def notice(self, message): ''' Functionally identical to info() except it prints the message brighter. The optional 'end' argument is for special circumstances where a different end-of-line is desired. ''' if not self.suppressed: with self.__mutex: self.__log.info(self._mf.format(Logger.__color_notice, self.__INFO_TOKEN, message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def warning(self, message): ''' Prints a warning message. The optional 'end' argument is for special circumstances where a different end-of-line is desired. ''' if not self.suppressed: with self.__mutex: self.__log.warning(self._mf.format(Logger.__color_warning, self.__WARN_TOKEN, message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def error(self, message): ''' Prints an error message. The optional 'end' argument is for special circumstances where a different end-of-line is desired. ''' if not self.suppressed: with self.__mutex: self.__log.error(self._mf.format(Logger.__color_error, self.__ERROR_TOKEN, Style.NORMAL + message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def critical(self, message): ''' Prints a critical or otherwise application-fatal message. ''' with self.__mutex: self.__log.critical(self._mf.format(Logger.__color_critical, self.__FATAL_TOKEN, Style.BRIGHT + message, Logger.__color_reset))
# ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
[docs] def file(self, message): ''' This is just info() but without any formatting. ''' with self.__mutex: self.__log.info(message)
#EOF