Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a task base class and basic logger #1160

Merged
merged 10 commits into from
Dec 8, 2022
176 changes: 176 additions & 0 deletions ush/python/pygw/src/pygw/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""
Logger
"""

import sys
from pathlib import Path
from typing import Union, List
import logging


class ColoredFormatter(logging.Formatter):
"""
Logging colored formatter
adapted from https://stackoverflow.com/a/56944256/3638629
"""

grey = '\x1b[38;21m'
blue = '\x1b[38;5;39m'
yellow = '\x1b[38;5;226m'
red = '\x1b[38;5;196m'
bold_red = '\x1b[31;1m'
reset = '\x1b[0m'

def __init__(self, fmt):
super().__init__()
self.fmt = fmt
self.formats = {
logging.DEBUG: self.blue + self.fmt + self.reset,
logging.INFO: self.grey + self.fmt + self.reset,
logging.WARNING: self.yellow + self.fmt + self.reset,
logging.ERROR: self.red + self.fmt + self.reset,
logging.CRITICAL: self.bold_red + self.fmt + self.reset
}

def format(self, record):
log_fmt = self.formats.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)


class Logger:
"""
Improved logging
"""
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
DEFAULT_LEVEL = 'INFO'
DEFAULT_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
def __init__(self, name: str = None,
level: str = DEFAULT_LEVEL,
_format: str = DEFAULT_FORMAT,
colored_log: bool = False,
logfile_path: Union[str, Path] = None):
"""
Initialize Logger

Parameters
----------
name : str
Name of the Logger object
default : None
level : str
Desired Logging level
default : 'INFO'
_format : str
Desired Logging Format
default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
colored_log : bool
Use colored logging for stdout
default: False
logfile_path : str or Path
Path for logging to a file
default : None
"""

self.name = name
self.level = level.upper()
self.format = _format
self.colored_log = colored_log

if self.level not in Logger.LOG_LEVELS:
raise LookupError('{self.level} is unknown logging level\n' +
'Currently supported log levels are:\n' +
f'{" | ".join(Logger.LOG_LEVELS)}')

# Initialize the root logger if no name is present
self._logger = logging.getLogger(name) if name else logging.getLogger()

self._logger.setLevel(self.level)

_handlers = []
# Add console handler for logger
_handler = Logger.add_stream_handler(
level=self.level,
_format=self.format,
colored_log=self.colored_log,
)
_handlers.append(_handler)
self._logger.addHandler(_handler)

# Add file handler for logger
if logfile_path is not None:
_handler = Logger.add_file_handler(logfile_path, level=self.level, _format=self.format)
self._logger.addHandler(_handler)
_handlers.append(_handler)

def __getattr__(self, attribute):
"""
Allows calling logging module methods directly
"""
return getattr(self._logger, attribute)

def get_logger(self):
"""
Return the logging object

Returns
-------
logger : Logger object
"""
return self._logger

@classmethod
def add_handlers(cls, logger: logging.Logger, handlers: List[logging.Handler]):
"""
Add a list of handlers to a logger

Parameters
aerorahul marked this conversation as resolved.
Show resolved Hide resolved
----------
logger
handlers

Returns
-------
logger
"""
for handler in handlers:
logger.addHandler(handler)

return logger

@classmethod
def add_stream_handler(cls, level: str = DEFAULT_LEVEL,
_format: str = DEFAULT_FORMAT,
colored_log: bool = False):
"""
Create stream handler
This classmethod will allow setting a custom stream handler on children
aerorahul marked this conversation as resolved.
Show resolved Hide resolved
"""

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
_format = ColoredFormatter(_format) if colored_log else logging.Formatter(_format)
handler.setFormatter(_format)

return handler

@classmethod
def add_file_handler(cls, logfile_path: Union[str, Path],
level: str = DEFAULT_LEVEL,
_format: str = DEFAULT_FORMAT):
"""
Create file handler.
This classmethod will allow setting custom file handler on children
aerorahul marked this conversation as resolved.
Show resolved Hide resolved
"""

logfile_path = Path(logfile_path)

# Create the directory containing the logfile_path
if not logfile_path.parent.is_dir():
logfile_path.mkdir(parents=True, exist_ok=True)

handler = logging.FileHandler(str(logfile_path))
handler.setLevel(level)
handler.setFormatter(logging.Formatter(_format))

return handler
45 changes: 45 additions & 0 deletions ush/python/pygw/src/pygw/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class Task:
"""
Base class for all tasks
"""

def __init__(self, config, *args, **kwargs):
"""
Every task needs a config.
Additional arguments (or key-value arguments) can be provided.

Parameters
----------
config : Dict
dictionary object containing task configuration

*args : tuple
Additional arguments to `Task`

**kwargs : dict, optional
Extra keyword arguments to `Task`
"""

# Store the config and arguments as attributes of the object
self.config = config

for arg in args:
setattr(self, str(arg), arg)

for key, value in kwargs.items():
setattr(self, key, value)

def initialize(self):
aerorahul marked this conversation as resolved.
Show resolved Hide resolved
pass

def configure(self):
pass

def execute(self):
pass

def finalize(self):
pass

def clean(self):
pass
42 changes: 42 additions & 0 deletions ush/python/pygw/src/tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pygw.logger import Logger

level = 'debug'
number_of_log_msgs = 5
reference = {'debug': "Logging test has started",
'info': "Logging to 'logger.log' in the script dir",
'warning': "This is my last warning, take heed",
'error': "This is an error",
'critical': "He's dead, She's dead. They are all dead!"}


def test_logger(tmp_path):
"""Test log file"""

logfile = tmp_path / "logger.log"

try:
log = Logger('test_logger', level=level, logfile_path=logfile, colored_log=True)
log.debug(reference['debug'])
log.info(reference['info'])
log.warning(reference['warning'])
log.error(reference['error'])
log.critical(reference['critical'])
except Exception as e:
raise AssertionError(f'logging failed as {e}')

# Make sure log to file created messages
try:
with open(logfile, 'r') as fh:
log_msgs = fh.readlines()
except Exception as e:
raise AssertionError(f'failed reading log file as {e}')

# Ensure number of messages are same
log_msgs_in_logfile = len(log_msgs)
assert log_msgs_in_logfile == number_of_log_msgs

# Ensure messages themselves are same
for count, line in enumerate(log_msgs):
lev = line.split('-')[3].strip().lower()
message = line.split(':')[-1].strip()
assert reference[lev] == message