Skip to content

Commit 4dc312c

Browse files
heitorlessaleandrodamascena
authored andcommitted
refactor(logger): remove subclassing and move unnecessary APIs (#2334)
Co-authored-by: Leandro Damascena <[email protected]>
1 parent 31e5de3 commit 4dc312c

File tree

3 files changed

+102
-96
lines changed

3 files changed

+102
-96
lines changed

Diff for: aws_lambda_powertools/logging/compat.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Maintenance: We can drop this upon Py3.7 EOL. It's a backport for "location" key to work."""
2+
from __future__ import annotations
3+
4+
import io
5+
import logging
6+
import os
7+
import traceback
8+
9+
10+
def findCaller(stack_info=False, stacklevel=2): # pragma: no cover
11+
"""
12+
Find the stack frame of the caller so that we can note the source
13+
file name, line number and function name.
14+
"""
15+
f = logging.currentframe() # noqa: VNE001
16+
# On some versions of IronPython, currentframe() returns None if
17+
# IronPython isn't run with -X:Frames.
18+
if f is None:
19+
return "(unknown file)", 0, "(unknown function)", None
20+
while stacklevel > 0:
21+
next_f = f.f_back
22+
if next_f is None:
23+
## We've got options here.
24+
## If we want to use the last (deepest) frame:
25+
break
26+
## If we want to mimic the warnings module:
27+
# return ("sys", 1, "(unknown function)", None) # noqa: E800
28+
## If we want to be pedantic: # noqa: E800
29+
# raise ValueError("call stack is not deep enough") # noqa: E800
30+
f = next_f # noqa: VNE001
31+
if not _is_internal_frame(f):
32+
stacklevel -= 1
33+
co = f.f_code
34+
sinfo = None
35+
if stack_info:
36+
with io.StringIO() as sio:
37+
sio.write("Stack (most recent call last):\n")
38+
traceback.print_stack(f, file=sio)
39+
sinfo = sio.getvalue()
40+
if sinfo[-1] == "\n":
41+
sinfo = sinfo[:-1]
42+
return co.co_filename, f.f_lineno, co.co_name, sinfo
43+
44+
45+
# The following is based on warnings._is_internal_frame. It makes sure that
46+
# frames of the import mechanism are skipped when logging at module level and
47+
# using a stacklevel value greater than one.
48+
def _is_internal_frame(frame): # pragma: no cover
49+
"""Signal whether the frame is a CPython or logging module internal."""
50+
filename = os.path.normcase(frame.f_code.co_filename)
51+
return filename == logging._srcfile or ("importlib" in filename and "_bootstrap" in filename)

Diff for: aws_lambda_powertools/logging/logger.py

+50-95
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
import functools
44
import inspect
5-
import io
65
import logging
76
import os
87
import random
98
import sys
10-
import traceback
119
from typing import (
1210
IO,
1311
TYPE_CHECKING,
@@ -25,6 +23,8 @@
2523

2624
import jmespath
2725

26+
from aws_lambda_powertools.logging import compat
27+
2828
from ..shared import constants
2929
from ..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/aws-powertools/lambda-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

724679
def 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-
773718
def 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__"]

Diff for: aws_lambda_powertools/logging/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def copy_config_to_registered_loggers(
2525
exclude : Optional[Set[str]], optional
2626
List of logger names to exclude, by default None
2727
"""
28-
level = log_level or source_logger.level
28+
level = log_level or source_logger.log_level
2929

3030
# Assumptions: Only take parent loggers not children (dot notation rule)
3131
# Steps:

0 commit comments

Comments
 (0)