"""tagit logging facility. Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports import logging import re # exports __all__ = ( 'CallbackHandler', 'Filter_Or', 'TagitFormatter', 'logger_config', ) ## code ## class CallbackHandler(logging.Handler): """Adapter for logging.Handler that delegates the message to a callback function.""" def __init__(self, clbk, *args, **kwargs): self._clbk = clbk super(CallbackHandler, self).__init__(*args, **kwargs) def emit(self, record): self.acquire() try: self._clbk(self.format, record) except Exception: self.handleError(record) finally: self.release() class Filter_Or(list): """Lets the record pass if any of the child filters accept it.""" def filter(self, record): for itm in iter(self): if itm.filter(record): return True return False class TagitFormatter(logging.Formatter): """Default Formatter for tagit. This formatter implements the following features: * Title awareness * Colors * Message truncating A message of the form 'Title: Message' will be split into the title and message part. The title formatting is specified in *fmt_title* and only included if a title is present. This format string expects a title argument. E.g. fmt_title='[{title}] ' would produce a title '[Title] ' that would then be included in the formatted record. The overal format string (*fmt*) can also include a 'title' argument that specifies how the title should be positioned. The curly-brace style is assumed. If a *maxlen* argument is given, the record's message will be truncated such that the overall length of the log line does not exceed that limit. """ def __init__(self, *args, fmt_title='{title}: ', colors=None, prefix='', truncate='front', maxlen=float('inf'), **kwargs): kwargs['style'] = '{' super(TagitFormatter, self).__init__(*args, **kwargs) self.fmt_title = fmt_title self.cmap = colors self.maxlen = maxlen self.prefix = prefix self.truncate = self.truncate_back if truncate == 'back' else self.truncate_front def color_levelname(self, levelname): if self.cmap is None: return levelname elif levelname in ('ERROR', 'CRITICAL'): return self.cmap.error(levelname) elif levelname in ('WARNING', ): return self.cmap.warn(levelname) elif levelname in ('INFO', ): return self.cmap.info(levelname) elif levelname in ('DEBUG', ): return self.cmap.debug(levelname) else: return levelname def color_title(self, title): if self.cmap is None: return title return self.cmap.title(title) def truncate_back(self, msg, maxlen=None): """Truncate a string.""" maxlen = maxlen if maxlen is not None else self.maxlen if len(msg) > maxlen: return msg[:int(maxlen-3)] + '...' return msg def truncate_front(self, msg, maxlen=None): """Truncate a string.""" maxlen = maxlen if maxlen is not None else self.maxlen if len(msg) > maxlen: return '...' + msg[-int(maxlen-3):] return msg def format_title(self, record): # tagit title format "title: content" rx1 = re.compile('^([^:\s]+(?:\s+[^:\s]+){0,1})\s*:(.*)$') # kivy title format "[title ] content" rx2 = re.compile('^\[(.{12})\](.*)$') msg = str(record.msg) % record.args m1 = rx1.search(msg) m2 = rx2.search(msg) m = m1 if m1 is not None else m2 if m is not None: # title title, msg = m.groups() return self.fmt_title.format(title=self.color_title(title.strip())), msg.strip() else: # no title return '', msg.strip() def format(self, record): # reset level name since it might have been modified record.levelname = logging.getLevelName(record.levelno) # title title, msg = self.format_title(record) # level name levelname = self.color_levelname(record.levelname) # escape message for coloring if self.cmap is not None: msg = self.cmap.escape(msg) # adjust record omsg, record.msg = record.msg, msg olvl, record.levelname = record.levelname, levelname record.title = title # compile log line logline = logging.Formatter(fmt=self._fmt, style='{').format(record) # get effective lengths of individual parts # truncate message to fixed length if 0 < self.maxlen and self.maxlen < float('inf'): if self.cmap is None: record.msg = self.truncate(record.msg, self.maxlen - len(logline) + len(record.msg) - 1) else: tlen = len(self.cmap.unescape(self.cmap.uncolor(logline))) mlen = len(self.cmap.unescape(self.cmap.uncolor(record.msg))) record.msg = self.cmap.escape(self.truncate(self.cmap.unescape(record.msg), self.maxlen - tlen + mlen - 1)) logline = logging.Formatter(fmt=self._fmt, style='{').format(record) # reset record record.msg, record.levelname = omsg, olvl return self.prefix + logline ## logger configuration ## def logger_config(handler, colors, config): """Configure a handler from a user-specified config. Returns the handler. The config is a dict with the following keys: * level : Log level (level name) * filter : Accept all specified modules (list of module names) * fmt : Main format string * title : Format string for the title * maxlen : Maximum log entry line. No limit if unspecified. * prefix : Log line prefix """ if 'level' in config: handler.setLevel(config['level'].upper()) if 'filter' in config: handler.addFilter(Filter_Or(map(logging.Filter, config['filter']))) handler.setFormatter(TagitFormatter( fmt = colors.escape(config.get('fmt', '{title}{message}')), fmt_title = colors.escape(config.get('title', '{title}: ')), maxlen = float(config.get('maxlen', 0)), colors = colors, prefix = config.get('prefix', ''), )) return handler ## EOF ##