From 340ce271e484e180146af9b1b990cc6d4c2dffe2 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Wed, 19 Nov 2025 18:15:21 -0600 Subject: [PATCH] Fix InternalLogger to properly register with logging hierarchy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InternalLogger instances were not being registered with Python's logging manager, causing them to have no parent logger and preventing handler propagation. This meant that: - basicConfig() had no effect on InternalLogger instances - They couldn't inherit handlers from parent loggers - Users couldn't see Prefab's internal logs without manual configuration This commit fixes the issue by: 1. Registering InternalLogger instances with logging.Logger.manager.loggerDict during __init__, ensuring they participate in the logging hierarchy 2. Setting up parent loggers properly (adapted from Python's logging internals) so handlers propagate correctly 3. Fixing the prefab_internal extra attribute by overriding _log() instead of log(), since info(), debug(), etc. call _log() directly The fix is compatible with both: - Standard logging (logging.basicConfig, manual handler configuration) - Structlog (when configured to use stdlib logging) The prefab_internal=True extra attribute is still added to all log records from InternalLogger instances, allowing users to filter/identify Prefab's internal messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- prefab_cloud_python/_internal_logging.py | 59 ++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/prefab_cloud_python/_internal_logging.py b/prefab_cloud_python/_internal_logging.py index 4e4410d..d8a79fa 100644 --- a/prefab_cloud_python/_internal_logging.py +++ b/prefab_cloud_python/_internal_logging.py @@ -69,9 +69,58 @@ def __init__(self, name: str, level: int = logging.NOTSET) -> None: super().__init__(name, level) self.thread_local = threading.local() - def log(self, level: int, msg, *args, **kwargs) -> None: + # Register this logger with the logging manager so it can participate + # in the logger hierarchy and inherit handlers from parent loggers + logging.Logger.manager.loggerDict[name] = self + + # Set up the parent logger in the hierarchy + # This is adapted from logging.Logger.manager._fixupParents + i = name.rfind(".") + rv = None + while (i > 0) and not rv: + substr = name[:i] + if substr not in logging.Logger.manager.loggerDict: + logging.Logger.manager.loggerDict[substr] = logging.PlaceHolder(self) + else: + obj = logging.Logger.manager.loggerDict[substr] + if isinstance(obj, logging.Logger): + rv = obj + else: + # It's a PlaceHolder + obj.append(self) + i = name.rfind(".", 0, i - 1) + if not rv: + rv = logging.root + self.parent = rv + + def _log( + self, + level: int, + msg, + args, + exc_info=None, + extra=None, + stack_info=False, + stacklevel=1, + ) -> None: + """ + Override _log to add prefab_internal to extra. + This is called by info(), debug(), warning(), error(), etc. + """ if not ReentrancyCheck.is_set(): - extras = kwargs.pop("extra", {}) - extras["prefab_internal"] = True - # Pass the possibly-modified 'extra' dictionary to the underlying logger - super().log(level, msg, *args, extra=extras, **kwargs) + if extra is None: + extra = {} + else: + # Make a copy to avoid modifying the caller's dict + extra = extra.copy() + extra["prefab_internal"] = True + + super()._log( + level, + msg, + args, + exc_info=exc_info, + extra=extra, + stack_info=stack_info, + stacklevel=stacklevel, + )