From 81b9297503505ee883659284010fcc10cd182be5 Mon Sep 17 00:00:00 2001 From: Andrew Schweitzer Date: Mon, 13 Jan 2025 16:39:12 -0500 Subject: [PATCH 1/2] tests: example mosquitto.conf defaults to only accepting connections from localhost --- tests/config/example-test-mosquitto.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/config/example-test-mosquitto.conf b/tests/config/example-test-mosquitto.conf index 62b44c0..3f25fe3 100644 --- a/tests/config/example-test-mosquitto.conf +++ b/tests/config/example-test-mosquitto.conf @@ -2,7 +2,7 @@ per_listener_settings true ################################# # Non-TLS, 'clear' MQTT on port 1883 -listener 1883 0.0.0.0 +listener 1883 localhost allow_anonymous true ################################# From 5015c235c80680e0430e7517ab3d58337eece6e7 Mon Sep 17 00:00:00 2001 From: Andrew Schweitzer Date: Mon, 13 Jan 2025 16:41:54 -0500 Subject: [PATCH 2/2] Logging: add 'category loggers' that can be managed (and all reset) via the ProactorLogger object --- pyproject.toml | 2 +- src/gwproactor/logger.py | 69 +++++++++++++++++- src/gwproactor_test/logger_guard.py | 3 + tests/test_proactor/test_proactor_logger.py | 80 +++++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e31027b..3eb63a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gridworks-proactor" -version = "1.1.2" +version = "1.1.3" description = "Gridworks Proactor" authors = ["Jessica Millar "] license = "MIT" diff --git a/src/gwproactor/logger.py b/src/gwproactor/logger.py index cceecde..ee00277 100644 --- a/src/gwproactor/logger.py +++ b/src/gwproactor/logger.py @@ -1,7 +1,7 @@ import contextlib import datetime import logging -from typing import Any, Optional +from typing import Any, Optional, Sequence, TypeAlias class MessageSummary: @@ -80,6 +80,22 @@ def format( # noqa: PLR0913 return "" +LoggerOrAdapter: TypeAlias = logging.Logger | logging.LoggerAdapter + + +class CategoryLoggerInfo: + logger: LoggerOrAdapter + default_level: int = logging.INFO + default_disabled = False + + def __init__( + self, logger: LoggerOrAdapter, default_level: int = logging.INFO + ) -> None: + self.logger = logger + self.default_level = default_level + self.default_disabled = logger.disabled + + class ProactorLogger(logging.LoggerAdapter): MESSAGE_DELIMITER_WIDTH = 88 MESSAGE_ENTRY_DELIMITER = "+" * MESSAGE_DELIMITER_WIDTH @@ -88,19 +104,25 @@ class ProactorLogger(logging.LoggerAdapter): message_summary_logger: logging.Logger lifecycle_logger: logging.Logger comm_event_logger: logging.Logger + category_loggers: dict[str, CategoryLoggerInfo] - def __init__( + def __init__( # noqa: PLR0913 self, base: str, message_summary: str, lifecycle: str, comm_event: str, extra: Optional[dict] = None, + category_logger_names: Optional[Sequence[str]] = None, ) -> None: super().__init__(logging.getLogger(base), extra=extra) self.message_summary_logger = logging.getLogger(message_summary) self.lifecycle_logger = logging.getLogger(lifecycle) self.comm_event_logger = logging.getLogger(comm_event) + self.category_loggers = {} + if category_logger_names is not None: + for category_logger_name in category_logger_names: + self.add_category_logger(category_logger_name) @property def general_enabled(self) -> bool: @@ -168,6 +190,49 @@ def message_exit(self, msg: str, *args: Any, **kwargs: Any) -> None: self.path(msg, *args, **kwargs) self.path(self.MESSAGE_EXIT_DELIMITER) + def category_logger_name(self, category: str) -> str: + return f"{self.name}.{category}" + + def category_logger( + self, category: str + ) -> Optional[logging.Logger | logging.LoggerAdapter]: + """Get existing category logger""" + logger_info = self.category_loggers.get(category) + if logger_info is not None: + return logger_info.logger + return None + + def add_category_logger( + self, + category: str = "", + level: int = logging.INFO, + logger: Optional[LoggerOrAdapter] = None, + ) -> LoggerOrAdapter: + if logger is None: + if not category: + raise ValueError( + "ERROR. add_category_logger() requires category value " + "unless logger is provided." + ) + logger = logging.getLogger(self.category_logger_name(category)) + logger.setLevel(level) + else: + category = logger.name + self_prefix = f"{self.name}." + if category.startswith(self_prefix): + category = category[len(self_prefix) :] + level = logger.getEffectiveLevel() + self.category_loggers[category] = CategoryLoggerInfo( + logger=logger, + default_level=level, + ) + return logger + + def reset_default_category_levels(self) -> None: + for logger_info in self.category_loggers.values(): + logger_info.logger.setLevel(logger_info.default_level) + logger_info.logger.disabled = logger_info.default_disabled + def __repr__(self) -> str: return ( f"<{self.__class__.__name__} " diff --git a/src/gwproactor_test/logger_guard.py b/src/gwproactor_test/logger_guard.py index 91b1d5a..0dba2a6 100644 --- a/src/gwproactor_test/logger_guard.py +++ b/src/gwproactor_test/logger_guard.py @@ -11,6 +11,7 @@ class LoggerGuard: level: int propagate: bool + disabled: bool handlers: set[logging.Handler] filters: set[logging.Filter] @@ -18,6 +19,7 @@ def __init__(self, logger: logging.Logger) -> None: self.logger = logger self.level = logger.level self.propagate = logger.propagate + self.disabled = logger.disabled self.handlers = set(logger.handlers) self.filters = set(logger.filters) @@ -37,6 +39,7 @@ def restore(self) -> None: ) self.logger.setLevel(self.level) self.logger.propagate = self.propagate + self.logger.disabled = self.disabled curr_handlers = set(self.logger.handlers) for handler in curr_handlers - self.handlers: self.logger.removeHandler(handler) diff --git a/tests/test_proactor/test_proactor_logger.py b/tests/test_proactor/test_proactor_logger.py index 274e3c3..648ba1f 100644 --- a/tests/test_proactor/test_proactor_logger.py +++ b/tests/test_proactor/test_proactor_logger.py @@ -3,6 +3,8 @@ import warnings from typing import Any +import pytest + from gwproactor import ProactorLogger, ProactorSettings, setup_logging from gwproactor.config import Paths from gwproactor_test import LoggerGuards @@ -69,3 +71,81 @@ def test_proactor_logger(caplog: Any) -> None: f"len(caplog.records) ({len(caplog.records)}) != 1 (#3)", stacklevel=2, ) + + +def test_category_logger() -> None: + # default - no categories + settings = ProactorSettings() + prlogger = ProactorLogger(**settings.logging.qualified_logger_names()) + assert not prlogger.category_loggers + + # One cat logger in constructor + cat_name = "Spots" + prlogger = ProactorLogger( + category_logger_names=[cat_name], + **settings.logging.qualified_logger_names(), + ) + assert len(prlogger.category_loggers) == 1 + logger = prlogger.category_logger(cat_name) + assert logger is not None + assert logger.getEffectiveLevel() == logging.INFO + assert not logger.disabled + assert logger.name == f"{prlogger.name}.{cat_name}" + + # Check valid arguments for add + with pytest.raises(ValueError): + prlogger.add_category_logger() + + # query for missing logger does not crash + prlogger = ProactorLogger(**settings.logging.qualified_logger_names()) + assert prlogger.category_logger("foo") is None + + # Add cat loggers in various ways + # Add by name + cat_name = "Max" + logger = prlogger.add_category_logger(cat_name, level=logging.DEBUG) + assert logger is not None + assert logger is prlogger.category_logger(cat_name) + assert logger.getEffectiveLevel() == logging.DEBUG + assert not logger.disabled + assert logger.name == f"{prlogger.name}.{cat_name}" + + # Add by an explicit logger + cat_name = "Sandy" + qualified_name = f"{prlogger.name}.{cat_name}" + explicit_logger = logging.getLogger(qualified_name) + explicit_logger.setLevel(logging.WARNING) + explicit_logger.disabled = True + logger = prlogger.add_category_logger(logger=explicit_logger) + assert logger is explicit_logger + assert logger is prlogger.category_logger(cat_name) + assert logger.getEffectiveLevel() == logging.WARNING + assert logger.disabled + assert logger.name == f"{prlogger.name}.{cat_name}" + + # Add by an explicit logger with name not qualifed on ProactorLogger's + # base name + cat_name = "Oreo" + explicit_logger = logging.getLogger(cat_name) + explicit_logger.setLevel(logging.ERROR) + logger = prlogger.add_category_logger(logger=explicit_logger) + assert logger is explicit_logger + assert logger is prlogger.category_logger(cat_name) + assert logger.getEffectiveLevel() == logging.ERROR + assert not logger.disabled + assert logger.name == cat_name + + # Test resetting the category logger levels + prlogger.category_logger("Max").setLevel(logging.INFO) + prlogger.category_logger("Max").disabled = True + prlogger.category_logger("Sandy").setLevel(logging.INFO) + prlogger.category_logger("Sandy").disabled = False + prlogger.category_logger("Oreo").setLevel(logging.INFO) + prlogger.category_logger("Oreo").disabled = True + prlogger.reset_default_category_levels() + assert prlogger.category_logger("Max").getEffectiveLevel() == logging.DEBUG + assert not prlogger.category_logger("Max").disabled + assert prlogger.category_logger("Sandy").getEffectiveLevel() == logging.WARNING + assert prlogger.category_logger("Sandy").disabled + assert prlogger.category_logger("Oreo").getEffectiveLevel() == logging.ERROR + assert not prlogger.category_logger("Oreo").disabled