|
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