From 95cd9424fd09d678d797b44117dd856063ca3436 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Sun, 11 Oct 2020 15:39:15 -0700 Subject: [PATCH] improve log handling --- hyperglass/__init__.py | 22 +++++++++----------- hyperglass/cli/commands.py | 3 ++- hyperglass/log.py | 42 +++++++++++++++----------------------- hyperglass/main.py | 28 +++++++++++++++++++++++-- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/hyperglass/__init__.py b/hyperglass/__init__.py index 96d45a2..af4e0bd 100644 --- a/hyperglass/__init__.py +++ b/hyperglass/__init__.py @@ -37,29 +37,27 @@ POSSIBILITY OF SUCH DAMAGE. """ # Standard Library -import sys +import logging # Third Party import uvloop +import rich.traceback # Project +from hyperglass.log import _get_rich from hyperglass.util import set_app_path from hyperglass.constants import METADATA -try: - # Third Party - import stackprinter -except ImportError: - pass -else: - if sys.stdout.isatty(): - _style = "darkbg2" - else: - _style = "plaintext" - stackprinter.set_excepthook(style=_style) +# Use Rich for traceback formatting. +rich.traceback.install() +# Set Rich as the default logging handler. +logging.getLogger().handlers = [_get_rich(True)] + +# Find hyperglass application directory. set_app_path() +# Use Uvloop for performance. uvloop.install() __name__, __version__, __author__, __copyright__, __license__ = METADATA diff --git a/hyperglass/cli/commands.py b/hyperglass/cli/commands.py index 9c60838..28c7de9 100644 --- a/hyperglass/cli/commands.py +++ b/hyperglass/cli/commands.py @@ -123,7 +123,8 @@ def start(build, direct, workers): start(**kwargs) elif not build and direct: uvicorn_start(**kwargs) - + except KeyboardInterrupt: + error("Stopping hyperglass due to keyboard interrupt.") except BaseException as err: error(str(err)) diff --git a/hyperglass/log.py b/hyperglass/log.py index 058e1d0..b58468e 100644 --- a/hyperglass/log.py +++ b/hyperglass/log.py @@ -54,6 +54,23 @@ def _get_rich(debug: bool = False) -> RichHandler: return RichHandler(**rich_kwargs) +def setup_lib_logging(debug: bool = False) -> None: + """Override the logging handlers for dependency libraries.""" + for name in ( + "gunicorn", + "gunicorn.access", + "gunicorn.error", + "uvicorn", + "uvicorn.access", + "uvicorn.error", + "uvicorn.asgi", + "netmiko", + "scrapli", + "httpx", + ): + logging.getLogger(name).handlers = [_get_rich(debug)] + + def base_logger(): """Initialize hyperglass logging instance.""" _loguru_logger.remove() @@ -75,31 +92,6 @@ def _log_success(self, message, *a, **kw): logging.Logger.success = _log_success -builtin_logging_config = { - "version": 1, - "formatters": {"basic": {"format": "%(message)s"}}, - "root": {"level": "INFO", "handlers": ["rich"]}, - "handlers": { - "rich": { - "level": "INFO", - "formatter": "basic", - "class": "rich.logging.RichHandler", - }, - "console": { - "level": "INFO", - "formatter": "basic", - "class": "rich.logging.RichHandler", - }, - }, - "loggers": { - "": {"handlers": ["console"], "level": "INFO", "propagate": True}, - "uvicorn": {"handlers": ["rich"], "level": "INFO", "propagate": True}, - "uvicorn.access": {"handlers": ["rich"], "level": "INFO", "propagate": True}, - "uvicorn.error": {"handlers": ["rich"], "level": "ERROR", "propagate": True}, - "uvicorn.asgi": {"handlers": ["rich"], "level": "INFO", "propagate": True}, - }, -} - def set_log_level(logger, debug): """Set log level based on debug state.""" diff --git a/hyperglass/main.py b/hyperglass/main.py index 9e6450a..0f55e4d 100644 --- a/hyperglass/main.py +++ b/hyperglass/main.py @@ -4,14 +4,16 @@ import sys import math import shutil +import logging import platform # Third Party from gunicorn.arbiter import Arbiter from gunicorn.app.base import BaseApplication +from gunicorn.glogging import Logger # Project -from hyperglass.log import log +from hyperglass.log import log, setup_lib_logging from hyperglass.constants import MIN_PYTHON_VERSION, __version__ pretty_version = ".".join(tuple(str(v) for v in MIN_PYTHON_VERSION)) @@ -45,6 +47,23 @@ else: loglevel = "WARNING" +class StubbedGunicornLogger(Logger): + """Custom logging to direct Gunicorn/Uvicorn logs to Loguru/Rich. + + See: https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/ + """ + + def setup(self, cfg): + """Override Gunicorn setup.""" + handler = logging.NullHandler() + self.error_logger = logging.getLogger("gunicorn.error") + self.error_logger.addHandler(handler) + self.access_logger = logging.getLogger("gunicorn.access") + self.access_logger.addHandler(handler) + self.error_logger.setLevel(loglevel) + self.access_logger.setLevel(loglevel) + + def check_redis_instance() -> bool: """Ensure Redis is running before starting server.""" @@ -93,6 +112,8 @@ def cache_config(): def on_starting(server: Arbiter): """Gunicorn pre-start tasks.""" + setup_lib_logging(params.debug) + python_version = platform.python_version() required = ".".join((str(v) for v in MIN_PYTHON_VERSION)) log.info("Python {} detected ({} required)", python_version, required) @@ -173,7 +194,10 @@ def start(**kwargs): "timeout": math.ceil(params.request_timeout * 1.25), "on_starting": on_starting, "on_exit": on_exit, - "raw_env": ["testing=test"], + "logger_class": StubbedGunicornLogger, + "accesslog": "-", + "errorlog": "-", + "logconfig_dict": {"formatters": {"generic": {"format": "%(message)s"}}}, **kwargs, }, ).run()