"""
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
import os
from . import utils
from .message_python2 import SICMessage
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
DEBUG = 10
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 SIC SICRedisConnection pubsub framework.
:param msg: The log message to send to the user
"""
self.msg = msg
self.client_id = client_id
super(SICLogMessage, self).__init__()
[docs]
class SICRemoteError(Exception):
"""An exception indicating the error happened on a remote device"""
[docs]
class SICClientLog(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: SICRedisConnection
:param logfile: The file path to write the log to.
:type logfile: str
"""
[docs]
def __init__(self):
self.redis = None
self.running = False
self.logfile = None
self.log_dir = None
self.write_to_logfile = False
self.lock = threading.Lock()
self.threshold = DEBUG
self.callback_thread = None
[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.callback_thread = self.redis.register_message_handler(
get_log_channel(client_id), self._handle_redis_log_message, name="SICClientLog"
)
[docs]
def stop(self):
"""
Stop the logging and unregister the callback thread.
"""
with self.lock: # Ensure thread-safe access
if self.running:
self.running = False
# Unregister the callback thread from Redis
if self.callback_thread and self.redis:
try:
self.redis.unregister_callback(self.callback_thread)
self.callback_thread = None
except Exception:
# Ignore errors during shutdown (Redis might already be closed)
pass
if self.logfile:
self.logfile.close()
self.logfile = None
[docs]
def set_log_file_path(self, path):
"""
Set the path to the log file.
:param path: The path to the log file.
:type path: str
"""
with self.lock:
self.log_dir = os.path.normpath(path)
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
if self.logfile is not None:
self.logfile.close()
self.logfile = None
[docs]
def _handle_redis_log_message(self, message):
"""
Handle a message sent on the Redis stream.
If it surpasses the threshold, it will be printed to the terminal and written to the logfile (if enabled).
:param message: The message to handle.
:type message: SICLogMessage
"""
# default to INFO level if not set
level = getattr(message, 'level', logging.INFO)
# check if the level is greater than or equal to the threshold
if level >= self.threshold:
# outputs to terminal
try:
print(message.msg, end="\n")
except BrokenPipeError:
pass
if self.write_to_logfile:
# 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
"""
acquired = self.lock.acquire(timeout=0.5)
if not acquired:
return
try:
if self.log_dir is None:
# on remote devices the log_dir is set to None. We don't want to write to a logfile on remote devices
return
if self.logfile is None:
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
current_date = datetime.now().strftime("%Y-%m-%d")
log_path = os.path.join(self.log_dir, "sic_{current_date}.log".format(current_date=current_date))
self.logfile = open(log_path, "a")
# 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()
finally:
self.lock.release()
[docs]
class SICRedisLogHandler(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: SICRedisConnection
:param client_id: The client id of the device that is logging
:type client_id: str
"""
[docs]
def __init__(self, redis, client_id):
super(SICRedisLogHandler, 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:
if self.redis.stopping:
return # silently ignore messages if the application is stopping
# 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
log_message.level = record.levelno
# Send over Redis
self.redis.send_message(log_channel, log_message)
except Exception:
if not self.redis.stopping:
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, client_logger=False):
"""
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 SICRedisConnection object
:type redis: SICRedisConnection
:return: The logger.
:rtype: logging.Logger
"""
# logging initialisation
# Always set logger to DEBUG so it logs everything - SICClientLog will filter what to display
logger = logging.Logger(name)
logger.setLevel(DEBUG)
log_format = SICLogFormatter()
handler_redis = SICRedisLogHandler(redis, client_id)
handler_redis.setFormatter(log_format)
logger.addHandler(handler_redis)
if client_logger:
SIC_CLIENT_LOG.redis = redis
SIC_CLIENT_LOG.subscribe_to_redis_log(client_id)
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_CLIENT_LOG = SICClientLog()
[docs]
def set_log_level(level):
"""
Set the log level threshold for SICClientLog.
This filters which messages are displayed/written, but all messages are still logged.
:param level: The log level threshold (DEBUG, INFO, WARNING, ERROR, CRITICAL)
:type level: int
"""
SIC_CLIENT_LOG.threshold = level
[docs]
def set_log_file(path):
SIC_CLIENT_LOG.write_to_logfile = True
SIC_CLIENT_LOG.set_log_file_path(path)