"""
sic_logging.py
This module contains the SICLogging class, which is used to log messages to the Redis log channel and a local logfile.
"""
from __future__ import print_function
import io
import logging
import re
import threading
from datetime import datetime
from . import utils
from .message_python2 import SICMessage
from .sic_redis import SICRedis
ANSI_CODE_REGEX = re.compile(r'\033\[[0-9;]*m')
# loglevel interpretation, mostly follows python's defaults
CRITICAL = 50
ERROR = 40
WARNING = 30
INFO = 20 # service dependent sparse information
DEBUG = 10 # service dependent verbose information
NOTSET = 0
[docs]
def get_log_channel(client_id=""):
"""
Get the global log channel. All components on any device should log to this channel.
"""
return "sic:logging:{client_id}".format(client_id=client_id)
[docs]
class SICLogMessage(SICMessage):
[docs]
def __init__(self, msg, client_id=""):
"""
A wrapper for log messages to be sent over the SICRedis pubsub framework.
:param msg: The log message to send to the user
"""
self.msg = msg
self.client_id = None
super(SICLogMessage, self).__init__()
[docs]
class SICRemoteError(Exception):
"""An exception indicating the error happened on a remote device"""
[docs]
class SICCommonLog(object):
"""
A class to subscribe to a Redis log channel and write all log messages to a logfile.
Pseudo singleton object. Does nothing when this file is executed during the import, but can subscribe to the log
channel for the user with subscribe_to_redis_log once.
:param redis: The Redis instance to use for logging.
:type redis: SICRedis
:param logfile: The file path to write the log to.
:type logfile: str
"""
[docs]
def __init__(self):
self.redis = None
self.running = False
# Create log filename with current date
current_date = datetime.now().strftime("%Y-%m-%d")
self.logfile = open("sic_{date}.log".format(date=current_date), "a")
self.lock = threading.Lock()
[docs]
def subscribe_to_redis_log(self, client_id=""):
"""
Subscribe to the Redis log channel and display any messages on the terminal.
This function may be called multiple times but will only subscribe once.
:return: None
"""
with self.lock: # Ensure thread-safe access
if not self.running:
self.running = True
self.redis = SICRedis(parent_name="SICCommonLog")
self.redis.register_message_handler(
get_log_channel(client_id), self._handle_redis_log_message
)
[docs]
def _handle_redis_log_message(self, message):
"""
Handle a message sent on a debug stream. Currently it's just printed to the terminal.
:param message: The message to handle.
:type message: SICLogMessage
"""
# outputs to terminal
print(message.msg, end="\n")
# writes to logfile
self._write_to_logfile(message.msg)
[docs]
def _write_to_logfile(self, message):
"""
Write a message to the logfile.
:param message: The message to write to the logfile.
:type message: str
"""
with self.lock:
# strip ANSI codes before writing to logfile
clean_message = ANSI_CODE_REGEX.sub("", message)
# add timestamp to the log message
timestamp = datetime.now().strftime("%H:%M:%S")
clean_message = "[{timestamp}] {clean_message}".format(timestamp=timestamp, clean_message=clean_message)
if clean_message[-1] != "\n":
clean_message += "\n"
# write to logfile
self.logfile.write(clean_message)
self.logfile.flush()
[docs]
def stop(self):
"""
Stop the logging.
"""
with self.lock: # Ensure thread-safe access
if self.running:
self.running = False
self.redis.close()
[docs]
class SICRedisHandler(logging.Handler):
"""
Facilities to log to Redis as a file-like object, to integrate with standard python logging facilities.
:param redis: The Redis instance to use for logging.
:type redis: SICRedis
:param client_id: The client id of the device that is logging
:type client_id: str
"""
[docs]
def __init__(self, redis, client_id):
super(SICRedisHandler, self).__init__()
self.redis = redis
self.client_id = client_id
self.logging_channel = get_log_channel(client_id)
[docs]
def emit(self, record):
"""
Emit a log message to the Redis log channel.
:param record: The log record to emit.
:type record: logging.LogRecord
"""
try:
# Get the formatted message
msg = self.format(record)
# Create the log message with client_id if it exists
log_message = SICLogMessage(msg)
# If additional client id is provided (as with the ComponentManager), use it to send the log message to the correct channel
if hasattr(record, 'client_id') and self.client_id == "":
log_message.client_id = record.client_id
log_channel = get_log_channel(log_message.client_id)
else:
log_channel = self.logging_channel
# Send over Redis
self.redis.send_message(log_channel, log_message)
except Exception:
self.handleError(record)
[docs]
def readable(self):
"""
Check if the stream is readable.
:return: False
:rtype: bool
"""
return False
[docs]
def writable(self):
"""
Check if the stream is writable.
:return: True
:rtype: bool
"""
return True
[docs]
def write(self, msg):
"""
Write a message to the Redis log channel.
:param msg: The message to write to the Redis log channel.
:type msg: str
"""
# only send logs to redis if a redis instance is associated with this logger
if self.redis != None:
message = SICLogMessage(msg)
self.redis.send_message(self.logging_channel, message)
[docs]
def flush(self):
"""
Flush the stream.
"""
return
[docs]
def get_sic_logger(name="", client_id="", redis=None, log_level=DEBUG):
"""
Set up logging to the log output channel to be able to report messages to users.
:param name: A readable and identifiable name to indicate to the user where the log originated
:type name: str
:param client_id: The client id of the device that is logging
:type client_id: str
:param redis: The SICRedis object
:type redis: SICRedis
:param log_level: The logger.LOGLEVEL verbosity level
:type log_level: int
:return: The logger.
:rtype: logging.Logger
"""
# logging initialisation
logger = logging.Logger(name)
logger.setLevel(log_level)
log_format = SICLogFormatter()
if redis:
# if redis is provided, use our custom handler
handler_redis = SICRedisHandler(redis, client_id)
handler_redis.setFormatter(log_format)
logger.addHandler(handler_redis)
else:
# if there is no redis instance, this is a local device
# make sure the SICCommonLog is subscribed to the Redis log channel
SIC_COMMON_LOG.subscribe_to_redis_log(client_id)
# For local logging, create a custom handler that uses SICCommonLog's file
class SICFileHandler(logging.Handler):
def emit(self, record):
SIC_COMMON_LOG._write_to_logfile(self.format(record))
# log to the terminal
handler_terminal = logging.StreamHandler()
handler_terminal.setFormatter(log_format)
logger.addHandler(handler_terminal)
# write to the logfile
handler_file = SICFileHandler()
handler_file.setFormatter(log_format)
logger.addHandler(handler_file)
return logger
# pseudo singleton object. Does nothing when this file is executed during the import, but can subscribe to the log
# channel for the user with subscribe_to_redis_log once
SIC_COMMON_LOG = SICCommonLog()