2
2
3
3
import functools
4
4
import inspect
5
- import io
6
5
import logging
7
6
import os
8
7
import random
9
8
import sys
10
- import traceback
11
9
from typing import (
12
10
IO ,
13
11
TYPE_CHECKING ,
25
23
26
24
import jmespath
27
25
26
+ from aws_lambda_powertools .logging import compat
27
+
28
28
from ..shared import constants
29
29
from ..shared .functions import (
30
30
extract_event_from_common_models ,
@@ -66,12 +66,7 @@ def _is_cold_start() -> bool:
66
66
return cold_start
67
67
68
68
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 :
75
70
"""Creates and setups a logger to format statements in JSON.
76
71
77
72
Includes service name and any additional key=value into logs
@@ -238,7 +233,6 @@ def __init__(
238
233
self .logger_handler = logger_handler or logging .StreamHandler (stream )
239
234
self .log_uncaught_exceptions = log_uncaught_exceptions
240
235
241
- self .log_level = self ._get_log_level (level )
242
236
self ._is_deduplication_disabled = resolve_truthy_env_var_choice (
243
237
env = os .getenv (constants .LOGGER_LOG_DEDUPLICATION_ENV , "false" )
244
238
)
@@ -258,7 +252,7 @@ def __init__(
258
252
"use_rfc3339" : use_rfc3339 ,
259
253
}
260
254
261
- self ._init_logger (formatter_options = formatter_options , ** kwargs )
255
+ self ._init_logger (formatter_options = formatter_options , log_level = level , ** kwargs )
262
256
263
257
if self .log_uncaught_exceptions :
264
258
logger .debug ("Replacing exception hook" )
@@ -277,11 +271,11 @@ def _get_logger(self):
277
271
"""Returns a Logger named {self.service}, or {self.service.filename} for child loggers"""
278
272
logger_name = self .service
279
273
if self .child :
280
- logger_name = f"{ self .service } .{ self . _get_caller_filename ()} "
274
+ logger_name = f"{ self .service } .{ _get_caller_filename ()} "
281
275
282
276
return logging .getLogger (logger_name )
283
277
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 ):
285
279
"""Configures new logger"""
286
280
287
281
# 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):
293
287
if self .child or is_logger_preconfigured :
294
288
return
295
289
290
+ self ._logger .setLevel (self ._determine_log_level (log_level ))
296
291
self ._configure_sampling ()
297
- self ._logger .setLevel (self .log_level )
298
292
self ._logger .addHandler (self .logger_handler )
299
293
self .structure_logs (formatter_options = formatter_options , ** kwargs )
300
294
301
295
# 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
303
297
304
298
# Pytest Live Log feature duplicates log records for colored output
305
299
# but we explicitly add a filter for log deduplication.
@@ -329,7 +323,7 @@ def _configure_sampling(self):
329
323
try :
330
324
if self .sampling_rate and random .random () <= float (self .sampling_rate ):
331
325
logger .debug ("Setting log level to Debug due to sampling rate" )
332
- self .setLevel (logging .DEBUG )
326
+ self ._logger . setLevel (logging .DEBUG )
333
327
except ValueError :
334
328
raise InvalidLoggerSamplingRateError (
335
329
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):
445
439
446
440
return decorate
447
441
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
-
461
442
def info (
462
443
self ,
463
444
msg : object ,
@@ -584,17 +565,6 @@ def append_keys(self, **additional_keys):
584
565
def remove_keys (self , keys : Iterable [str ]):
585
566
self .registered_formatter .remove_keys (keys )
586
567
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
-
598
568
def structure_logs (self , append : bool = False , formatter_options : Optional [Dict ] = None , ** keys ):
599
569
"""Sets logging formatting to JSON.
600
570
@@ -663,8 +633,38 @@ def get_correlation_id(self) -> Optional[str]:
663
633
return self .registered_formatter .log_format .get ("correlation_id" )
664
634
return None
665
635
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
+
666
666
@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 ]:
668
668
"""Returns preferred log level set by the customer in upper case"""
669
669
if isinstance (level , int ):
670
670
return level
@@ -675,51 +675,6 @@ def _get_log_level(level: Union[str, int, None]) -> Union[str, int]:
675
675
676
676
return log_level .upper ()
677
677
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
-
723
678
724
679
def set_package_logger (
725
680
level : Union [str , int ] = logging .DEBUG ,
@@ -760,16 +715,16 @@ def set_package_logger(
760
715
logger .addHandler (handler )
761
716
762
717
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
-
773
718
def log_uncaught_exception_hook (exc_type , exc_value , exc_traceback , logger : Logger ):
774
719
"""Callback function for sys.excepthook to use Logger to log uncaught exceptions"""
775
720
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