|
14 | 14 | import sys
|
15 | 15 | import warnings
|
16 | 16 | from contextlib import contextmanager
|
17 |
| -from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterable, Mapping, TypeVar, overload |
| 17 | +from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterable, Mapping, TypeVar, cast, overload |
18 | 18 |
|
19 | 19 | from aws_lambda_powertools.logging.constants import (
|
| 20 | + LOGGER_ATTRIBUTE_HANDLER, |
| 21 | + LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER, |
20 | 22 | LOGGER_ATTRIBUTE_PRECONFIGURED,
|
21 | 23 | )
|
22 |
| -from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError |
| 24 | +from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError, OrphanedChildLoggerError |
23 | 25 | from aws_lambda_powertools.logging.filters import SuppressFilter
|
24 | 26 | from aws_lambda_powertools.logging.formatter import (
|
25 | 27 | RESERVED_FORMATTER_CUSTOM_KEYS,
|
@@ -230,13 +232,14 @@ def __init__(
|
230 | 232 | self.child = child
|
231 | 233 | self.logger_formatter = logger_formatter
|
232 | 234 | self._stream = stream or sys.stdout
|
233 |
| - self.logger_handler = logger_handler or logging.StreamHandler(self._stream) |
| 235 | + |
234 | 236 | self.log_uncaught_exceptions = log_uncaught_exceptions
|
235 | 237 |
|
236 | 238 | self._is_deduplication_disabled = resolve_truthy_env_var_choice(
|
237 | 239 | env=os.getenv(constants.LOGGER_LOG_DEDUPLICATION_ENV, "false"),
|
238 | 240 | )
|
239 | 241 | self._logger = self._get_logger()
|
| 242 | + self.logger_handler = logger_handler or self._get_handler() |
240 | 243 |
|
241 | 244 | # NOTE: This is primarily to improve UX, so IDEs can autocomplete LambdaPowertoolsFormatter options
|
242 | 245 | # previously, we masked all of them as kwargs thus limiting feature discovery
|
@@ -275,6 +278,23 @@ def _get_logger(self) -> logging.Logger:
|
275 | 278 |
|
276 | 279 | return logging.getLogger(logger_name)
|
277 | 280 |
|
| 281 | + def _get_handler(self) -> logging.Handler: |
| 282 | + # is a logger handler already configured? |
| 283 | + if getattr(self, LOGGER_ATTRIBUTE_HANDLER, None): |
| 284 | + return self.logger_handler |
| 285 | + |
| 286 | + # Detect Powertools logger by checking for unique handler |
| 287 | + # Retrieve the first handler if it's a Powertools instance |
| 288 | + if getattr(self._logger, "powertools_handler", None): |
| 289 | + return self._logger.handlers[0] |
| 290 | + |
| 291 | + # for children, use parent's handler |
| 292 | + if self.child: |
| 293 | + return getattr(self._logger.parent, LOGGER_ATTRIBUTE_POWERTOOLS_HANDLER, None) # type: ignore[return-value] # always checked in formatting |
| 294 | + |
| 295 | + # otherwise, create a new stream handler (first time init) |
| 296 | + return logging.StreamHandler(self._stream) |
| 297 | + |
278 | 298 | def _init_logger(
|
279 | 299 | self,
|
280 | 300 | formatter_options: dict | None = None,
|
@@ -317,6 +337,7 @@ def _init_logger(
|
317 | 337 | # std logging will return the same Logger with our attribute if name is reused
|
318 | 338 | logger.debug(f"Marking logger {self.service} as preconfigured")
|
319 | 339 | self._logger.init = True # type: ignore[attr-defined]
|
| 340 | + self._logger.powertools_handler = self.logger_handler # type: ignore[attr-defined] |
320 | 341 |
|
321 | 342 | def _configure_sampling(self) -> None:
|
322 | 343 | """Dynamically set log level based on sampling rate
|
@@ -723,13 +744,20 @@ def registered_handler(self) -> logging.Handler:
|
723 | 744 | """Convenience property to access the first logger handler"""
|
724 | 745 | # We ignore mypy here because self.child encodes whether or not self._logger.parent is
|
725 | 746 | # None, mypy can't see this from context but we can
|
726 |
| - handlers = self._logger.parent.handlers if self.child else self._logger.handlers # type: ignore[union-attr] |
727 |
| - return handlers[0] |
| 747 | + return self._get_handler() |
728 | 748 |
|
729 | 749 | @property
|
730 | 750 | def registered_formatter(self) -> BasePowertoolsFormatter:
|
731 | 751 | """Convenience property to access the first logger formatter"""
|
732 |
| - return self.registered_handler.formatter # type: ignore[return-value] |
| 752 | + handler = self.registered_handler |
| 753 | + if handler is None: |
| 754 | + raise OrphanedChildLoggerError( |
| 755 | + "Orphan child loggers cannot append nor remove keys until a parent is initialized first. " |
| 756 | + "To solve this issue, you can A) make sure a parent logger is initialized first, or B) move append/remove keys operations to a later stage." # noqa: E501 |
| 757 | + "Reference: https://docs.powertools.aws.dev/lambda/python/latest/core/logger/#reusing-logger-across-your-code", |
| 758 | + ) |
| 759 | + |
| 760 | + return cast(BasePowertoolsFormatter, handler.formatter) |
733 | 761 |
|
734 | 762 | @property
|
735 | 763 | def log_level(self) -> int:
|
|
0 commit comments