Initial commit
This commit is contained in:
commit
3c11dfcf8d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__pycache__/*
|
50
README.md
Normal file
50
README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Common Logger
|
||||
|
||||
A Python library to allow for logging consistently across multiple scripts to ensure logs are stored in a known location. Includes daily rotation and deletion of stale log files.
|
||||
|
||||
## Python usage
|
||||
Example:
|
||||
```python
|
||||
import common_logger
|
||||
|
||||
logging = common_logger.Logger(level=common_logger.DEBUG)
|
||||
|
||||
logging.debug('Could not find the %s.', 'cat')
|
||||
logging.info('Hello World!')
|
||||
logging.warn('Be careful!')
|
||||
logging.error('Something terrible has happened')
|
||||
```
|
||||
|
||||
```plain
|
||||
$ cat /srv/log/example.log
|
||||
2021-03-22 16:19:34 +0000 DEBUG example.py: Could not find the cat.
|
||||
2021-03-22 16:19:34 +0000 INFO example.py: Hello World!
|
||||
2021-03-22 16:19:34 +0000 WARNING example.py: Be careful!
|
||||
2021-03-22 16:19:34 +0000 ERROR example.py: Something terrible has happened
|
||||
```
|
||||
|
||||
## Command line usage
|
||||
```plain
|
||||
usage: common_logger.py [-h] [-d | -i | -w | -e] [-n NAME] ...
|
||||
|
||||
positional arguments:
|
||||
message message to log. Reads STDIN if not provided.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-d, --debug log message at level DEBUG
|
||||
-i, --info log message at level INFO
|
||||
-w, --warning log message at level WARNING
|
||||
-e, --error log message at level ERROR
|
||||
-n NAME, --name NAME basename of the log file to write to
|
||||
```
|
||||
|
||||
Example:
|
||||
```shell
|
||||
echo 'Hello world!' | ./common_logger.py -n example
|
||||
```
|
||||
|
||||
```plain
|
||||
$ cat /srv/log/example.log
|
||||
2021-03-22 16:15:10 +0000 INFO example: Hello world!
|
||||
```
|
308
common_logger.py
Normal file
308
common_logger.py
Normal file
|
@ -0,0 +1,308 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common logging for Python scripts and CLI programs.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import inspect
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os.path
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import dateutil.tz
|
||||
import psutil
|
||||
import pytz
|
||||
|
||||
DEBUG = logging.DEBUG
|
||||
INFO = logging.INFO
|
||||
WARNING = logging.WARNING
|
||||
ERROR = logging.ERROR
|
||||
CRITICAL = logging.CRITICAL
|
||||
FATAL = logging.FATAL
|
||||
|
||||
# FIXME: Come up with a better method for determining the OS-specific paths, etc.
|
||||
# Particularly the linux/linux2 thing.
|
||||
PLATFORM_DEFS = {
|
||||
'darwin': {
|
||||
'socket': '/var/run/syslog',
|
||||
'logdir': os.path.expanduser('~/Library/Logs'),
|
||||
},
|
||||
# Python2
|
||||
'linux2': {
|
||||
'socket': '/dev/log',
|
||||
'logdir': '/srv/log',
|
||||
},
|
||||
# Python3
|
||||
'linux': {
|
||||
'socket': '/dev/log',
|
||||
'logdir': '/srv/log',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class UcFormatter(logging.Formatter):
|
||||
"""
|
||||
Class for formatting the date & time correctly when logging
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _get_local_tz_str():
|
||||
"""
|
||||
Method to fetch the correct location-string for the local timezone
|
||||
|
||||
e.g. returns "Europe/London"
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
return '/'.join(
|
||||
os.path.realpath(dateutil.tz.gettz()._filename).split('/')[-2:]
|
||||
) # pyright: reportGeneralTypeIssues=false
|
||||
|
||||
def converter(self, timestamp):
|
||||
"""
|
||||
Method to add the local timezone to the the provided timestamp
|
||||
"""
|
||||
tsdt = datetime.datetime.fromtimestamp(timestamp)
|
||||
tzinfo = pytz.timezone(self._get_local_tz_str())
|
||||
|
||||
return tzinfo.localize(tsdt)
|
||||
|
||||
def formatTime(self, record, datefmt=None):
|
||||
"""
|
||||
Method to format the timestamp for the log record
|
||||
"""
|
||||
recdt = self.converter(record.created)
|
||||
if datefmt:
|
||||
return recdt.strftime(datefmt)
|
||||
else:
|
||||
try:
|
||||
return recdt.isoformat(timespec='seconds')
|
||||
except TypeError:
|
||||
return recdt.isoformat()
|
||||
|
||||
|
||||
class Logger(object):
|
||||
"""
|
||||
Class for implementing a consistent logging format and location
|
||||
"""
|
||||
|
||||
source = None
|
||||
enable_logfile = True
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
logname=None,
|
||||
level=INFO,
|
||||
enable_logfile=True,
|
||||
syslog_facility=logging.handlers.SysLogHandler.LOG_USER,
|
||||
logpath=PLATFORM_DEFS[sys.platform]['logdir'],
|
||||
):
|
||||
self.enable_logfile = enable_logfile
|
||||
|
||||
# Configure logging basics
|
||||
logger = logging.getLogger()
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
}
|
||||
)
|
||||
logger.setLevel(level)
|
||||
|
||||
# Add stream handler
|
||||
stream_handler = logging.StreamHandler()
|
||||
logger.addHandler(stream_handler)
|
||||
|
||||
if logname:
|
||||
self.source = logname
|
||||
else:
|
||||
self._get_source()
|
||||
|
||||
# Set log formatting
|
||||
basefmt = '%(levelname)s {}: %(message)s'.format(self.source)
|
||||
local_fmt = UcFormatter(
|
||||
fmt='%(asctime)s {}'.format(basefmt),
|
||||
datefmt='%Y-%m-%d %H:%M:%S %z',
|
||||
)
|
||||
syslog_fmt = logging.Formatter(fmt=basefmt)
|
||||
|
||||
# Set formatter for StreamHandler
|
||||
stream_handler.setFormatter(local_fmt)
|
||||
|
||||
if self.enable_logfile:
|
||||
# Set file handler
|
||||
try:
|
||||
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
os.path.join(
|
||||
logpath,
|
||||
'{}.log'.format(os.path.splitext(self.source)[0]),
|
||||
),
|
||||
when='midnight',
|
||||
backupCount=90,
|
||||
)
|
||||
except IOError as error:
|
||||
logging.warning('FileHandler: %s', error)
|
||||
else:
|
||||
# Configure main logger with file handler
|
||||
file_handler.setFormatter(local_fmt)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
try:
|
||||
syslog_handler = logging.handlers.SysLogHandler(
|
||||
address=PLATFORM_DEFS[sys.platform]['socket'],
|
||||
facility=syslog_facility,
|
||||
)
|
||||
except socket.error as error:
|
||||
logging.warning(
|
||||
'SyslogHandler: %s: %s',
|
||||
error,
|
||||
PLATFORM_DEFS[sys.platform]['socket'],
|
||||
)
|
||||
else:
|
||||
syslog_handler.setFormatter(syslog_fmt)
|
||||
logger.addHandler(syslog_handler)
|
||||
|
||||
self.logger = logger
|
||||
|
||||
def _get_source(self):
|
||||
"""
|
||||
Internal method to determine the calling script.
|
||||
|
||||
Uses stack inspection and the OS process list.
|
||||
"""
|
||||
if __name__ == '__main__':
|
||||
# Called as a command
|
||||
# Get parent process's file
|
||||
try:
|
||||
open_files = psutil.Process().parent().open_files()
|
||||
except psutil.AccessDenied:
|
||||
open_files = []
|
||||
|
||||
if open_files:
|
||||
self.source = os.path.basename(open_files[0].path)
|
||||
else:
|
||||
# Being called directly. No logfile.
|
||||
logging.warning(
|
||||
'Unable to determine calling script. Not writing to disk.'
|
||||
)
|
||||
self.enable_logfile = False
|
||||
self.source = '%(module)s'
|
||||
else:
|
||||
# Called as a Python module
|
||||
self.source = os.path.basename(
|
||||
inspect.getframeinfo(inspect.stack()[-1][0]).filename
|
||||
)
|
||||
|
||||
# Override built-in method
|
||||
# pylint: disable=invalid-name
|
||||
def setLevel(self, level):
|
||||
"""
|
||||
Method to set the logging level
|
||||
"""
|
||||
self.logger.setLevel(level)
|
||||
|
||||
# FIXME: Somehow remove the redundancy below (functools.partial?)
|
||||
def debug(self, *args, **kwargs):
|
||||
"""
|
||||
Method to log a debug level message.
|
||||
"""
|
||||
self.logger.debug(*args, **kwargs)
|
||||
|
||||
def info(self, *args, **kwargs):
|
||||
"""
|
||||
Method to log an info level message.
|
||||
"""
|
||||
self.logger.info(*args, **kwargs)
|
||||
|
||||
def warning(self, *args, **kwargs):
|
||||
"""
|
||||
Method to log a warning level message.
|
||||
"""
|
||||
self.logger.warning(*args, **kwargs)
|
||||
|
||||
def error(self, *args, **kwargs):
|
||||
"""
|
||||
Method to log an error level message.
|
||||
"""
|
||||
self.logger.error(*args, **kwargs)
|
||||
|
||||
def critical(self, *args, **kwargs):
|
||||
"""
|
||||
Method to log an critical level message.
|
||||
"""
|
||||
self.logger.critical(*args, **kwargs)
|
||||
|
||||
# Alias some of the methods
|
||||
warn = warning
|
||||
fatal = critical
|
||||
exception = error
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Function to parse the CLI arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
for level in ['debug', 'info', 'warning', 'error']:
|
||||
group.add_argument(
|
||||
'-{}'.format(level[0]),
|
||||
'--{}'.format(level),
|
||||
action='store_true',
|
||||
help='log message at level {}'.format(level.upper()),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--name',
|
||||
nargs=1,
|
||||
default=[None],
|
||||
help='basename of the log file to write to',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'message',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='message to log. Reads STDIN if not provided.',
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entrypoint for the CLI
|
||||
"""
|
||||
args = parse_args()
|
||||
|
||||
logger = Logger(args.name[0])
|
||||
|
||||
if args.debug:
|
||||
log = logger.debug
|
||||
elif args.info:
|
||||
log = logger.info
|
||||
elif args.warning:
|
||||
log = logger.warning
|
||||
elif args.error:
|
||||
log = logger.error
|
||||
else:
|
||||
# Default to INFO
|
||||
log = logger.info
|
||||
|
||||
if args.message:
|
||||
log(' '.join(args.message))
|
||||
|
||||
# Check if we have data from stdin
|
||||
if not sys.stdin.isatty():
|
||||
data = sys.stdin.read().strip()
|
||||
if data:
|
||||
for line in data.split('\n'):
|
||||
log(line)
|
||||
|
||||
main()
|
Loading…
Reference in a new issue