406 lines
12 KiB
Python
406 lines
12 KiB
Python
'''
|
|
Logger object
|
|
=============
|
|
|
|
The Kivy `Logger` class provides a singleton logger instance. This instance
|
|
exposes a standard Python
|
|
`logger <https://docs.python.org/3/library/logging.html>`_ object but adds
|
|
some convenient features.
|
|
|
|
All the standard logging levels are available : `trace`, `debug`, `info`,
|
|
`warning`, `error` and `critical`.
|
|
|
|
Example Usage
|
|
-------------
|
|
|
|
Use the `Logger` as you would a standard Python logger. ::
|
|
|
|
from kivy.logger import Logger
|
|
|
|
Logger.info('title: This is a info message.')
|
|
Logger.debug('title: This is a debug message.')
|
|
|
|
try:
|
|
raise Exception('bleh')
|
|
except Exception:
|
|
Logger.exception('Something happened!')
|
|
|
|
The message passed to the logger is split into two parts separated by a colon
|
|
(:). The first part is used as a title and the second part is used as the
|
|
message. This way, you can "categorize" your messages easily. ::
|
|
|
|
Logger.info('Application: This is a test')
|
|
|
|
# will appear as
|
|
|
|
[INFO ] [Application ] This is a test
|
|
|
|
You can change the logging level at any time using the `setLevel` method. ::
|
|
|
|
from kivy.logger import Logger, LOG_LEVELS
|
|
|
|
Logger.setLevel(LOG_LEVELS["debug"])
|
|
|
|
|
|
Features
|
|
--------
|
|
|
|
Although you are free to use standard python loggers, the Kivy `Logger` offers
|
|
some solid benefits and useful features. These include:
|
|
|
|
* simplied usage (single instance, simple configuration, works by default)
|
|
* color-coded output
|
|
* output to `stdout` by default
|
|
* message categorization via colon separation
|
|
* access to log history even if logging is disabled
|
|
* built-in handling of various cross-platform considerations
|
|
|
|
Kivys' logger was designed to be used with kivy apps and makes logging from
|
|
Kivy apps more convenient.
|
|
|
|
Logger Configuration
|
|
--------------------
|
|
|
|
The Logger can be controlled via the Kivy configuration file::
|
|
|
|
[kivy]
|
|
log_level = info
|
|
log_enable = 1
|
|
log_dir = logs
|
|
log_name = kivy_%y-%m-%d_%_.txt
|
|
log_maxfiles = 100
|
|
|
|
More information about the allowed values are described in the
|
|
:mod:`kivy.config` module.
|
|
|
|
Logger History
|
|
--------------
|
|
|
|
Even if the logger is not enabled, you still have access to the last 100
|
|
messages::
|
|
|
|
from kivy.logger import LoggerHistory
|
|
|
|
print(LoggerHistory.history)
|
|
|
|
'''
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import kivy
|
|
from random import randint
|
|
from functools import partial
|
|
|
|
__all__ = (
|
|
'Logger', 'LOG_LEVELS', 'COLORS', 'LoggerHistory', 'file_log_handler')
|
|
|
|
try:
|
|
PermissionError
|
|
except NameError: # Python 2
|
|
PermissionError = OSError, IOError
|
|
|
|
Logger = None
|
|
|
|
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(8))
|
|
|
|
# These are the sequences need to get colored ouput
|
|
RESET_SEQ = "\033[0m"
|
|
COLOR_SEQ = "\033[1;%dm"
|
|
BOLD_SEQ = "\033[1m"
|
|
|
|
previous_stderr = sys.stderr
|
|
|
|
|
|
def formatter_message(message, use_color=True):
|
|
if use_color:
|
|
message = message.replace("$RESET", RESET_SEQ)
|
|
message = message.replace("$BOLD", BOLD_SEQ)
|
|
else:
|
|
message = message.replace("$RESET", "").replace("$BOLD", "")
|
|
return message
|
|
|
|
|
|
COLORS = {
|
|
'TRACE': MAGENTA,
|
|
'WARNING': YELLOW,
|
|
'INFO': GREEN,
|
|
'DEBUG': CYAN,
|
|
'CRITICAL': RED,
|
|
'ERROR': RED}
|
|
|
|
logging.TRACE = 9
|
|
LOG_LEVELS = {
|
|
'trace': logging.TRACE,
|
|
'debug': logging.DEBUG,
|
|
'info': logging.INFO,
|
|
'warning': logging.WARNING,
|
|
'error': logging.ERROR,
|
|
'critical': logging.CRITICAL}
|
|
|
|
|
|
class FileHandler(logging.Handler):
|
|
history = []
|
|
filename = 'log.txt'
|
|
fd = None
|
|
log_dir = ''
|
|
encoding = 'utf-8'
|
|
|
|
def purge_logs(self):
|
|
'''Purge log is called randomly to prevent the log directory from being
|
|
filled by lots and lots of log files.
|
|
You've a chance of 1 in 20 that purge log will be fired.
|
|
'''
|
|
if randint(0, 20) != 0:
|
|
return
|
|
if not self.log_dir:
|
|
return
|
|
|
|
from kivy.config import Config
|
|
maxfiles = Config.getint('kivy', 'log_maxfiles')
|
|
|
|
if maxfiles < 0:
|
|
return
|
|
|
|
Logger.info('Logger: Purge log fired. Analysing...')
|
|
join = os.path.join
|
|
unlink = os.unlink
|
|
|
|
# search all log files
|
|
lst = [join(self.log_dir, x) for x in os.listdir(self.log_dir)]
|
|
if len(lst) > maxfiles:
|
|
# get creation time on every files
|
|
lst = [{'fn': x, 'ctime': os.path.getctime(x)} for x in lst]
|
|
|
|
# sort by date
|
|
lst = sorted(lst, key=lambda x: x['ctime'])
|
|
|
|
# get the oldest (keep last maxfiles)
|
|
lst = lst[:-maxfiles] if maxfiles else lst
|
|
Logger.info('Logger: Purge %d log files' % len(lst))
|
|
|
|
# now, unlink every file in the list
|
|
for filename in lst:
|
|
try:
|
|
unlink(filename['fn'])
|
|
except PermissionError as e:
|
|
Logger.info('Logger: Skipped file {0}, {1}'.
|
|
format(filename['fn'], e))
|
|
|
|
Logger.info('Logger: Purge finished!')
|
|
|
|
def _configure(self, *largs, **kwargs):
|
|
from time import strftime
|
|
from kivy.config import Config
|
|
log_dir = Config.get('kivy', 'log_dir')
|
|
log_name = Config.get('kivy', 'log_name')
|
|
|
|
_dir = kivy.kivy_home_dir
|
|
if log_dir and os.path.isabs(log_dir):
|
|
_dir = log_dir
|
|
else:
|
|
_dir = os.path.join(_dir, log_dir)
|
|
if not os.path.exists(_dir):
|
|
os.makedirs(_dir)
|
|
self.log_dir = _dir
|
|
|
|
pattern = log_name.replace('%_', '@@NUMBER@@')
|
|
pattern = os.path.join(_dir, strftime(pattern))
|
|
n = 0
|
|
while True:
|
|
filename = pattern.replace('@@NUMBER@@', str(n))
|
|
if not os.path.exists(filename):
|
|
break
|
|
n += 1
|
|
if n > 10000: # prevent maybe flooding ?
|
|
raise Exception('Too many logfile, remove them')
|
|
|
|
if FileHandler.filename == filename and FileHandler.fd is not None:
|
|
return
|
|
FileHandler.filename = filename
|
|
if FileHandler.fd is not None:
|
|
FileHandler.fd.close()
|
|
FileHandler.fd = open(filename, 'w', encoding=FileHandler.encoding)
|
|
Logger.info('Logger: Record log in %s' % filename)
|
|
|
|
def _write_message(self, record):
|
|
if FileHandler.fd in (None, False):
|
|
return
|
|
|
|
msg = self.format(record)
|
|
stream = FileHandler.fd
|
|
fs = "%s\n"
|
|
stream.write('[%-7s] ' % record.levelname)
|
|
stream.write(fs % msg)
|
|
stream.flush()
|
|
|
|
def emit(self, message):
|
|
# during the startup, store the message in the history
|
|
if Logger.logfile_activated is None:
|
|
FileHandler.history += [message]
|
|
return
|
|
|
|
# startup done, if the logfile is not activated, avoid history.
|
|
if Logger.logfile_activated is False:
|
|
FileHandler.history = []
|
|
return
|
|
|
|
if FileHandler.fd is None:
|
|
try:
|
|
self._configure()
|
|
from kivy.config import Config
|
|
Config.add_callback(self._configure, 'kivy', 'log_dir')
|
|
Config.add_callback(self._configure, 'kivy', 'log_name')
|
|
except Exception:
|
|
# deactivate filehandler...
|
|
FileHandler.fd = False
|
|
Logger.exception('Error while activating FileHandler logger')
|
|
return
|
|
while FileHandler.history:
|
|
_message = FileHandler.history.pop()
|
|
self._write_message(_message)
|
|
|
|
self._write_message(message)
|
|
|
|
|
|
class LoggerHistory(logging.Handler):
|
|
|
|
history = []
|
|
|
|
def emit(self, message):
|
|
LoggerHistory.history = [message] + LoggerHistory.history[:100]
|
|
|
|
@classmethod
|
|
def clear_history(cls):
|
|
del cls.history[:]
|
|
|
|
def flush(self):
|
|
super(LoggerHistory, self).flush()
|
|
self.clear_history()
|
|
|
|
|
|
class ColoredFormatter(logging.Formatter):
|
|
|
|
def __init__(self, msg, use_color=True):
|
|
logging.Formatter.__init__(self, msg)
|
|
self.use_color = use_color
|
|
|
|
def format(self, record):
|
|
try:
|
|
msg = record.msg.split(':', 1)
|
|
if len(msg) == 2:
|
|
record.msg = '[%-12s]%s' % (msg[0], msg[1])
|
|
except:
|
|
pass
|
|
levelname = record.levelname
|
|
if record.levelno == logging.TRACE:
|
|
levelname = 'TRACE'
|
|
record.levelname = levelname
|
|
if self.use_color and levelname in COLORS:
|
|
levelname_color = (
|
|
COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ)
|
|
record.levelname = levelname_color
|
|
return logging.Formatter.format(self, record)
|
|
|
|
|
|
class ConsoleHandler(logging.StreamHandler):
|
|
|
|
def filter(self, record):
|
|
try:
|
|
msg = record.msg
|
|
k = msg.split(':', 1)
|
|
if k[0] == 'stderr' and len(k) == 2:
|
|
previous_stderr.write(k[1] + '\n')
|
|
return False
|
|
except:
|
|
pass
|
|
return True
|
|
|
|
|
|
class LogFile(object):
|
|
|
|
def __init__(self, channel, func):
|
|
self.buffer = ''
|
|
self.func = func
|
|
self.channel = channel
|
|
self.errors = ''
|
|
|
|
def write(self, s):
|
|
s = self.buffer + s
|
|
self.flush()
|
|
f = self.func
|
|
channel = self.channel
|
|
lines = s.split('\n')
|
|
for l in lines[:-1]:
|
|
f('%s: %s' % (channel, l))
|
|
self.buffer = lines[-1]
|
|
|
|
def flush(self):
|
|
return
|
|
|
|
def isatty(self):
|
|
return False
|
|
|
|
|
|
def logger_config_update(section, key, value):
|
|
if LOG_LEVELS.get(value) is None:
|
|
raise AttributeError('Loglevel {0!r} doesn\'t exists'.format(value))
|
|
Logger.setLevel(level=LOG_LEVELS.get(value))
|
|
|
|
|
|
#: Kivy default logger instance
|
|
Logger = logging.getLogger('kivy')
|
|
Logger.logfile_activated = None
|
|
Logger.trace = partial(Logger.log, logging.TRACE)
|
|
|
|
# set the Kivy logger as the default
|
|
logging.root = Logger
|
|
|
|
# add default kivy logger
|
|
Logger.addHandler(LoggerHistory())
|
|
file_log_handler = None
|
|
if 'KIVY_NO_FILELOG' not in os.environ:
|
|
file_log_handler = FileHandler()
|
|
Logger.addHandler(file_log_handler)
|
|
|
|
# Use the custom handler instead of streaming one.
|
|
if 'KIVY_NO_CONSOLELOG' not in os.environ:
|
|
if hasattr(sys, '_kivy_logging_handler'):
|
|
Logger.addHandler(getattr(sys, '_kivy_logging_handler'))
|
|
else:
|
|
use_color = (
|
|
(
|
|
os.environ.get("WT_SESSION") or
|
|
os.environ.get("COLORTERM") == 'truecolor' or
|
|
os.environ.get('PYCHARM_HOSTED') == '1' or
|
|
os.environ.get('TERM') in (
|
|
'rxvt',
|
|
'rxvt-256color',
|
|
'rxvt-unicode',
|
|
'rxvt-unicode-256color',
|
|
'xterm',
|
|
'xterm-256color',
|
|
)
|
|
) and os.environ.get('KIVY_BUILD') not in ('android', 'ios')
|
|
)
|
|
if not use_color:
|
|
# No additional control characters will be inserted inside the
|
|
# levelname field, 7 chars will fit "WARNING"
|
|
color_fmt = formatter_message(
|
|
'[%(levelname)-7s] %(message)s', use_color)
|
|
else:
|
|
# levelname field width need to take into account the length of the
|
|
# color control codes (7+4 chars for bold+color, and reset)
|
|
color_fmt = formatter_message(
|
|
'[%(levelname)-18s] %(message)s', use_color)
|
|
formatter = ColoredFormatter(color_fmt, use_color=use_color)
|
|
console = ConsoleHandler()
|
|
console.setFormatter(formatter)
|
|
Logger.addHandler(console)
|
|
|
|
# install stderr handlers
|
|
sys.stderr = LogFile('stderr', Logger.warning)
|
|
|
|
#: Kivy history handler
|
|
LoggerHistory = LoggerHistory
|