Skip to content

Commit

Permalink
Monitoring friendly logging
Browse files Browse the repository at this point in the history
  • Loading branch information
sol1-matt committed Nov 1, 2023
1 parent a1a28af commit 7921e28
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 9 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ It has been designed so you can add multiple tests and the class will intelligen

__Documentation__
You can find documentation in the [`docs`](./docs/monitoring_plugins.md) folder.
Code examples can be found in the [`examples`](./examples/check_day_of_the_week.py) folder.
Code usage examples can be found in the [`examples`](./examples/check_day_of_the_week.py) folder.

__Maturity__: Stable.

## Logging
```python
from sol1_monitoring_plugins_lib import initLogging, initLoggingArgparse, DEFAULT_LOG_LEVELS
```

The Logging functions setup loguru based logging for monitoring plugins with settings that allow for simple debugging during development and a short history for production usage by default.

__Documentation__
You can find documentation in the [`docs`](./docs/logging.md) folder.
Code usage examples can be found in the [`examples`](./examples/check_day_of_the_week.py) folder.

__Maturity__: Stable.

Expand All @@ -37,7 +50,3 @@ Run tests
```
python3 -m pytest tests/
```
## Deploy
```
python3 -m twine upload dist/*
```
74 changes: 74 additions & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Monitoring Logging
This library provides utilities for initializing logging that is check friendly and simple to deploy and manage, the logger used is loguru.

## Constants
`DEFAULT_LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL']`: List of valid logging levels used to validate logging level supplied. Append to this last and pass it to initization if you add custom logging levels.

## Functions
### initLogging()
Initization of the logging with sensible defaults and output format for monitoring checks.

The function performs the following tasks:

1. Removes existing loggers.
1. Adds a screen logger to standard error if `enable_screen_debug` is `True`.
1. Adds a file logger with rotation and retention policies if `enable_log_file` is `True`.
1. Logs an initialization message with the final configuration if `log_level` is `DEBUG`.

Log retention is short and log level is `WARNING` as by default so only problems are logged and they aren't kept for long.

```python
initLogging(debug=False,
enable_screen_debug=False,
enable_log_file=True,
log_file='/var/log/icinga2/check_monitoring.log',
log_rotate='1 day',
log_retention='3 days',
log_level='WARNING',
available_log_levels=DEFAULT_LOG_LEVELS,
**kwargs)
```
__Parameters:__
`debug` (optional): If True, sets the log level to `DEBUG`. Defaults to `False`.
`enable_screen_debug` (optional): If True, enables screen logging to standard error. Defaults to `False`.
`enable_log_file` (optional): If True, enables file logging. Defaults to `True`.
`log_file` (optional): The path to the log file. Defaults to `/var/log/icinga2/check_monitoring.log`.
`log_rotate` (optional): The log file rotation policy. Defaults to `1 day`.
`log_retention` (optional): The log file retention policy. Defaults to `3 days`.
`log_level` (optional): The logging level. Defaults to `WARNING`.
`available_log_levels` (optional): A list of available logging levels. Defaults to `DEFAULT_LOG_LEVELS`.
`**kwargs` : Legacy variables to override function arguments if they exist.


## initLoggingArgparse()
Adds Argparse arguments to be passed to `initLogging()`

```python
initLoggingArgparse(parser,
log_file='/var/log/icinga2/check_monitoring.log',
log_rotate='1 day',
log_retention='3 days',
log_level='WARNING',
available_log_levels=DEFAULT_LOG_LEVELS)
```

__Parameters:__
`log_file` (optional): The path to the log file. Defaults to `/var/log/icinga2/check_monitoring.log`.
`log_rotate` (optional): The log file rotation policy. Defaults to `1 day`.
`log_retention` (optional): The log file retention policy. Defaults to `3 days`.
`log_level` (optional): The logging level. Defaults to `WARNING`.
`available_log_levels` (optional): A list of available logging levels. Defaults to `DEFAULT_LOG_LEVELS`.

__Argparse Arguments Added:__
Flags
`--debug`
`--enable-screen-debug`
`--disable-log-file`
_Note: `--disable-log-file` should get inversly passed to the `enable_log_file`, this is done so the user is explictly disabling the log file, the opposite of the default which is enabled._

Keyword arguments
`--log-file`
`--log-rotate`
`--log-retention`
`--log-level`
_Note: there is no argument `--available-log-levels` added to argparse, the avaiable log levels are only used to provide choices for `--log-level`._
19 changes: 16 additions & 3 deletions examples/check_day_of_the_week.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@
from loguru import logger
from datetime import datetime

from sol1_monitoring_plugins_lib import MonitoringPlugin
from sol1_monitoring_plugins_lib import MonitoringPlugin, initLogging, initLoggingArgparse

# it's nice to put the args at the top of the file as it is commonly referenced


def getArgs():
parser = argparse.ArgumentParser(description='Check the day of the week.')
parser.add_argument('--day', type=str, help='Day of the week', required=True)
# Adding the logging arguments to argparse
initLoggingArgparse(parser=parser)
return parser.parse_args()


if __name__ == "__main__":
args = getArgs()
# Initalize the logging with the arguments from argparse
initLogging(debug=args.debug,
enable_screen_debug=args.enable_screen_debug,
enable_log_file=not args.disable_log_file,
log_file=args.log_file,
log_rotate=args.log_rotate,
log_retention=args.log_retention,
log_level=args.log_level,
available_log_levels=args.available_log_levels
)

# initalze the plugin
plugin = MonitoringPlugin("Day of the week")
Expand Down Expand Up @@ -47,7 +61,7 @@ def getArgs():
plugin.setMessage(f"Day \"{args.day.capitalize()}\" isn't even a real day\n", plugin.STATE_CRITICAL, True)
# instead of complex and deep if else statements once we reach a show stopper problem we just exit
plugin.exit()

target_day_num = day_to_number.get(args.day.capitalize())

days_until_target = (target_day_num - datetime.now().isoweekday() + 7) % 7
Expand All @@ -56,7 +70,6 @@ def getArgs():
if days_until_target == 1:
state = plugin.STATE_WARNING
plugin.setMessage(f"Day {args.day.capitalize()} is {days_until_target} day(s) away", state, True)


# catch all for exiting
plugin.exit()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

setup(
name="sol1-monitoring-plugins-lib",
version="1.0.1",
version="1.1.0",
author='Matthew Smith',
author_email='matthew.smith@sol1.com.au',
description='Simple Library to manage the output for Nagios and Icinga Monitoring Plugins',
Expand Down
1 change: 1 addition & 0 deletions src/sol1_monitoring_plugins_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .monitoring_plugins import MonitoringPlugin
from .logging import initLogging, initLoggingArgparse, DEFAULT_LOG_LEVELS
107 changes: 107 additions & 0 deletions src/sol1_monitoring_plugins_lib/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@

import os
import sys
from loguru import logger

DEFAULT_LOG_LEVELS = ['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL']


def initLoggingArgparse(parser,
log_file='/var/log/icinga2/check_monitoring.log',
log_rotate='1 day',
log_retention='3 days',
log_level='WARNING',
available_log_levels=DEFAULT_LOG_LEVELS,
):
"""
Initalize argparse arguments for logging, you can change the argparse argument defaults with the function arguments
Args:
parser (obj): Argparse parser object
log_file (str, optional): Override default argument value for --log-file. Defaults to '/var/log/icinga2/check_monitoring.log'.
log_rotate (str, optional): Override default argument value for --log-rotate. Defaults to '1 day'.
log_retention (str, optional): Override default argument value for --log-retention. Defaults to '3 days'.
log_level (str, optional): Override default argument value for --log-level. Defaults to 'WARNING'.
available_log_levels (list, optional): Override default argument value for --available-log-levels. Defaults to DEFAULT_LOG_LEVELS.
"""
parser.add_argument('--debug', action="store_true", help="Sets the log level to DEBUG.")
parser.add_argument('--enable-screen-debug', action="store_true", help="Enables screen logging to standard error.")
parser.add_argument('--disable-log-file', action="store_true", help="Disables file logging")
parser.add_argument('--log-file', type=str, default=log_file, help="The path to the log file")
parser.add_argument('--log-rotate', type=str, default=log_rotate, help="The log file rotation policy")
parser.add_argument('--log-retention', type=str, default=log_retention, help="The log file retention policy")
parser.add_argument('--log-level', type=str, choices=available_log_levels,
default=log_level, help="The logging level")


def initLogging(debug=False,
enable_screen_debug=False,
enable_log_file=True,
log_file='/var/log/icinga2/check_monitoring.log',
log_rotate='1 day',
log_retention='3 days',
log_level='WARNING',
available_log_levels=DEFAULT_LOG_LEVELS,
**kwargs
):
"""
Initalize logging for monitoring checks.
It defaults to logging to file for WARNING level and above, the log files autorotate with compression and retention.
The log format includes the date and process id so you can identify all log entries from the same run.
Args:
debug (bool, optional): If True, sets the log level to DEBUG. Defaults to False.
enable_screen_debug (bool, optional): If True, enables screen logging to standard error. Defaults to False.
enable_log_file (bool, optional): If True, enables file logging. Defaults to True.
log_file (str, optional): The path to the log file. Defaults to '/var/log/icinga2/check_monitoring.log'.
log_rotate (str, optional): The log file rotation policy. Defaults to '1 day'.
log_retention (str, optional): The log file retention policy. Defaults to '3 days'.
log_level (str, optional): The logging level. Defaults to 'WARNING'.
available_log_levels (list, optional): A list of available logging levels. Aliased as available_log_levels. Defaults to DEFAULT_LOG_LEVELS.
"""
#
# Legacy vars, will override function args if they exist
# If true screen logging is added to standard error
enable_screen_debug = kwargs.get('enableScreenDebug', enable_screen_debug)
enable_log_file = kwargs.get('enableLogFile', enable_log_file)
log_file = kwargs.get('logFile', log_file)
log_rotate = kwargs.get('logRotate', log_rotate)
log_retention = kwargs.get('logRetention', log_retention)
log_level = str(kwargs.get('logLevel', log_level)).upper()
available_log_levels = kwargs.get('availableLogLevels', available_log_levels)

if debug:
log_level = 'DEBUG'

if log_level not in available_log_levels:
log_level = 'INFO'

# Because the library comes with a logger to std.err initalized and we get rid of that
logger.remove()
# Now add the screen std.err logger back using the right log level
if enable_screen_debug:
logger.add(sys.stderr, colorize=True,
level='DEBUG',
backtrace=True,
diagnose=True,
format="<blue>{time:YYYY-MM-DD HH:mm:ss.SSS}</blue> <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> <level>{level}</level>: {message}"
)

# Add file logging if required
if enable_log_file:
if os.path.isfile(log_file):
if not os.access(log_file, os.W_OK):
print("Permissions error, unable to write to log file ({})".format(log_file))
sys.exit(os.EX_CONFIG)

logger.add(log_file, colorize=True,
format="<blue>{time:YYYY-MM-DD HH:mm:ss.SSS}</blue> <yellow>({process.id})</yellow> <level>{level}</level>: {message}",
level=log_level,
rotation=log_rotate,
retention=log_retention,
compression="gz"
)

logger.debug(
f"Log initalized with level: {log_level}, enable screen debug: {enable_screen_debug}, enable log file: {enable_log_file}, file: {log_file}, rotate: {log_rotate}, retention: {log_retention}")
34 changes: 34 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import print_function

import pytest
import argparse
import pytest
from sol1_monitoring_plugins_lib import initLogging, initLoggingArgparse, DEFAULT_LOG_LEVELS


def test_initLoggingArgparse():
parser = argparse.ArgumentParser()
initLoggingArgparse(parser)
args = parser.parse_args([])
assert args.debug == False
assert args.enable_screen_debug == False
assert args.disable_log_file == False
assert args.log_file == '/var/log/icinga2/check_monitoring.log'
assert args.log_rotate == '1 day'
assert args.log_retention == '3 days'
assert args.log_level == 'WARNING'
assert args.available_log_levels == DEFAULT_LOG_LEVELS

args = parser.parse_args(['--debug', '--enable-screen-debug', '--disable-log-file', '--log-file', 'test.log'])
assert args.debug == True
assert args.enable_screen_debug == True
assert args.disable_log_file == True
assert args.log_file == 'test.log'

def test_initLogging(capfd):
initLogging(debug=True, enable_screen_debug=True, enable_log_file=False)
out, err = capfd.readouterr()
assert "enable log file: False" in err
assert "debug: True" in err
assert "Log initalized with level: DEBUG" in err

0 comments on commit 7921e28

Please sign in to comment.