From 82def598f09abe02ad1fe63738040d886e568bf3 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 14:20:47 -0400 Subject: [PATCH] Added healthcheck to the cmds this patch moves the healthcheck to it's own command. aprsd healthcheck --- aprsd/aprsd.py | 4 +- aprsd/cmds/healthcheck.py | 76 +++++++++++++ aprsd/fake_aprs.py | 84 -------------- aprsd/healthcheck.py | 233 -------------------------------------- setup.cfg | 2 - 5 files changed, 79 insertions(+), 320 deletions(-) create mode 100644 aprsd/cmds/healthcheck.py delete mode 100644 aprsd/fake_aprs.py delete mode 100644 aprsd/healthcheck.py diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 7f1aefa..e1ef9c3 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -96,7 +96,9 @@ def cli(ctx, loglevel, config_file, quiet): def main(): - from .cmds import completion, dev, listen, send_message, server # noqa + from .cmds import ( # noqa + completion, dev, healthcheck, listen, send_message, server, + ) cli() diff --git a/aprsd/cmds/healthcheck.py b/aprsd/cmds/healthcheck.py new file mode 100644 index 0000000..7fd7576 --- /dev/null +++ b/aprsd/cmds/healthcheck.py @@ -0,0 +1,76 @@ +# +# Used to fetch the stats url and determine if +# aprsd server is 'healthy' +# +# +# python included libs +import datetime +import json +import logging +import sys + +import click +import requests + +import aprsd +from aprsd import utils + +# local imports here +from ..aprsd import cli + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") + +@cli.command() +@click.option( + "--url", + "health_url", + show_default=True, + default="http://localhost:8001/stats", + help="The aprsd url to call for checking health/stats", +) +@click.option( + "--timeout", + show_default=True, + default=3, + help="How long to wait for healtcheck url to come back", +) +@click.pass_context +def healthcheck(ctx, health_url, timeout): + """Check the health of the running aprsd server.""" + ctx.obj["config"] + LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") + + try: + url = health_url + response = requests.get(url, timeout=timeout) + response.raise_for_status() + except Exception as ex: + LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'") + sys.exit(-1) + else: + stats = json.loads(response.text) + LOG.debug(stats) + + email_thread_last_update = stats["stats"]["email"]["thread_last_update"] + + delta = utils.parse_delta_str(email_thread_last_update) + d = datetime.timedelta(**delta) + max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} + max_delta = datetime.timedelta(**max_timeout) + if d > max_delta: + LOG.error(f"Email thread is very old! {d}") + sys.exit(-1) + + aprsis_last_update = stats["stats"]["aprs-is"]["last_update"] + delta = utils.parse_delta_str(aprsis_last_update) + d = datetime.timedelta(**delta) + max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} + max_delta = datetime.timedelta(**max_timeout) + if d > max_delta: + LOG.error(f"APRS-IS last update is very old! {d}") + sys.exit(-1) + + sys.exit(0) diff --git a/aprsd/fake_aprs.py b/aprsd/fake_aprs.py deleted file mode 100644 index 8eafe6b..0000000 --- a/aprsd/fake_aprs.py +++ /dev/null @@ -1,84 +0,0 @@ -import argparse -import logging -from logging.handlers import RotatingFileHandler -import socketserver -import sys -import time - -from aprsd import utils - - -# command line args -parser = argparse.ArgumentParser() -parser.add_argument( - "--loglevel", - default="DEBUG", - choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - help="The log level to use for aprsd.log", -) -parser.add_argument("--quiet", action="store_true", help="Don't log to stdout") - -parser.add_argument("--port", default=9099, type=int, help="The port to listen on .") -parser.add_argument("--ip", default="127.0.0.1", help="The IP to listen on ") - -CONFIG = None -LOG = logging.getLogger("ARPSSERVER") - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(args): - global LOG - levels = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, - } - log_level = levels[args.loglevel] - - LOG.setLevel(log_level) - log_format = "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]" " %(message)s" - date_format = "%m/%d/%Y %I:%M:%S %p" - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - fh = RotatingFileHandler("aprs-server.log", maxBytes=(10248576 * 5), backupCount=4) - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - if not args.quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -class MyAPRSTCPHandler(socketserver.BaseRequestHandler): - def handle(self): - # self.request is the TCP socket connected to the client - self.data = self.request.recv(1024).strip() - LOG.debug(f"{self.client_address[0]} wrote:") - LOG.debug(self.data) - # just send back the same data, but upper-cased - self.request.sendall(self.data.upper()) - - -def main(): - global CONFIG - args = parser.parse_args() - setup_logging(args) - LOG.info("Test APRS server starting.") - time.sleep(1) - - CONFIG = utils.parse_config(args) - - ip = CONFIG["aprs"]["host"] - port = CONFIG["aprs"]["port"] - LOG.info(f"Start server listening on {args.ip}:{args.port}") - - with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server: - server.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/aprsd/healthcheck.py b/aprsd/healthcheck.py deleted file mode 100644 index 462661c..0000000 --- a/aprsd/healthcheck.py +++ /dev/null @@ -1,233 +0,0 @@ -# -# Used to fetch the stats url and determine if -# aprsd server is 'healthy' -# -# -# python included libs -import datetime -import json -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import re -import sys - -import click -import click_completion -import requests - -# local imports here -import aprsd -from aprsd import config as aprsd_config - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -def show(shell, case_insensitive): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@click.option( - "--append/--overwrite", - help="Append the completion code to the file", - default=None, -) -@click.option( - "-i", - "--case-insensitive/--no-case-insensitive", - help="Case insensitive completion", -) -@click.argument( - "shell", - required=False, - type=click_completion.DocumentedChoice(click_completion.core.shells), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): - log_level = LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s" - date_format = "%m/%d/%Y %I:%M:%S %p" - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprs"].get("logfile", None) - if log_file: - fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) - else: - fh = NullHandler() - - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -def parse_delta_str(s): - if "day" in s: - m = re.match( - r"(?P[-\d]+) day[s]*, (?P\d+):(?P\d+):(?P\d[\.\d+]*)", - s, - ) - else: - m = re.match(r"(?P\d+):(?P\d+):(?P\d[\.\d+]*)", s) - return {key: float(val) for key, val in m.groupdict().items()} - - -@main.command() -@click.option( - "--loglevel", - default="INFO", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - help="The log level to use for aprsd.log", -) -@click.option( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "--url", - "health_url", - show_default=True, - default="http://localhost:8001/stats", - help="The aprsd url to call for checking health/stats", -) -@click.option( - "--timeout", - show_default=True, - default=3, - help="How long to wait for healtcheck url to come back", -) -def check(loglevel, config_file, health_url, timeout): - """APRSD Plugin test app.""" - - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, False) - LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") - - try: - url = health_url - response = requests.get(url, timeout=timeout) - response.raise_for_status() - except Exception as ex: - LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'") - sys.exit(-1) - else: - stats = json.loads(response.text) - LOG.debug(stats) - - email_thread_last_update = stats["stats"]["email"]["thread_last_update"] - - delta = parse_delta_str(email_thread_last_update) - d = datetime.timedelta(**delta) - max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.error(f"Email thread is very old! {d}") - sys.exit(-1) - - aprsis_last_update = stats["stats"]["aprs-is"]["last_update"] - delta = parse_delta_str(aprsis_last_update) - d = datetime.timedelta(**delta) - max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.error(f"APRS-IS last update is very old! {d}") - sys.exit(-1) - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index 89e33bd..a254fed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ packages = [entry_points] console_scripts = aprsd = aprsd.aprsd:main - aprsd-healthcheck = aprsd.healthcheck:main - fake_aprs = aprsd.fake_aprs:main [build_sphinx] source-dir = docs