Source code for libioc.Logger

# Copyright (c) 2017-2019, Stefan Grönke
# Copyright (c) 2014-2018, iocage
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""ioc logging module."""
import os
import sys
import typing

import libioc.errors


[docs]class LogEntry: """A single log entry.""" def __init__( self, message: str, level: str, indent: int=0, logger: 'libioc.Logger.Logger'=None ) -> None: self.message = message self.level = level self.indent = indent self.logger = logger
[docs] def edit( self, message: str=None, indent: int=None ) -> None: """Change the log entry.""" if self.logger is None: raise libioc.errors.CannotRedrawLine( reason="No logger available" ) if message is not None: self.message = message if indent is not None: self.indent = indent self.logger.redraw(self)
def __len__(self) -> int: """Return the number of lines of the log entry.""" return len(self.message.splitlines())
[docs]class Logger: """ioc Logger module.""" COLORS = ( "black", "red", "green", "yellow", "blue", "margenta", "cyan", "white", ) LOG_LEVEL_SETTINGS: typing.Dict[ str, typing.Dict[str, typing.Optional[ typing.Union[str, bool]] ] ] = { "screen": {"color": None}, "info": {"color": None}, "notice": {"color": "magenta"}, "verbose": {"color": "blue"}, "spam": {"color": "green"}, "critical": {"color": "red", "bold": True}, "error": {"color": "red"}, "debug": {"color": "green"}, "warn": {"color": "yellow"} } LOG_LEVELS = ( "critical", "error", "warn", "info", "notice", "verbose", "debug", "spam", "screen" ) __INDENT_PREFIX = " " PRINT_HISTORY: typing.List[LogEntry] = [] def __init__( self, print_level: typing.Optional[str]=None, log_directory: str="/var/log/iocage" ) -> None: self._print_level = print_level self._set_log_directory(log_directory) @property def default_print_level(self) -> str: """Return the static default print level.""" return "info" @property def print_level(self) -> str: """Return the configured or default print level.""" if self._print_level is None: return self.default_print_level else: return self._print_level @print_level.setter def print_level(self, value: str) -> None: """Set a custom print level to override the default.""" if value not in Logger.LOG_LEVELS: raise libioc.errors.InvalidLogLevel( log_level=value, logger=self ) self._print_level = value def _set_log_directory(self, log_directory: str) -> None: self.log_directory = os.path.abspath(log_directory) if not os.path.isdir(log_directory): self._create_log_directory() self.log(f"Log directory set to '{log_directory}'", level="spam")
[docs] def log( self, message: str, level: str="info", indent: int=0 ) -> LogEntry: """Add a log entry.""" log_entry = LogEntry( message=message, level=level, indent=indent, logger=self ) if self._should_print_log_entry(log_entry): self._print_log_entry(log_entry) self.PRINT_HISTORY.append(log_entry) return log_entry
[docs] def verbose( self, message: str, indent: int=0, ) -> LogEntry: """Add a verbose log entry.""" return self.log(message, level="verbose", indent=indent)
[docs] def error( self, message: str, indent: int=0 ) -> LogEntry: """Add an error log entry.""" return self.log(message, level="error", indent=indent)
[docs] def warn( self, message: str, indent: int=0 ) -> LogEntry: """Add a warning log entry.""" return self.log(message, level="warn", indent=indent)
[docs] def debug( self, message: str, indent: int=0 ) -> LogEntry: """Add a debug log entry.""" return self.log(message, level="debug", indent=indent)
[docs] def spam( self, message: str, indent: int=0 ) -> LogEntry: """Add a spam log entry.""" return self.log(message, level="spam", indent=indent)
[docs] def screen( self, message: str, indent: int=0 ) -> LogEntry: """Screen never gets printed to log files.""" return self.log(message, level="screen", indent=indent)
[docs] def redraw(self, log_entry: LogEntry) -> None: """Redraw and update a log entry that was already printed.""" if log_entry not in self.PRINT_HISTORY: raise libioc.errors.CannotRedrawLine( reason="Log entry not found in history" ) if log_entry.level != "screen": raise libioc.errors.CannotRedrawLine( reason=( "Log level 'screen' is required to redraw, " f"but got '{log_entry.level}'" ) ) # calculate the delta of messages printed since i = self.PRINT_HISTORY.index(log_entry) n = len(self.PRINT_HISTORY) delta = sum( map(lambda i: self.PRINT_HISTORY[i].__len__(), range(i, n)) ) output = "".join([ "\r", f"\033[{delta}F", # CPL - Cursor Previous Line "\r", # CR - Carriage Return self._indent( log_entry.message, log_entry.indent ), "\033[K", # EL - Erase in Line "\n" * (delta), "\r" ]) sys.stdout.write(output)
def _should_print_log_entry(self, log_entry: LogEntry) -> bool: if log_entry.level == "screen": return True if self.print_level is False: return False print_level = Logger.LOG_LEVELS.index(self.print_level) return Logger.LOG_LEVELS.index(log_entry.level) <= print_level def _beautify_message( self, message: str, level: str, indent: int=0 ) -> str: color = self._get_level_color(level) message = self._indent(message, indent) message = self._colorize(message, color) return message def _print(self, message: str, level: str, indent: int=0) -> None: print(self._beautify_message(message, level, indent)) def _print_log_entry(self, log_entry: LogEntry) -> None: self._print( log_entry.message, log_entry.level, log_entry.indent ) def _indent(self, message: str, level: int) -> str: indent = Logger.__INDENT_PREFIX * level return "\n".join(map(lambda x: f"{indent}{x}", message.splitlines())) def _create_log_directory(self) -> None: if os.geteuid() != 0: raise libioc.errors.MustBeRoot( f"create {self.log_directory}") os.makedirs(self.log_directory, 0x600) self.log(f"Log directory '{self.log_directory}' created", level="info") def _get_color_code(self, color_name: str) -> int: return Logger.COLORS.index(color_name) + 30 def _get_level_color(self, log_level: str) -> str: try: log_level_setting = Logger.LOG_LEVEL_SETTINGS[log_level] return str(log_level_setting["color"]) except KeyError: return "none" def _colorize(self, message: str, color_name: str) -> str: try: color_code = self._get_color_code(color_name) except ValueError: return message return f"\033[1;{color_code}m{message}\033[0m"