Skip to content

Commit

Permalink
Merge pull request #285 from thegridelectric/dev
Browse files Browse the repository at this point in the history
Add "category loggers" for more flexible log control in derived classes.
  • Loading branch information
anschweitzer authored Jan 13, 2025
2 parents ffdf4c7 + 5015c23 commit 307c57a
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 4 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "gridworks-proactor"
version = "1.1.2"
version = "1.1.3"
description = "Gridworks Proactor"
authors = ["Jessica Millar <[email protected]>"]
license = "MIT"
Expand Down
69 changes: 67 additions & 2 deletions src/gwproactor/logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contextlib
import datetime
import logging
from typing import Any, Optional
from typing import Any, Optional, Sequence, TypeAlias


class MessageSummary:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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__} "
Expand Down
3 changes: 3 additions & 0 deletions src/gwproactor_test/logger_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
class LoggerGuard:
level: int
propagate: bool
disabled: bool
handlers: set[logging.Handler]
filters: set[logging.Filter]

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)

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/config/example-test-mosquitto.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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

#################################
Expand Down
80 changes: 80 additions & 0 deletions tests/test_proactor/test_proactor_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 307c57a

Please sign in to comment.