22
33import functools
44import inspect
5- import io
65import logging
76import os
87import random
98import sys
10- import traceback
119from typing import (
1210 IO ,
1311 TYPE_CHECKING ,
2523
2624import jmespath
2725
26+ from aws_lambda_powertools .logging import compat
27+
2828from ..shared import constants
2929from ..shared .functions import (
3030 extract_event_from_common_models ,
@@ -66,12 +66,7 @@ def _is_cold_start() -> bool:
6666 return cold_start
6767
6868
69- # PyCharm does not support autocomplete via getattr
70- # so we need to return to subclassing removed in #97
71- # All methods/properties continue to be proxied to inner logger
72- # https://github.com/awslabs/aws-lambda-powertools-python/issues/107
73- # noinspection PyRedeclaration
74- class Logger (logging .Logger ): # lgtm [py/missing-call-to-init]
69+ class Logger :
7570 """Creates and setups a logger to format statements in JSON.
7671
7772 Includes service name and any additional key=value into logs
@@ -238,7 +233,6 @@ def __init__(
238233 self .logger_handler = logger_handler or logging .StreamHandler (stream )
239234 self .log_uncaught_exceptions = log_uncaught_exceptions
240235
241- self .log_level = self ._get_log_level (level )
242236 self ._is_deduplication_disabled = resolve_truthy_env_var_choice (
243237 env = os .getenv (constants .LOGGER_LOG_DEDUPLICATION_ENV , "false" )
244238 )
@@ -258,7 +252,7 @@ def __init__(
258252 "use_rfc3339" : use_rfc3339 ,
259253 }
260254
261- self ._init_logger (formatter_options = formatter_options , ** kwargs )
255+ self ._init_logger (formatter_options = formatter_options , log_level = level , ** kwargs )
262256
263257 if self .log_uncaught_exceptions :
264258 logger .debug ("Replacing exception hook" )
@@ -277,11 +271,11 @@ def _get_logger(self):
277271 """Returns a Logger named {self.service}, or {self.service.filename} for child loggers"""
278272 logger_name = self .service
279273 if self .child :
280- logger_name = f"{ self .service } .{ self . _get_caller_filename ()} "
274+ logger_name = f"{ self .service } .{ _get_caller_filename ()} "
281275
282276 return logging .getLogger (logger_name )
283277
284- def _init_logger (self , formatter_options : Optional [Dict ] = None , ** kwargs ):
278+ def _init_logger (self , formatter_options : Optional [Dict ] = None , log_level : Union [ str , int , None ] = None , ** kwargs ):
285279 """Configures new logger"""
286280
287281 # Skip configuration if it's a child logger or a pre-configured logger
@@ -293,13 +287,13 @@ def _init_logger(self, formatter_options: Optional[Dict] = None, **kwargs):
293287 if self .child or is_logger_preconfigured :
294288 return
295289
290+ self ._logger .setLevel (self ._determine_log_level (log_level ))
296291 self ._configure_sampling ()
297- self ._logger .setLevel (self .log_level )
298292 self ._logger .addHandler (self .logger_handler )
299293 self .structure_logs (formatter_options = formatter_options , ** kwargs )
300294
301295 # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work
302- self ._logger .findCaller = self .findCaller
296+ self ._logger .findCaller = compat .findCaller
303297
304298 # Pytest Live Log feature duplicates log records for colored output
305299 # but we explicitly add a filter for log deduplication.
@@ -329,7 +323,7 @@ def _configure_sampling(self):
329323 try :
330324 if self .sampling_rate and random .random () <= float (self .sampling_rate ):
331325 logger .debug ("Setting log level to Debug due to sampling rate" )
332- self .setLevel (logging .DEBUG )
326+ self ._logger . setLevel (logging .DEBUG )
333327 except ValueError :
334328 raise InvalidLoggerSamplingRateError (
335329 f"Expected a float value ranging 0 to 1, but received { self .sampling_rate } instead."
@@ -445,19 +439,6 @@ def decorate(event, context, *args, **kwargs):
445439
446440 return decorate
447441
448- def setLevel (self , level : Union [str , int ]):
449- """
450- Set the logging level for the logger.
451-
452- Parameters:
453- -----------
454- level str | int
455- The level to set. Can be a string representing the level name: 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
456- or an integer representing the level value: 10 for 'DEBUG', 20 for 'INFO', 30 for 'WARNING', 40 for 'ERROR', 50 for 'CRITICAL'. # noqa: E501
457- """
458- self .log_level = level
459- self ._logger .setLevel (level )
460-
461442 def info (
462443 self ,
463444 msg : object ,
@@ -584,17 +565,6 @@ def append_keys(self, **additional_keys):
584565 def remove_keys (self , keys : Iterable [str ]):
585566 self .registered_formatter .remove_keys (keys )
586567
587- @property
588- def registered_handler (self ) -> logging .Handler :
589- """Convenience property to access logger handler"""
590- handlers = self ._logger .parent .handlers if self .child else self ._logger .handlers
591- return handlers [0 ]
592-
593- @property
594- def registered_formatter (self ) -> BasePowertoolsFormatter :
595- """Convenience property to access logger formatter"""
596- return self .registered_handler .formatter # type: ignore
597-
598568 def structure_logs (self , append : bool = False , formatter_options : Optional [Dict ] = None , ** keys ):
599569 """Sets logging formatting to JSON.
600570
@@ -663,8 +633,38 @@ def get_correlation_id(self) -> Optional[str]:
663633 return self .registered_formatter .log_format .get ("correlation_id" )
664634 return None
665635
636+ @property
637+ def registered_handler (self ) -> logging .Handler :
638+ """Convenience property to access the first logger handler"""
639+ handlers = self ._logger .parent .handlers if self .child else self ._logger .handlers
640+ return handlers [0 ]
641+
642+ @property
643+ def registered_formatter (self ) -> BasePowertoolsFormatter :
644+ """Convenience property to access the first logger formatter"""
645+ return self .registered_handler .formatter # type: ignore[return-value]
646+
647+ @property
648+ def log_level (self ) -> int :
649+ return self ._logger .level
650+
651+ @property
652+ def name (self ) -> str :
653+ return self ._logger .name
654+
655+ @property
656+ def handlers (self ) -> List [logging .Handler ]:
657+ """List of registered logging handlers
658+
659+ Notes
660+ -----
661+
662+ Looking for the first configured handler? Use registered_handler property instead.
663+ """
664+ return self ._logger .handlers
665+
666666 @staticmethod
667- def _get_log_level (level : Union [str , int , None ]) -> Union [str , int ]:
667+ def _determine_log_level (level : Union [str , int , None ]) -> Union [str , int ]:
668668 """Returns preferred log level set by the customer in upper case"""
669669 if isinstance (level , int ):
670670 return level
@@ -675,51 +675,6 @@ def _get_log_level(level: Union[str, int, None]) -> Union[str, int]:
675675
676676 return log_level .upper ()
677677
678- @staticmethod
679- def _get_caller_filename ():
680- """Return caller filename by finding the caller frame"""
681- # Current frame => _get_logger()
682- # Previous frame => logger.py
683- # Before previous frame => Caller
684- frame = inspect .currentframe ()
685- caller_frame = frame .f_back .f_back .f_back
686- return caller_frame .f_globals ["__name__" ]
687-
688- # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work
689- def findCaller (self , stack_info = False , stacklevel = 2 ): # pragma: no cover
690- """
691- Find the stack frame of the caller so that we can note the source
692- file name, line number and function name.
693- """
694- f = logging .currentframe () # noqa: VNE001
695- # On some versions of IronPython, currentframe() returns None if
696- # IronPython isn't run with -X:Frames.
697- if f is None :
698- return "(unknown file)" , 0 , "(unknown function)" , None
699- while stacklevel > 0 :
700- next_f = f .f_back
701- if next_f is None :
702- ## We've got options here.
703- ## If we want to use the last (deepest) frame:
704- break
705- ## If we want to mimic the warnings module:
706- # return ("sys", 1, "(unknown function)", None) # noqa: E800
707- ## If we want to be pedantic: # noqa: E800
708- # raise ValueError("call stack is not deep enough") # noqa: E800
709- f = next_f # noqa: VNE001
710- if not _is_internal_frame (f ):
711- stacklevel -= 1
712- co = f .f_code
713- sinfo = None
714- if stack_info :
715- with io .StringIO () as sio :
716- sio .write ("Stack (most recent call last):\n " )
717- traceback .print_stack (f , file = sio )
718- sinfo = sio .getvalue ()
719- if sinfo [- 1 ] == "\n " :
720- sinfo = sinfo [:- 1 ]
721- return co .co_filename , f .f_lineno , co .co_name , sinfo
722-
723678
724679def set_package_logger (
725680 level : Union [str , int ] = logging .DEBUG ,
@@ -760,16 +715,16 @@ def set_package_logger(
760715 logger .addHandler (handler )
761716
762717
763- # Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work
764- # The following is based on warnings._is_internal_frame. It makes sure that
765- # frames of the import mechanism are skipped when logging at module level and
766- # using a stacklevel value greater than one.
767- def _is_internal_frame (frame ): # pragma: no cover
768- """Signal whether the frame is a CPython or logging module internal."""
769- filename = os .path .normcase (frame .f_code .co_filename )
770- return filename == logging ._srcfile or ("importlib" in filename and "_bootstrap" in filename )
771-
772-
773718def log_uncaught_exception_hook (exc_type , exc_value , exc_traceback , logger : Logger ):
774719 """Callback function for sys.excepthook to use Logger to log uncaught exceptions"""
775720 logger .exception (exc_value , exc_info = (exc_type , exc_value , exc_traceback )) # pragma: no cover
721+
722+
723+ def _get_caller_filename ():
724+ """Return caller filename by finding the caller frame"""
725+ # Current frame => _get_logger()
726+ # Previous frame => logger.py
727+ # Before previous frame => Caller
728+ frame = inspect .currentframe ()
729+ caller_frame = frame .f_back .f_back .f_back
730+ return caller_frame .f_globals ["__name__" ]
0 commit comments