#!/usr/bin/env python
# pylint: disable=line-too-long
"""
CLI - Print a log message in the console
Can use: python3 logtoconsole.py [message] -L [level] &>> /tmp/log.log
to append it to the log file (/tmp/log.log is an example)
"""
# Copyright 2023 by David Heurtevent.
# SPDX_LICENSE: MIT
# License: MIT License
# Author: David HEURTEVENT <david@heurtevent.org>
import argparse
import logging
import sys
from fruafr.log.lib import templating
from fruafr.log.lib import common
# Defaults
LEVEL = 'info'
SEP = ' - '
OPTSEP = SEP
[docs]
class Console(object):
"""Class Console
Parses the command line arguments
"""
[docs]
def parse_args(self, args) -> argparse.Namespace:
"""Parse the arguments from the command line
Args:
args (list): the list of arguments from the command line
Returns:
the argparse.Namespace object containing the parsed arguments
"""
parser = argparse.ArgumentParser(
prog='CLI - Print log message to the console\n',
description='Options after --nolevel modify the content of the message before it is logged.',
epilog='Can use: python3 logtoconsole.py [message] -L [level] &>> /tmp/log.log to append it to the log file (/tmp/log.log is an example)')
# arguments
parser.add_argument('message',
help='The message to log. Ignored if -MES or --message is specified (please set it to . to avoid error messsages)')
parser.add_argument('-L', '--level',
dest='level',
choices=['debug', 'info', 'warning', 'error', 'critical'],
default=f"{LEVEL}",
help=f"The level [debug, info, warning, error, critical]. [Default: {LEVEL}]")
parser.add_argument('-s', '--sep',
dest='sep',
help=f"Separator [Default: {SEP}]",
default=f"{SEP}")
parser.add_argument('-f', '--format',
dest='format',
help='Python logging format')
parser.add_argument('-df', '--dateformat',
dest='dateformat',
help='Datetime format')
# Additional flags
parser.add_argument('--noasctime',
dest='noasctime',
action='store_true',
default=False,
help='Removes asctime')
parser.add_argument('--nolevel',
dest='nolevel',
action='store_true',
default=False,
help='Removes level')
# Additional arguments that can be passed to append to the message beforehand
parser.add_argument('-o', '--options',
dest='options',
help='order of optional fields to template (comma separated string). Values not in this option are ignored even when provided to the cli')
parser.add_argument('-osep', '--optsep',
dest='optsep',
help=f"Separator for the additional options [Default: {OPTSEP}]",
default=f"{OPTSEP}")
for option in common.CLI_OPTIONS.items():
parser.add_argument(
option[1]['short_option'],
option[1]['long_option'],
dest=option[1]['dest'],
help=option[1]['help'],
default=option[1]['default']
)
# return
return parser.parse_args(args)
def _prepare_fmt(self, args: argparse.Namespace) -> str:
"""Prepares the format string from the CLI arguments
Args:
args(argparse.Namespace): The CLI arguments
Returns:
str: The format string
"""
# format if not provided
if args.format is None:
if args.noasctime:
fmt = []
else:
fmt = ['%(asctime)s']
if not args.nolevel:
fmt.append('%(levelname)s')
fmt.append('%(message)s')
output = args.sep.join(fmt)
else:
output = args.format
#return formatted string
return output
def _prepare_date_format(self, args: argparse.Namespace) -> str:
"""Prepares the date format string from the CLI arguments
Args:
args(argparse.Namespace): The CLI arguments
Returns:
str: The date format string
"""
# date time format if not provided
if args.dateformat is None:
return None
else:
return args.dateformat
def _prepare_level(self, args) -> int:
"""Prepares the date format string from the CLI arguments
Args:
args(argparse.Namespace): The CLI arguments
Returns:
int: The level integer
"""
levelint = -1
accepted_levels = ['debug', 'info', 'warning', 'error', 'critical']
# reject if not an accepted level
if args.level not in accepted_levels:
print(f"LEVEL {args.level} is not recognized", file=sys.stderr)
sys.exit(1)
# format the output
if args.level == 'info':
levelint = logging.INFO
elif args.level == 'debug':
levelint = logging.DEBUG
elif args.level == 'warning':
levelint = logging.WARNING
elif args.level == 'error':
levelint = logging.ERROR
else:
levelint = logging.CRITICAL
# return
return levelint
def _prepare_console_logger(self, fmt: str, datefmt: str) -> logging.Logger:
"""Prepares the console logger
Args:
fmt (str): the template format
datefmt (str): the date format
Return:
loggign.Logger: The logger instance
"""
# get the root logger
logger = logging.getLogger('')
# set the level of the root logger
logger.setLevel(logging.DEBUG)
# set the formatter
formatter = logging.Formatter(fmt, datefmt)
# set up logging to console
console = logging.StreamHandler()
# set logging level to lowest
console.setLevel(logging.DEBUG)
# set the format
console.setFormatter(formatter)
# add the handler to the root logger
logger.addHandler(console)
# return the root logger with the console attached to it
return logger
def _determine_list_options_to_template(self, args: argparse.Namespace) -> list:
"""Determine the list of options provided to template (in the message section of the log record)
Args:
args(argparse.Namespace): The CLI arguments
Returns:
list: The list of options provided to template (in the message section of the log record)
"""
# list additional arguments set
arguments_set = []
for option in common.CLI_OPTIONS.keys():
if hasattr(args, option):
if getattr(args, option) != 'None' and getattr(args, option) is not None:
arguments_set.append(option)
return arguments_set
def _validate_list_options_to_template(self, options: str):
"""Validates the list of options to template
Args:
options (str): the list of options to validate (should be a string representing a comma\
separated list)
Returns:
list: if valid, the list if valid, otherwise raises ValueError.
appends message to the list provided if not included in the list
"""
# the options list must be a comma separated list
try:
orders = options.split(',')
except AttributeError:
print("order must be a comma(,) separated list", file=sys.stderr)
sys.exit(1)
# the options string must contain valid options
for order in orders:
# if not a valid option and message is an exception
if order not in common.CLI_OPTIONS.keys() and order != 'message':
k = ', '.join(common.CLI_OPTIONS.keys())
print(f"OPTION no found. Must contain one or several options from: {k}", file=sys.stderr)
sys.exit(1)
# if message not found in order, add
if 'message' not in orders:
orders.append('message')
#if valid, return order
return orders
def _prepare_variables_to_render(self, args: argparse.Namespace) -> dict:
"""Prepare variables to be rendered with the template
Args:
args(argparse.Namespace): The CLI arguments
Returns:
dict: The variables to be rendered
"""
# elements to template
elements_to_template = {}
for elem in common.CLI_OPTIONS.keys():
if hasattr(args, elem):
if getattr(args, elem) is not None:
elements_to_template[elem] = getattr(args, elem)
#add message
elements_to_template['message'] = getattr(args, 'message')
return elements_to_template
def _apply_template(self, options: list, variables: dict, separator: str):
"""Apply the template to the options with the given variables
Args:
options(list): list of options to template
variables(dict): dict of variables to apply the template to {option: variable}
separator(str): string to separate the options with in the template
Returns:
str : the result of applying the template.
Otherwise, raises ValueError
"""
# prepare the template
t = templating.Templating.create_template(options, separator)
# validate the template
validate = templating.Templating.validate_template(t, options)
if validate is not None:
raise ValueError(f"order must contain {validate}")
# apply the template
try:
message = templating.Templating.apply_template(t, variables)
return message
except KeyError:
print (f"Could not apply the template {t} with the variables provided\
{variables}", file=sys.stderr)
sys.exit(1)
[docs]
def process(self, args: argparse.Namespace):
"""Process the command line arguments
Args:
args (argparser.Namespace): Command line arguments
"""
# determine the list of command line options to template in the message part
# of the log record
if args.options is None:
options = self._determine_list_options_to_template(args)
else:
options = self._validate_list_options_to_template(args.options)
# prepate the variables
if options is not None and options != []:
variables = self._prepare_variables_to_render(args)
message= self._apply_template(options, variables, args.optsep)
else:
message = args.message
# determine the format
fmt = self._prepare_fmt(args)
# determine the date format
date_format = self._prepare_date_format(args)
# determine the levelint
levelint = self._prepare_level(args)
# create the logger and obtain it
logger = self._prepare_console_logger(fmt, date_format)
# log the message
logger.log(levelint, message)
[docs]
def main():
"""Main : CLI logic"""
# parse arguments
args = Console().parse_args(sys.argv[1:])
# process arguments
Console().process(args)
if __name__ == "__main__":
main()