diff --git a/src/daqpytools/apps/logging_demonstrator.py b/src/daqpytools/apps/logging_demonstrator.py index 400f6f1..4d9e6a1 100644 --- a/src/daqpytools/apps/logging_demonstrator.py +++ b/src/daqpytools/apps/logging_demonstrator.py @@ -10,9 +10,11 @@ from daqpytools.logging.handlers import ( HandlerType, LogHandlerConf, + add_stderr_handler, + add_stdout_handler, ) from daqpytools.logging.levels import logging_log_level_keys -from daqpytools.logging.logger import get_daq_logger +from daqpytools.logging.logger import get_daq_logger, setup_daq_ers_logger from daqpytools.logging.utils import get_width @@ -225,6 +227,68 @@ def test_handlerconf(main_logger: logging.Logger) -> None: extra=handlerconf.ERS ) + +def test_fallback_handlers(log_level: str) -> None: + """Demonstrate fallback handler behavior for a logger. + + Args: + log_level (str): Log level used to initialize the demo logger. + + Returns: + None + """ + fallback_log: logging.Logger = get_daq_logger( + logger_name="fallback_logger", + log_level=log_level, + stream_handlers=False, + rich_handler=True, + ) + + fallback_log.info("Rich Only") + + add_stdout_handler(fallback_log, True) + add_stderr_handler(fallback_log, True, {HandlerType.Unknown}) + + fallback_log.critical("Rich + stdout only") + fallback_log.critical( + "Rich + stdout + stderr", + extra={"handlers": [HandlerType.Rich, HandlerType.Stream]}, + ) + + +def test_ers_handler_configuration(log_level: str) -> None: + """Demonstrate ERS-driven handler configuration for a logger. + + Args: + log_level (str): Log level used to initialize the demo logger. + + Returns: + None + """ + # Injecting specific + os.environ["DUNEDAQ_ERS_WARNING"] = "lstdout" + os.environ["DUNEDAQ_ERS_INFO"] = "rich" + os.environ["DUNEDAQ_ERS_FATAL"] = "lstderr,rich" + os.environ["DUNEDAQ_ERS_ERROR"] = "rich" + + ers_logger: logging.Logger = get_daq_logger( + logger_name="ers_logger", + log_level=log_level, + stream_handlers=False, + rich_handler=True, + ) + ers_logger.info("Just rich is added") + + # Sets up the logger with all the relevant handlers + setup_daq_ers_logger(ers_logger, "session_temp") + ers_logger.info("ERS configured, but should still only be rich") + + ers_hc = LogHandlerConf(init_ers=True) + ers_logger.info("ERS Info rich ", extra=ers_hc.ERS) + ers_logger.warning("ERS error lstdout", extra=ers_hc.ERS) + ers_logger.critical("ERS critical lstderr + rich", extra=ers_hc.ERS) + + class AllOptionsCommand(click.Command): """Parse the arguments passed and validate they are acceptable, otherwise print the relevant options. @@ -294,11 +358,19 @@ def parse_args(self, ctx: click.Context, args: list[str]) -> None: ), ) @click.option( - "-e", + "-ep", "--ersprotobufstream", - is_flag=True, + type=str, + help=( + "Set up an ERS protobuf handler, and publish to ERS via protobuf." + ) + ) +@click.option( + "-eh", + "--ershandlers", + is_flag=True, help=( - "Set up an ERS handler, and publish to ERS" + "Demonstrate automatic logger configuration with ers variables." ) ) @click.option( @@ -357,6 +429,14 @@ def parse_args(self, ctx: click.Context, args: list[str]) -> None: "logger handlers assigned to the given logger instance" ), ) +@click.option( + "-fh", + "--fallback-handlers", + is_flag=True, + help=( + "If true, demonstrates the use of fallback handlers." + ), +) def main( log_level: str, rich_handler: bool, @@ -364,11 +444,14 @@ def main( stream_handlers: bool, child_logger: bool, disable_logger_inheritance: bool, - ersprotobufstream: bool, + ersprotobufstream: str, handlertypes:bool, handlerconf:bool, throttle: bool, - suppress_basic: bool + suppress_basic: bool, + fallback_handlers: bool, + ershandlers: bool, + ) -> None: """Demonstrate use of the daq_logging class with daqpyutils_logging_demonstrator. Note - if you are seeing output logs without any explicit handlers assigned, this is @@ -384,7 +467,8 @@ def main( disable_logger_inheritance (bool): If true, disable logger inheritance so each logger instance only uses the logger handlers assigned to the given logger instance. - ersprotobufstream (bool): If true, sets up an ERS protobuf handler. Error msg + ersprotobufstream (str): Sets up an ERS protobuf handler with supplied + session name. Error msg are demonstrated in the HandlerType demonstration, requiring handlerconf to be set to true. The topic for these tests is session_tester. handlertypes (bool): If true, demonstrates the advanced feature of HandlerTypes. @@ -393,6 +477,8 @@ def main( throttle (bool): If true, demonstrates the throttling feature. Requires Rich. suppress_basic (bool): If true, supresses basic functionality. Useful to only test the advanced features of logging + fallback_handlers (bool): If true, demonstrates fallback handler behavior. + ershandlers (bool): If true, demonstrates ERS-based handler setup. Returns: None @@ -431,7 +517,10 @@ def main( test_handlertypes(main_logger) if handlerconf: test_handlerconf(main_logger) - + if fallback_handlers: + test_fallback_handlers(log_level) + if ershandlers: + test_ers_handler_configuration(log_level) if __name__ == "__main__": main() diff --git a/src/daqpytools/logging/handlers.py b/src/daqpytools/logging/handlers.py index 11c126a..09b1547 100644 --- a/src/daqpytools/logging/handlers.py +++ b/src/daqpytools/logging/handlers.py @@ -8,6 +8,7 @@ import sys import time from collections import defaultdict +from collections.abc import Callable from dataclasses import dataclass, field from datetime import datetime from enum import Enum @@ -336,13 +337,15 @@ def reset(self) -> None: self.suppressed_counter: int = 0 self.last_occurrence_formatted: str = "" - class BaseHandlerFilter(logging.Filter): """Base filter that hold the logic on choosing if a handler should emit based on what HandlersTypes are supplied to it. """ - def __init__(self) -> None: + def __init__(self, fallback_handlers: set[HandlerType] | None = None) -> None: """C'tor.""" + if fallback_handlers is None: + fallback_handlers = set(LogHandlerConf.get_base()) + self.fallback_handlers = fallback_handlers super().__init__() def get_allowed(self, record: logging.LogRecord) -> list | None: @@ -367,7 +370,7 @@ def get_allowed(self, record: logging.LogRecord) -> list | None: # Handle the non-ERS case else: - allowed = getattr(record, "handlers", LogHandlerConf.get_base()) + allowed = getattr(record, "handlers", self.fallback_handlers) return allowed class HandleIDFilter(BaseHandlerFilter): @@ -375,11 +378,17 @@ class HandleIDFilter(BaseHandlerFilter): if the current handler (defined by the handler_id) is within the set of allowed handlers. """ - def __init__(self, handler_id: HandlerType | list[HandlerType]) -> None: + def __init__( + self, + handler_id: HandlerType | list[HandlerType], + fallback_handlers: set[HandlerType] | None = None, + ) -> None: """Initialises HandleIDFilter with the handler_id, to identify what kind of handler this filter is. """ - super().__init__() + super().__init__( + fallback_handlers = fallback_handlers + ) # Normalise handler_id to be a set if isinstance(handler_id, list): @@ -416,9 +425,16 @@ class ThrottleFilter(BaseHandlerFilter): ... logger.error("Repeated error message") """ - def __init__(self, initial_threshold: int = 30, time_limit: int = 30) -> None: + def __init__( + self, + fallback_handlers: set[HandlerType] | None = None, + initial_threshold: int = 30, + time_limit: int = 30, + ) -> None: """C'tor.""" - super().__init__() + super().__init__( + fallback_handlers = fallback_handlers + ) self.initial_threshold = initial_threshold self.time_limit = time_limit self.issue_map: dict[str, IssueRecord] = defaultdict(IssueRecord) @@ -543,8 +559,41 @@ def _format_timestamp(timestamp: float) -> str: padding: int = LOG_RECORD_PADDING.get("time", 25) time_str: str = dt.strftime(DATE_TIME_BASE_FORMAT).ljust(padding)[:padding] return Text(time_str, style="logging.time") + +def logger_has_handler( + log: logging.Logger, + handler_type: type[logging.Handler], + target_stream: io.IOBase | None = None, +) -> bool: + """Check if logger already has a matching handler. -def check_parent_handlers( + For StreamHandler, ``target_stream`` can be used to distinguish stdout/stderr. + """ + # Catches cases when a MockLogger is used in pytest + if not isinstance(log, logging.Logger): + return False + + type_matches = [ + isinstance(handler, handler_type) + for handler in log.handlers + if not isinstance(handler, logging.StreamHandler) + ] + + stream_matches = [ + handler.stream is target_stream if target_stream else False + for handler in log.handlers + if isinstance(handler, logging.StreamHandler) + ] + return any(type_matches + stream_matches) + +def logger_has_filter(log: logging.Logger, filter_type: type[logging.Filter]) -> bool: + """Check if logger already has a matching filter type.""" + if not isinstance(log, logging.Logger): + return False + + return any(isinstance(logger_filter, filter_type) for logger_filter in log.filters) + +def ancestors_have_handlers( log: logging.Logger, use_parent_handlers: bool, handler_type: type[logging.Handler], @@ -567,13 +616,16 @@ def check_parent_handlers( """ # Sanity check if not use_parent_handlers: - return + return False + + if not isinstance(log, logging.Logger): + return False # Check that we are not using the true logging root logger python_root_logger_name = logging.getLogger().name if log.name == python_root_logger_name: - err_nsg = "You should not be interfacing with the root logger" - raise ValueError(err_nsg) + err_msg = "You should not be interfacing with the root logger" + raise ValueError(err_msg) # Validate the stream handler has a target stream if handler_type.__name__ == "StreamHandler" and target_stream is None: err_msg = ( @@ -589,29 +641,76 @@ def check_parent_handlers( raise ValueError(err_msg) logger_parent = log.parent - this_is_root_logger = logger_parent.name == python_root_logger_name - while not this_is_root_logger: - handler_checking = [ - isinstance(handler, handler_type) for handler in logger_parent.handlers - ] - stream_handler_checking = [ - handler.stream is target_stream if target_stream else False - for handler in logger_parent.handlers - if isinstance(handler, logging.StreamHandler) - ] - if any(handler_checking + stream_handler_checking): - raise LoggerHandlerError(logger_parent.name, handler_type) + visited_logger_ids: set[int] = set() + + while isinstance(logger_parent, logging.Logger): + logger_id = id(logger_parent) + if logger_id in visited_logger_ids: + return False # Prevents infinite loop + visited_logger_ids.add(logger_id) + + if logger_parent.name == python_root_logger_name: + break + + if logger_has_handler(logger_parent,handler_type, target_stream): + return True logger_parent = logger_parent.parent - this_is_root_logger = logger_parent.name == python_root_logger_name - return + return False + + +def check_parent_handlers( + log: logging.Logger, + use_parent_handlers: bool, + handler_type: type[logging.Handler], + target_stream: io.IOBase | None = None, +) -> None: + """Raise when a matching handler already exists on an ancestor logger.""" + if ancestors_have_handlers(log, use_parent_handlers, handler_type, target_stream): + raise LoggerHandlerError(log.name, handler_type) + + +def logger_or_ancestors_have_handler( + log: logging.Logger, + use_parent_handlers: bool, + handler_type: type[logging.Handler], + target_stream: io.IOBase | None = None, +) -> bool: + """Check if logger or (optionally) its ancestors have a matching handler.""" + return logger_has_handler( + log, handler_type, target_stream + ) or ancestors_have_handlers(log, use_parent_handlers, handler_type, target_stream) + +def add_throttle_filter( + log: logging.Logger, + fallback_handlers: set[HandlerType] | None = None, +) -> None: + """Add the Throttle filter to the logger. + + Args: + log (logging.Logger): Logger to add the rich handler to. + fallback_handlers (set[HandlerType] | None): Default handler set used when + records do not explicitly include handler routing. + Returns: + None + """ + if fallback_handlers is None: + fallback_handlers = {HandlerType.Throttle} + log.addFilter(ThrottleFilter(fallback_handlers=fallback_handlers)) + return -def add_rich_handler(log: logging.Logger, use_parent_handlers: bool) -> None: +def add_rich_handler( + log: logging.Logger, + use_parent_handlers: bool, + fallback_handlers: set[HandlerType] | None = None, +) -> None: """Add a rich handler to the logger. Args: log (logging.Logger): Logger to add the rich handler to. use_parent_handlers (bool): Whether to check parent handlers. + fallback_handlers (set[HandlerType] | None): Default handler set used when + records do not explicitly include handler routing. Returns: None @@ -619,32 +718,59 @@ def add_rich_handler(log: logging.Logger, use_parent_handlers: bool) -> None: Raises: LoggerHandlerError: If a parent logger has a rich handler. """ + if fallback_handlers is None: + fallback_handlers = {HandlerType.Rich} check_parent_handlers(log, use_parent_handlers, FormattedRichHandler) width: int = get_width() - handler: RichHandler = FormattedRichHandler(width=width) - handler.addFilter(HandleIDFilter(HandlerType.Rich)) + handler: RichHandler = FormattedRichHandler(width=width) + + handler.addFilter( + HandleIDFilter( + handler_id=HandlerType.Rich, + fallback_handlers=fallback_handlers + ) + ) log.addHandler(handler) return - -def add_ers_kafka_handler(log: logging.Logger, use_parent_handlers: bool, - session_name:str, topic: str = "ers_stream", - address: str ="monkafka.cern.ch:30092") -> None: + +def add_ers_kafka_handler( + log: logging.Logger, + use_parent_handlers: bool, + session_name: str, + fallback_handlers: set[HandlerType] | None = None, + topic: str = "ers_stream", + address: str = "monkafka.cern.ch:30092", +) -> None: # TODO/future: topic and address are new, propagate to all relevant implementation """Add an ers protobuf handler to the root logger.""" + if fallback_handlers is None: + fallback_handlers = {HandlerType.Protobufstream} check_parent_handlers(log, use_parent_handlers, ERSKafkaLogHandler) handler: ERSKafkaLogHandler = ERSKafkaLogHandler(session=session_name, kafka_address = address, kafka_topic = topic ) - handler.addFilter(HandleIDFilter(HandlerType.Protobufstream)) + + handler.addFilter( + HandleIDFilter( + handler_id=HandlerType.Protobufstream, + fallback_handlers=fallback_handlers + ) + ) log.addHandler(handler) -def add_stdout_handler(log: logging.Logger, use_parent_handlers: bool) -> None: +def add_stdout_handler( + log: logging.Logger, + use_parent_handlers: bool, + fallback_handlers: set[HandlerType] | None = None, +) -> None: """Add a stdout handler to the logger. Args: log (logging.Logger): Logger to add the stdout handler to. use_parent_handlers (bool): Whether to check parent handlers. + fallback_handlers (set[HandlerType] | None): Default handler set used when + records do not explicitly include handler routing. Returns: None @@ -652,6 +778,8 @@ def add_stdout_handler(log: logging.Logger, use_parent_handlers: bool) -> None: Raises: LoggerHandlerError: If a parent logger has a stdout handler. """ + if fallback_handlers is None: + fallback_handlers = {HandlerType.Stream, HandlerType.Lstdout} check_parent_handlers( log, use_parent_handlers, @@ -660,12 +788,21 @@ def add_stdout_handler(log: logging.Logger, use_parent_handlers: bool) -> None: ) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(LoggingFormatter()) - stdout_handler.addFilter(HandleIDFilter([HandlerType.Stream, HandlerType.Lstdout])) + + stdout_handler.addFilter( + HandleIDFilter( + handler_id=[HandlerType.Stream, HandlerType.Lstdout], + fallback_handlers=fallback_handlers + ) + ) log.addHandler(stdout_handler) return - -def add_stderr_handler(log: logging.Logger, use_parent_handlers: bool) -> None: +def add_stderr_handler( + log: logging.Logger, + use_parent_handlers: bool, + fallback_handlers: set[HandlerType] | None = None, +) -> None: """Add a stderr handler to the logger. The error is set to the ERROR level, and will only log messages at that level @@ -675,6 +812,8 @@ def add_stderr_handler(log: logging.Logger, use_parent_handlers: bool) -> None: Args: log (logging.Logger): Logger to add the stderr handler to. use_parent_handlers (bool): Whether to check parent handlers. + fallback_handlers (set[HandlerType] | None): Default handler set used when + records do not explicitly include handler routing. Returns: None @@ -682,6 +821,8 @@ def add_stderr_handler(log: logging.Logger, use_parent_handlers: bool) -> None: Raises: LoggerHandlerError: If a parent logger has a stderr handler. """ + if fallback_handlers is None: + fallback_handlers = {HandlerType.Lstderr, HandlerType.Stream} check_parent_handlers( log, use_parent_handlers, @@ -690,19 +831,30 @@ def add_stderr_handler(log: logging.Logger, use_parent_handlers: bool) -> None: ) stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setFormatter(LoggingFormatter()) - stderr_handler.addFilter(HandleIDFilter([HandlerType.Stream, HandlerType.Lstderr])) + stderr_handler.addFilter( + HandleIDFilter( + handler_id=[HandlerType.Stream, HandlerType.Lstderr], + fallback_handlers=fallback_handlers + ) + ) stderr_handler.setLevel(logging.ERROR) log.addHandler(stderr_handler) return - -def add_file_handler(log: logging.Logger, use_parent_handlers: bool, path: str) -> None: +def add_file_handler( + log: logging.Logger, + use_parent_handlers: bool, + path: str, + fallback_handlers: set[HandlerType] | None = None, +) -> None: """Add a file handler to the root logger. Args: log (logging.Logger): Logger to add the file handler to. use_parent_handlers (bool): Whether to check parent handlers. path (str): Path to the log file. + fallback_handlers (set[HandlerType] | None): Default handler set used when + records do not explicitly include handler routing. Returns: None @@ -710,10 +862,125 @@ def add_file_handler(log: logging.Logger, use_parent_handlers: bool, path: str) Raises: LoggerHandlerError: If a parent logger has a file handler. """ + if fallback_handlers is None: + fallback_handlers = {HandlerType.File} check_parent_handlers(log, use_parent_handlers, logging.FileHandler) file_handler = logging.FileHandler(filename=path) file_handler.setFormatter(LoggingFormatter()) - file_handler.addFilter(HandleIDFilter(HandlerType.File)) + file_handler.addFilter( + HandleIDFilter( + handler_id=HandlerType.File, + fallback_handlers=fallback_handlers + ) + ) log.addHandler(file_handler) return +def add_handlers_from_types( + log: logging.Logger, + handler_types: set[HandlerType], + use_parent_handlers: bool, + file_name: str | None, + ers_session_name: str | None, + fallback_handlers: set[HandlerType], +) -> None: + """Add handlers to a logger based on a set of HandlerType values. + + This helper intentionally supports only the default options for now: + - ``use_parent_handlers`` is always True. + - ``HandlerType.File`` is not supported and raises immediately. + - ``HandlerType.Protobufstream`` requires ``ers_session_name``. + """ + if HandlerType.Protobufstream in handler_types and not ers_session_name: + err_msg = "ers_session_name is required for HandlerType.Protobufstream" + raise ValueError(err_msg) + + if HandlerType.File in handler_types and not file_name: + err_msg = "file_name is required for HandlerType.File" + raise ValueError(err_msg) + + # Update relevant handler types that was parsed + effective_handler_types = set(handler_types) + if HandlerType.Stream in effective_handler_types: + effective_handler_types.update({HandlerType.Lstdout, HandlerType.Lstderr}) + + # Generate handler configurations based on arguments for auto install + handler_configs: dict[ + HandlerType, + tuple[ + type[logging.Handler] | None, # Handler as seen by Python's Logger + io.IOBase | None, # Used for streamhandling + type[logging.Filter] | None, # For filters attached to loggers + Callable[[], None], # Installer code + ], + ] = { + HandlerType.Rich: ( + FormattedRichHandler, + None, + None, + lambda: add_rich_handler(log, use_parent_handlers, fallback_handlers), + ), + HandlerType.Lstdout: ( + logging.StreamHandler, + cast(io.IOBase, sys.stdout), + None, + lambda: add_stdout_handler(log, use_parent_handlers, fallback_handlers), + ), + HandlerType.Lstderr: ( + logging.StreamHandler, + cast(io.IOBase, sys.stderr), + None, + lambda: add_stderr_handler(log, use_parent_handlers, fallback_handlers), + ), + HandlerType.Protobufstream: ( + ERSKafkaLogHandler, + None, + None, + lambda: add_ers_kafka_handler( + log, use_parent_handlers, ers_session_name, {HandlerType.Unknown} + # WE DONT WANT TO TRANSMIT BY DEFAULT + ), + ), + HandlerType.Throttle: ( + None, + None, + ThrottleFilter, + lambda: add_throttle_filter(log, fallback_handlers), + ), + HandlerType.File: ( + logging.FileHandler, + None, + None, + lambda: add_file_handler( + log, use_parent_handlers, file_name, fallback_handlers + ), + ), + } + + + for handler_type, ( + handler_class, + target_stream, + filter_type, + installer, + ) in handler_configs.items(): + + # Skips if it encounters an unrequested handler + if handler_type not in effective_handler_types: + continue + + # Skips if handler/filter exists in either the logger or any of its ancestors + handler_exists = ( + handler_class is not None + and logger_or_ancestors_have_handler( + log, + use_parent_handlers, + handler_class, + target_stream=target_stream, + ) + ) or (filter_type is not None and logger_has_filter(log, filter_type)) + + if handler_exists: + continue + + installer() diff --git a/src/daqpytools/logging/logger.py b/src/daqpytools/logging/logger.py index 2f99d17..e298c7a 100644 --- a/src/daqpytools/logging/logger.py +++ b/src/daqpytools/logging/logger.py @@ -8,12 +8,9 @@ from daqpytools.logging.exceptions import LoggerSetupError from daqpytools.logging.handlers import ( - ThrottleFilter, - add_ers_kafka_handler, - add_file_handler, - add_rich_handler, - add_stderr_handler, - add_stdout_handler, + HandlerType, + LogHandlerConf, + add_handlers_from_types, ) from daqpytools.logging.levels import logging_log_level_to_int from daqpytools.logging.utils import get_width @@ -71,8 +68,8 @@ def get_daq_logger( rich_handler: bool = False, file_handler_path: str | None = None, stream_handlers: bool = False, - ers_kafka_handler: bool = False, - throttle: bool = False + ers_kafka_handler: str | None = None, + throttle: bool = False, ) -> logging.Logger: """C'tor for the default logging instances. @@ -84,8 +81,9 @@ def get_daq_logger( file_handler_path (str | None): Path to the file handler log file. If None, no file handler is added. stream_handlers (bool): Whether to add both stdout and stderr stream handlers. - ers_kafka_handler (bool): Whether to add an ERS protobuf handler. - throttle (bool): Whether to add the throttle filter or not. Note, does not mean + ers_kafka_handler (str | None): ERS session name used to add an ERS + protobuf handler. If None, no ERS protobuf handler is added. + throttle (bool): Whether to add the throttle filter or not. Note, does not mean outputs are filtered by default! See ThrottleFilter for details. Returns: @@ -141,20 +139,28 @@ def get_daq_logger( logger.setLevel(log_level) logger.propagate = use_parent_handlers - # Add requested handlers + fallback_handlers: set[HandlerType] = set() if rich_handler: - add_rich_handler(logger, use_parent_handlers) + fallback_handlers.add(HandlerType.Rich) if file_handler_path: - add_file_handler(logger, use_parent_handlers, file_handler_path) + fallback_handlers.add(HandlerType.File) if stream_handlers: - add_stdout_handler(logger, use_parent_handlers) - add_stderr_handler(logger, use_parent_handlers) - if ers_kafka_handler: - add_ers_kafka_handler(logger, use_parent_handlers, "session_tester") - + fallback_handlers.add(HandlerType.Stream) + if ers_kafka_handler: + fallback_handlers.add(HandlerType.Protobufstream) if throttle: - # Note: Default parameters used. No functionality on customisability yet - logger.addFilter(ThrottleFilter()) + fallback_handlers.add(HandlerType.Throttle) + + add_handlers_from_types( + logger, + fallback_handlers, + use_parent_handlers, + file_handler_path, + ers_kafka_handler, + fallback_handlers, + ) + + # Set log level for all handlers if requested if log_level is not logging.NOTSET: @@ -166,3 +172,33 @@ def get_daq_logger( handler.setLevel(log_level) return logger + + +def setup_daq_ers_logger( + logger: logging.Logger, + ers_session_name: str, +) -> None: + """Configure logger handlers from ERS environment-derived configuration. + + Args: + logger (logging.Logger): Logger to configure. + ers_session_name (str): ERS session name used for protobufstream handler. + + Returns: + None + """ + all_handlers = { + handler + for handler_conf in LogHandlerConf._get_oks_conf().values() + for handler in handler_conf.handlers + } + + add_handlers_from_types( + logger, + all_handlers, + use_parent_handlers=True, + file_name=None, + ers_session_name=ers_session_name, + fallback_handlers={HandlerType.Unknown}, + ) + diff --git a/tests/logging/test_logger.py b/tests/logging/test_logger.py index 684ef35..ce7db56 100644 --- a/tests/logging/test_logger.py +++ b/tests/logging/test_logger.py @@ -1,9 +1,11 @@ import logging import tempfile +from unittest.mock import MagicMock import pytest from daqpytools.logging.exceptions import LoggerSetupError +from daqpytools.logging.handlers import logger_or_ancestors_have_handler from daqpytools.logging.logger import get_daq_logger, setup_root_logger test_logger_name = "test_logger" @@ -209,3 +211,17 @@ def test_get_daq_logger(caplog: pytest.LogCaptureFixture): # Shutdown logging to reset any internal state logging.shutdown() + + +def test_logger_parent_walk_handles_mock_logger_cycle(): + """Ensure parent traversal does not hang on mock logger-like objects.""" + fake_logger = MagicMock() + fake_parent = MagicMock() + + fake_logger.parent = fake_parent + fake_parent.parent = fake_parent + + assert ( + logger_or_ancestors_have_handler(fake_logger, True, logging.NullHandler) + is False + )