Skip to content
Snippets Groups Projects
Commit e01cfc16 authored by Tamino Huxohl's avatar Tamino Huxohl
Browse files

document logging module

parent fad21501
No related branches found
No related tags found
No related merge requests found
......@@ -11,7 +11,9 @@ import os
import shutil
from typing import Dict, Optional, List
date_format="%m/%d/%Y %I:%M:%S %p"
# default date format string
date_format = "%m/%d/%Y %I:%M:%S %p"
# default formatter for log messages
FORMATTER = Formatter(
fmt="%(asctime)s - %(levelname)7s - %(name)s - %(message)s", datefmt=date_format
)
......@@ -20,7 +22,20 @@ FORMATTER = Formatter(
def add_logging_args(parser: argparse.ArgumentParser, defaults: Dict[str, str]):
"""
Add logging arguments to an argument parser. This includes parameters for a
filename logged to and the log level.
filename and the log level.
Parameters
----------
parser: argparse.ArgumentParser
the parser to which arguments are added
defaults: Dict[str, str]
default values for the arguments
use the keys `--logfile` or `--logleve` to specify the defaults
Returns
-------
argparse.ArgumentParser
the modified parser
"""
parser.add_argument(
"--logfile",
......@@ -38,24 +53,69 @@ def add_logging_args(parser: argparse.ArgumentParser, defaults: Dict[str, str]):
return parser
def timestamp_filename(filename: str):
def timestamp_filename(filename: str) -> str:
"""
Attach a timestamp to a filename.
The timestamp is attached as a postfix to the filename.
E.g., `img.png` becomes `img_2023-02-20-12:59:53.png`.
Parameters
----------
filename: str
Returns
-------
str
"""
timestamp = datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
basename, ext = os.path.splitext(filename)
return f"{basename}_{timestamp}{ext}"
def rotate_log_file(filename: str):
"""
Rotate a log file.
This means that the given file is moved to a filename
with a timestamp (see `timestamp_filename`).
Parameters
----------
filename: str
the log file to be rotated
"""
if not os.path.isfile(filename):
return
shutil.move(filename, timestamp_filename(filename))
def get_logger(logfile: Optional[str] = None, loglevel: Optional[str] = None, name: Optional[str] = None):
def get_logger(
logfile: Optional[str] = None,
loglevel: Optional[str] = None,
name: Optional[str] = None,
) -> logging.Logger:
"""
Create a new logger.
If there is already a logger with the given name, it will
be returned instead of creating a new one.
Parameters
----------
logfile: str, optional
loglevel: str, otional
name: str
Returns
-------
logging.Logger
"""
logger = getLogger() if name is None else getLogger(name)
if logger.hasHandlers():
# logger alrady exists, so refrain from adding more handlers
# logger already exists, so refrain from adding more handlers
return logger
if loglevel:
......@@ -75,23 +135,65 @@ def get_logger(logfile: Optional[str] = None, loglevel: Optional[str] = None, na
return logger
def get_logger_by_args(args):
def get_logger_by_args(args: argparse.Namespace) -> logging.Logger:
"""
Utility function to create a logger from arguments added
with `add_logging_args`.
Parameters
----------
args: argparse.Namespace
Returns
-------
logging.Logger
"""
return get_logger(args.logfile, args.loglevel)
@dataclass
class LogLine:
"""
Data class which represents the different parts of a single
line in a log.
"""
time: datetime
loglevel: str
message: str
def __repr__(self):
return f"{self.time.strftime(date_format)} - {self.loglevel:>7} - {self.message}"
return (
f"{self.time.strftime(date_format)} - {self.loglevel:>7} - {self.message}"
)
def parse_line(logline, start_idx_message=3):
def parse_line(logline: str, start_idx_message: int = 3) -> LogLine:
"""
Parse a single line of a log into a structured LogLine object.
Elements in a line are expected to be separated by the `-` character.
The first element is the date time, the second the log level, the third
is the logger name and the last is the message.
Note that the third element is optional and that the message may
contain additional `-` characters. This can be handled using the
`start_idx_message` parameter.
Parameters
----------
logline: str
line of a log
start_idx_message: int, optional
index at which split of the ling the message starts
Returns
-------
LogLine
"""
_split = logline.strip().split("-")
assert len(_split) >= start_idx_message, f"A logged line should consists of a least {start_idx_message} elements with the format [TIME - LOGLEVEL - ... - MESSAGE] but got [{logline.strip()}]"
assert (
len(_split) >= start_idx_message
), f"A logged line should consists of a least {start_idx_message} elements with the format [TIME - LOGLEVEL - ... - MESSAGE] but got [{logline.strip()}]"
time_str = _split[0].strip()
time = datetime.strptime(time_str, date_format)
......@@ -101,12 +203,27 @@ def parse_line(logline, start_idx_message=3):
message = "-".join(_split[start_idx_message:]).strip()
return LogLine(time=time, loglevel=loglevel, message=message)
def parse_file(logfile: str, start_idx_message: int = 3) -> List[LogLine]:
"""
Parse a logfile into a list of `LogLine`.
Parameters
----------
logfile: str
start_idx_message: int, optional
see `parse_line`
Returns
-------
list of LogLine
"""
with open(logfile, mode="r") as f:
lines = f.readlines()
lines = map(lambda line: parse_line(line, start_idx_message), lines)
return list(lines)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
add_logging_args(parser, defaults={"--loglevel": "DEBUG", "--logfile": "tmp.log"})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment