Source code for exopy.app.log.tools

# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2015-2018 by Exopy Authors, see AUTHORS for more details.
#
# Distributed under the terms of the BSD license.
#
# The full license is in the file LICENCE, distributed with this software.
# -----------------------------------------------------------------------------
"""This module defines some tools to make easier the use of the logging module.

It provide tools to seamlessly convert stream information into log record so
that any `print` can get recorded, and others to process log emitted in a
subprocess.

:Contains:
    StreamToLogRedirector
        Simple class to redirect a stream to a logger.
    QueueHandler
        Logger handler putting records into a queue.
    GuiConsoleHandler
        Logger handler adding the message of a record to a GUI panel.
    QueueLoggerThread
        Thread getting log record from a queue and asking logging to handle
        them.

"""
import logging
import os
import time
import datetime
import queue
from logging.handlers import TimedRotatingFileHandler
from threading import Thread
from enaml.application import deferred_call
from atom.api import Atom, Str, Int
import codecs


[docs]class StreamToLogRedirector(object): """Simple class to redirect a stream to a logger. Stream like object which can be used to replace `sys.stdout`, or `sys.stderr`. Parameters ---------- logger : instance(`Logger`) Instance of a loger object returned by a call to logging.getLogger stream_type : {'stdout', 'stderr'}, optionnal Type of stream being redirected. Stderr stream are logged as CRITICAL Attributes ---------- logger : instance(`Logger`) Instance of a loger used to log the received message """ def __init__(self, logger, stream_type='stdout'): self.logger = logger if stream_type == 'stderr': self.write = self.write_error else: self.write = self.write_info
[docs] def write_info(self, message): """Log the received message as info, used for stdout. The received message is first strip of starting and trailing whitespaces and line return. """ message = message.strip() message = str(message) if message != '': self.logger.info(message)
[docs] def write_error(self, message): """Log the received message as critical, used for stderr. The received message is first strip of starting and trailing whitespaces and line return. """ message = message.strip() message = str(message) if message != '': self.logger.critical(message)
[docs] def flush(self): """Useless function implemented for compatibility. """ return None
# Copied and pasted from the logging module of Python 3.3
[docs]class QueueHandler(logging.Handler): """Handler sending events to a queue. Typically, it would be used together with a multiprocessing Queue to centralise logging to file in one process (in a multi-process application), so as to avoid file write contention between processes. Errors are silently ignored to avoid possible recursions and that's why this handler should be coupled to another, safer one. Parameters ---------- queue : Queue to use to log the messages. """ def __init__(self, queue): logging.Handler.__init__(self) self.queue = queue
[docs] def enqueue(self, record): """Enqueue a record. The base implementation uses put_nowait. You may want to override this method if you want to use blocking, timeouts or custom queue implementations. """ self.queue.put_nowait(record)
[docs] def prepare(self, record): """ Prepares a record for queueing. The object returned by this method is enqueued. The base implementation formats the record to merge the message and arguments, and removes unpickleable items from the record in-place. You might want to override this method if you want to convert the record to a dict or JSON string, or send a modified copy of the record while leaving the original intact. """ # The format operation gets traceback text into record.exc_text # (if there's exception data), and also puts the message into # record.message. We can then use this to replace the original # msg + args, as these might be unpickleable. We also zap the # exc_info attribute, as it's no longer needed and, if not None, # will typically not be pickleable. self.format(record) record.msg = record.message record.args = None record.exc_info = None return record
[docs] def emit(self, record): """Emit a record. Writes the LogRecord to the queue, preparing it first. """ try: self.enqueue(self.prepare(record)) except Exception: # Don't try to handle the error as we might be redirecting sys.std # let another logger handle the issue. pass
[docs]class QueueLoggerThread(Thread): """Thread emptying a queue containing log record and sending them to the appropriate logger. Attributes ---------- queue : Queue from which to collect log records. """ def __init__(self, queue): Thread.__init__(self) self.queue = queue self.flag = True
[docs] def run(self): """ Pull any output from the queue while the listened process does not put `None` into the queue or somebody turn off the flag. """ while self.flag: # Collect all display output from process try: record = self.queue.get(timeout=0.5) if record is None: break logger = logging.getLogger(record.name) logger.handle(record) except queue.Empty: continue
[docs]class LogModel(Atom): """Simple object which can be used in a GuiHandler. """ #: Text representing all the messages sent by the handler. #: Should not be altered by user code. text = Str() #: Maximum number of lines. buff_size = Int(1000)
[docs] def clean_text(self): """Empty the text member. """ self.text = '' self._lines = 0
[docs] def add_message(self, message): """Add a message to the text member. """ if self._lines > self.buff_size: self.text = self.text.split('\n', self._lines - self.buff_size)[-1] message = message.strip() message = str(message) message += '\n' self._lines += message.count('\n') self.text += message
#: Number of lines. _lines = Int()
ERR_MESS = 'An error occured please check the log file for more details.'
[docs]class GuiHandler(logging.Handler): """Logger record sending the log message to an object which can be linked to a GUI. Errors are silently ignored to avoid possible recursions and that's why this handler should be coupled to another, safer one. Parameters ---------- model : Atom Model object with a text member. Methods ------- emit(record) Handle a log record by appending the log message to the model """ def __init__(self, model): logging.Handler.__init__(self) self.model = model
[docs] def emit(self, record): """ Write the log record message to the model. Use Html encoding to add colors, etc. """ # TODO add coloring. Better to create a custom formatter try: msg = self.format(record) if record.levelname == 'INFO': deferred_call(self.model.add_message, msg + '\n') elif record.levelname == 'CRITICAL': deferred_call(self.model.add_message, ERR_MESS + '\n') else: deferred_call(self.model.add_message, record.levelname + ': ' + msg + '\n') except Exception: pass
[docs]class DayRotatingTimeHandler(TimedRotatingFileHandler): """ Custom implementation of the TimeRotatingHandler to avoid issues on win32. Found on StackOverflow ... """ def __init__(self, filename, mode='wb', **kwargs): self.mode = mode self.path = '' super(DayRotatingTimeHandler, self).__init__(filename, when='MIDNIGHT', **kwargs) def _open(self): """Open a file named accordingly to the base name and the time of creation of the file with the (original) mode and encoding. """ today = str(datetime.date.today()) base_dir, base_filename = os.path.split(self.baseFilename) aux = base_filename.split('.') # Change filename when the logging system start several time on the # same day. i = 0 filename = aux[0] + today + '_%d' + '.' + aux[1] while os.path.isfile(os.path.join(base_dir, filename % i)): i += 1 path = os.path.join(base_dir, filename % i) self.path = path if self.encoding is None: stream = open(path, self.mode) else: stream = codecs.open(path, self.mode, self.encoding) return stream
[docs] def doRollover(self): """Do a rollover. Close old file and open a new one, no renaming is performed to avoid issues on window. """ if self.stream: self.stream.close() self.stream = None # get the time that this sequence started at and make it a TimeTuple current_time = int(time.time()) dst_now = time.localtime(current_time)[-1] self.stream = self._open() new_rollover_at = self.computeRollover(current_time) while new_rollover_at <= current_time: new_rollover_at = new_rollover_at + self.interval # If DST changes and midnight or weekly rollover, adjust for this. if ((self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc): dst_at_rollover = time.localtime(new_rollover_at)[-1] if dst_now != dst_at_rollover: # DST kicks in before next rollover, so we need to deduct an # hour if not dst_now: addend = -3600 # DST bows out before next rollover, so we need to add an hour else: addend = 3600 new_rollover_at += addend self.rolloverAt = new_rollover_at