# yup... I am a dirty windows user ;) otherwise the sha-bang would have been here import sys import logging try: # This is only needed in order to detect if we are in debug mode when running in Pycharm IDE import pydevd DEBUGGING = True except ImportError: # we must not be running from inside the Pycharm IDE, so lets check if the code was called # from a terminal along with the `-d` system flag DEBUGGING = len(sys.argv)>1 and sys.flags.debug==1 # note that sys.flags is a namedtuple object # the following "table" of data was taken from the official python docs for # python version 3.6.8 on May 5, 2019 # found here: https://docs.python.org/3.6/library/logging.html # see that link for the full description of the available attributes # # Attribute name Format # args You shouldn’t need to format this yourself. # asctime %(asctime)s # created %(created)f # exc_info You shouldn’t need to format this yourself. # filename %(filename)s # funcName %(funcName)s # levelname %(levelname)s # levelno %(levelno)s # lineno %(lineno)d # message %(message)s # module %(module)s # msecs %(msecs)d # msg You shouldn’t need to format this yourself. # name %(name)s # pathname %(pathname)s # process %(process)d # processName %(processName)s # relativeCreated %(relativeCreated)d # thread %(thread)d # threadName %(threadName)s # Level Numeric value # CRITICAL 50 # ERROR 40 # WARNING 30 # INFO 20 # DEBUG 10 # NOTSET 0 logging.basicConfig(level=logging.DEBUG if DEBUGGING else logging.INFO) class LoggerExtraDefined(logging.Logger): """ a subclass of logging.Logger which allows the definition of the extra dict at instantiation time so that we may define contextual information for our log records that may be most easily known at the time of the logger's instantiation. This subclass will still defer to invocations of the extra keyword argument on calls to log, but if a call to log leaves extra as None, we substitute in the reference given at instantiation. """ def __init__(self, name, level=logging.NOTSET,extra:dict=None): super().__init__(name, level) self.extra = extra if extra is not None else {"extra":""} def assign_exta_dict(self,extra:dict): self.extra=extra or self.extra def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False): extra = extra if extra is not None else self.extra super(LoggerExtraDefined, self)._log(level, msg, args, exc_info, extra) # logger formatting logging.setLoggerClass(LoggerExtraDefined) # regarding *_PRI_LOGGER_HEADER_COLOR_PREFIX, defined bellow: # HIGH, and LOW priority header color prefix templates vary only in that HIGH sets 7 as a flag. # By including 7 as a formatting flag (the order of flags does not matter) the text will trade # foreground and background colors. # E.G. for a critical message header, supposing the background # is set to black by default, we will see black text on a bright-red background. # # As added emphasis, the order of flags is irrelevant. "1;4;7;Xm"<=>"7;1;4;Xm" or any other # possible permutation of the ordering. # # For further reference and details, please see the following wikipedia link: # http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html ESC_SEQ = "\033[" HIGH_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ+"1;4;7;{}m" LOW_PRI_LOGGER_HEADER_COLOR_PREFIX = ESC_SEQ+"1;4;{}m" LOGGER_MSG_COLOR = ESC_SEQ+"1;{}m" NOTSET_ID = 30 # basic white DBG_ID = 35 # purple INFO_ID = 32 # green -- might be just my eyes, but green(32) and yellow(33) look almost identical WARN_ID = 93 # bright-yellow ERR_ID = 95 # bright-purple CRIT_ID = 91 # bright-red RESET = ESC_SEQ+"0m" log_id_map = {logging.ERROR:ERR_ID, logging.INFO:INFO_ID, logging.WARNING:WARN_ID, logging.DEBUG:DBG_ID, logging.NOTSET:NOTSET_ID} log_levels_to_names = {logging.CRITICAL:"Critical", logging.ERROR:"Error", logging.WARNING:"Warning", logging.INFO:"Info", logging.DEBUG:"Debug", logging.NOTSET:"NotSet"} def logger_setup(root_name: str, child_name: str = None, formatter: logging.Formatter = None, level: int = None, extra_dict:dict=None, handler_delegate=logging.StreamHandler, use_message_first_line_indent:bool=True, **handler_kwargs): """A convenience function for creating well named loggers with optional custom formatter. This function will implement an already defined formatter for you if the formatter param is None. For an example of a good general use logger see the example code at the bottom of this file. SPECIAL NOTE: Although this function's signature allows the caller to pass virtually any logging.Handler subclass into the handler_delegate parameter, I've only tested this functionality against the logging.StreamHandler class. If you wish to use it for others you will likely encounter bugs. But if you are up to the task it seems like it could potentially be a helpful tool for allowing the instantiation of a wide array of utility loggers under a single interface. :param root_name: The root of a logger hierarchy you wish to use for controlling default configuration details for any child loggers you wish to use. Typically it is best to pass in the name of the file or class in which the logger is being instantiated. E.G. __name__ :type root_name: A string :param child_name: The auxiliary logger you wish to create to handle some specific task. The logger which maps to this child will set its level, handlers, and formatters according to your inputs, allowing you to specify many different loggers to manage data output in a form that suites you. :type child_name: A string :param formatter: An optional parameter that specifies the manner in which you wish to format the available logging-event-data as well as how you wish to present the message data for your log events. The default formatter will take one of two styles, based the given `level`. For level==logging.INFO (20) and bellow: log messages will be presented in two parts. * First, a header line that's formatted to be bold, and underlined, that gives the time-stamp for when the log was submitted, the child_name, the model and function and line number from which the log-message was originated * Followed by an indented new-line where the log-message will be be printed. The message For level==logging.INFO+1 (21) and above: log messages will be presented in two parts. * First, a header line that's formatted to be bold, and underlined, that gives the time-stamp for when the log was submitted, the child_name, the model and function and line number from which the log-message was originated * Followed, on the same line as the header, the log-message. This distinction, as opposed to the indented new-line in lower level messaged, is done because it is often the case the when higher level messages occur, there are very many of them. Forcing each one to then be a multi-line message actually makes it much harder to visually parse. * Special note: In order to aid in automated parsing of these log-messages, the header details and log message will be seperated by the following character key: `::>` :type formatter: an instance of logging.Formatter :param handler_delegate: An optional parameter that Specifies the type of handler you want to associate to the logger instance that's mapped to the root_name.child_name you've passed in. The handler will be set up inside of this function, this parameter simply allows you to indicate the way you wish to have your output handled. (E.G. to std_out, std_err, or some file output stream) :type handler_delegate: This should be a delegate function of your desired handler's constructor, DEFAULT=logging.StreamHandler :param extra_dict: If you wish to add additional contextual information to the resulting logger, you can provide that information as key:value pairs. :type extra_dict: A dictionary :param level: Specifies the desired logging level of the resulting logger. DEFAULT=logging.DEBUG :type level: An int, must be in the range of [0,0xffffffff] :param use_message_first_line_indent: This bool can be used to indicate you want the logger to automatically indent the first line of the log-message. DEFAULT=True, this often helps to make it clear at a glance where the log-message starts. :type use_message_first_line_indent: bool :type handler_kwargs: Any additional keywords that should be passed into the construction of the handler. These are necessary for the instantiation of handlers that will output to anything other than sys.std_out, and sys.std_err. :return: a reference to the logger instance that's mapped to the input naming scheme of <root_name>.<child_name > :rtype: logging.Logger """ level = level or (logging.DEBUG if DEBUGGING else logging.NOTSET) if child_name: logger = logging.getLogger(root_name).getChild(child_name) else: child_name = log_levels_to_names[level] logger = logging.getLogger(root_name) logger.assign_exta_dict(extra_dict) # The caller may pass an int into `level` that doesn't directly map onto our color coding. So, # we should level_normalized = min(level,logging.CRITICAL) while level_normalized not in log_levels_to_names: level_normalized-=1 colr_id = log_id_map[level_normalized] if formatter is None: if level<=20: formatter = logging.Formatter( "{header}%(asctime)s - {} - %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg}" "\n\t%(message)s{reset}\n".format( child_name.capitalize(), header=LOW_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id), msg=LOGGER_MSG_COLOR.format(colr_id), reset=RESET), "%H:%M:%S") else: formatter = logging.Formatter( "{header}%(asctime)s - {} - %(module)s.%(funcName)s(...) - %(lineno)d:{reset}{msg}" " ::> %(message)s{reset}".format( child_name.capitalize(), header=LOW_PRI_LOGGER_HEADER_COLOR_PREFIX.format(colr_id), msg=LOGGER_MSG_COLOR.format(colr_id), reset=RESET), "%H:%M:%S") logger.propagate = False handler_kwargs["stream"] = handler_kwargs.get("stream",sys.stdout) handler = handler_delegate(**handler_kwargs) if level > logger.level: logger.setLevel(level) handler.setLevel(level) handler.setFormatter(formatter) logger.addHandler(handler) return logger