From ac9957db28fd921f18cf2ac1fee8e359fd36810d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sun, 28 Feb 2021 18:20:23 +0100 Subject: [PATCH 01/17] feat: initial working skeleton Signed-off-by: heitorlessa --- aws_lambda_powertools/tracing/tracer.py | 47 ++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index bfd18be245a..279dc889fcd 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -4,11 +4,9 @@ import inspect import logging import os +from importlib import import_module from typing import Any, Callable, Dict, List, Optional, Tuple -import aws_xray_sdk -import aws_xray_sdk.core - from ..shared import constants from ..shared.functions import resolve_truthy_env_var_choice @@ -17,7 +15,18 @@ # Set the streaming threshold to 0 on the default recorder to force sending # subsegments individually, rather than batching them. # See https://github.com/awslabs/aws-lambda-powertools-python/issues/283 -aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) +# aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 + +XRAY_SDK_MOD = "aws_xray_sdk" +XRAY_SDK_CORE_MOD = "aws_xray_sdk.core" + + +def _import_xray(): + xray_sdk = import_module(XRAY_SDK_MOD) + xray_sdk_core = import_module(XRAY_SDK_CORE_MOD) + + globals()[XRAY_SDK_MOD] = xray_sdk + globals()[XRAY_SDK_CORE_MOD] = xray_sdk_core class Tracer: @@ -134,12 +143,18 @@ def handler(event: dict, context: Any) -> Dict: * Async handler not supported """ + # aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 + # Types + # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/models/subsegment.py + # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/async_recorder.py + # Add attr to avoid double importing + _default_config = { "service": "service_undefined", "disabled": False, "auto_patch": True, "patch_modules": None, - "provider": aws_xray_sdk.core.xray_recorder, + "provider": None, } _config = copy.copy(_default_config) @@ -149,8 +164,13 @@ def __init__( disabled: bool = None, auto_patch: bool = None, patch_modules: List = None, - provider: aws_xray_sdk.core.xray_recorder = None, + provider: Any = None, + # provider: aws_xray_sdk.core.xray_recorder = None, ): + + _import_xray() # use return to allow code completion + self._config["provider"] = aws_xray_sdk.core.xray_recorder # noqa: F821 + self.__build_config( service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider ) @@ -232,9 +252,9 @@ def patch(self, modules: Tuple[str] = None): return if modules is None: - aws_xray_sdk.core.patch_all() + aws_xray_sdk.core.patch_all() # noqa: F821 else: - aws_xray_sdk.core.patch(modules) + aws_xray_sdk.core.patch(modules) # noqa: F821 def capture_lambda_handler( self, @@ -625,7 +645,8 @@ def _add_response_as_metadata( self, method_name: str = None, data: Any = None, - subsegment: aws_xray_sdk.core.models.subsegment = None, + subsegment: Any = None, + # subsegment: aws_xray_sdk.core.models.subsegment = None, capture_response: Optional[bool] = None, ): """Add response as metadata for given subsegment @@ -650,7 +671,8 @@ def _add_full_exception_as_metadata( self, method_name: str = None, error: Exception = None, - subsegment: aws_xray_sdk.core.models.subsegment = None, + subsegment: Any = None, + # subsegment: aws_xray_sdk.core.models.subsegment = None, capture_error: Optional[bool] = None, ): """Add full exception object as metadata for given subsegment @@ -675,7 +697,7 @@ def _add_full_exception_as_metadata( def _disable_tracer_provider(): """Forcefully disables tracing""" logger.debug("Disabling tracer provider...") - aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) + aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) # noqa: F821 @staticmethod def _is_tracer_disabled() -> bool: @@ -712,7 +734,8 @@ def __build_config( disabled: bool = None, auto_patch: bool = None, patch_modules: List = None, - provider: aws_xray_sdk.core.xray_recorder = None, + provider: Any = None, + # provider: aws_xray_sdk.core.xray_recorder = None, ): """ Populates Tracer config for new and existing initializations """ is_disabled = disabled if disabled is not None else self._is_tracer_disabled() From f429a74295057401043c0463769b437baf0d9063 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 09:50:12 +0100 Subject: [PATCH 02/17] feat: use global lazy import for intellisense --- aws_lambda_powertools/shared/constants.py | 4 +++ aws_lambda_powertools/shared/lazy_import.py | 38 +++++++++++++++++++++ aws_lambda_powertools/tracing/tracer.py | 24 ++++--------- 3 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 aws_lambda_powertools/shared/lazy_import.py diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index c69d6b5ea49..eaad5640dfd 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -14,3 +14,7 @@ CHALICE_LOCAL_ENV: str = "AWS_CHALICE_CLI_MODE" SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME" XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID" + + +XRAY_SDK_MODULE = "aws_xray_sdk" +XRAY_SDK_CORE_MODULE = "aws_xray_sdk.core" diff --git a/aws_lambda_powertools/shared/lazy_import.py b/aws_lambda_powertools/shared/lazy_import.py new file mode 100644 index 00000000000..42ace91a719 --- /dev/null +++ b/aws_lambda_powertools/shared/lazy_import.py @@ -0,0 +1,38 @@ +import importlib +import types + + +class LazyLoader(types.ModuleType): + """Lazily import a module, mainly to avoid pulling in large dependencies. + + `contrib`, and `ffmpeg` are examples of modules that are large and not always + needed, and this allows them to only be loaded when they are used. + + Note: Subclassing types.ModuleType allow us to correctly adhere with sys.modules, import system + """ + + def __init__(self, local_name, parent_module_globals, name): # pylint: disable=super-on-old-class + self._local_name = local_name + self._parent_module_globals = parent_module_globals + + super(LazyLoader, self).__init__(name) + + def _load(self): + # Import the target module and insert it into the parent's namespace + module = importlib.import_module(self.__name__) + self._parent_module_globals[self._local_name] = module + + # Update this object's dict so that if someone keeps a reference to the + # LazyLoader, lookups are efficient (__getattr__ is only called on lookups + # that fail). + self.__dict__.update(module.__dict__) + + return module + + def __getattr__(self, item): + module = self._load() + return getattr(module, item) + + def __dir__(self): + module = self._load() + return dir(module) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 279dc889fcd..bae0095494a 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -4,11 +4,11 @@ import inspect import logging import os -from importlib import import_module from typing import Any, Callable, Dict, List, Optional, Tuple from ..shared import constants from ..shared.functions import resolve_truthy_env_var_choice +from ..shared.lazy_import import LazyLoader is_cold_start = True logger = logging.getLogger(__name__) @@ -17,16 +17,8 @@ # See https://github.com/awslabs/aws-lambda-powertools-python/issues/283 # aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 -XRAY_SDK_MOD = "aws_xray_sdk" -XRAY_SDK_CORE_MOD = "aws_xray_sdk.core" - - -def _import_xray(): - xray_sdk = import_module(XRAY_SDK_MOD) - xray_sdk_core = import_module(XRAY_SDK_CORE_MOD) - - globals()[XRAY_SDK_MOD] = xray_sdk - globals()[XRAY_SDK_CORE_MOD] = xray_sdk_core +aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE) +aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) class Tracer: @@ -147,7 +139,6 @@ def handler(event: dict, context: Any) -> Dict: # Types # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/models/subsegment.py # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/async_recorder.py - # Add attr to avoid double importing _default_config = { "service": "service_undefined", @@ -168,9 +159,6 @@ def __init__( # provider: aws_xray_sdk.core.xray_recorder = None, ): - _import_xray() # use return to allow code completion - self._config["provider"] = aws_xray_sdk.core.xray_recorder # noqa: F821 - self.__build_config( service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider ) @@ -252,9 +240,9 @@ def patch(self, modules: Tuple[str] = None): return if modules is None: - aws_xray_sdk.core.patch_all() # noqa: F821 + aws_xray_sdk.core.patch_all() else: - aws_xray_sdk.core.patch(modules) # noqa: F821 + aws_xray_sdk.core.patch(modules) def capture_lambda_handler( self, @@ -697,7 +685,7 @@ def _add_full_exception_as_metadata( def _disable_tracer_provider(): """Forcefully disables tracing""" logger.debug("Disabling tracer provider...") - aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) # noqa: F821 + aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) @staticmethod def _is_tracer_disabled() -> bool: From 2095142eaffd329bcc5d9e2a4e06b15250d58f52 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 11:24:55 +0100 Subject: [PATCH 03/17] fix: default lazy provider --- aws_lambda_powertools/tracing/tracer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index bae0095494a..4cebdf20a63 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -685,7 +685,7 @@ def _add_full_exception_as_metadata( def _disable_tracer_provider(): """Forcefully disables tracing""" logger.debug("Disabling tracer provider...") - aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) + aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) # noqa: F821 @staticmethod def _is_tracer_disabled() -> bool: @@ -729,7 +729,7 @@ def __build_config( is_disabled = disabled if disabled is not None else self._is_tracer_disabled() is_service = service if service is not None else os.getenv(constants.SERVICE_NAME_ENV) - self._config["provider"] = provider if provider is not None else self._config["provider"] + self._config["provider"] = provider or self._config["provider"] or aws_xray_sdk.core.xray_recorder self._config["auto_patch"] = auto_patch if auto_patch is not None else self._config["auto_patch"] self._config["service"] = is_service or self._config["service"] self._config["disabled"] = is_disabled or self._config["disabled"] From e85aaf879ed52fbb6d5ee196f56814e79e418890 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 11:30:56 +0100 Subject: [PATCH 04/17] chore: trigger CI #1 From 188a51787208d4eac8d40a1376bd683213b532f3 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 11:32:25 +0100 Subject: [PATCH 05/17] chore: trigger CI #2 From cc2e0a5453a1625bfda7950a68798cd686a40de7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 11:38:39 +0100 Subject: [PATCH 06/17] chore: uncaught linting --- aws_lambda_powertools/tracing/tracer.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 4cebdf20a63..4d251b515dd 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -156,7 +156,6 @@ def __init__( auto_patch: bool = None, patch_modules: List = None, provider: Any = None, - # provider: aws_xray_sdk.core.xray_recorder = None, ): self.__build_config( @@ -634,7 +633,7 @@ def _add_response_as_metadata( method_name: str = None, data: Any = None, subsegment: Any = None, - # subsegment: aws_xray_sdk.core.models.subsegment = None, + # subsegment: aws_xray_sdk.core.models.subsegment = None, # noqa: E800 capture_response: Optional[bool] = None, ): """Add response as metadata for given subsegment @@ -660,7 +659,7 @@ def _add_full_exception_as_metadata( method_name: str = None, error: Exception = None, subsegment: Any = None, - # subsegment: aws_xray_sdk.core.models.subsegment = None, + # subsegment: aws_xray_sdk.core.models.subsegment = None, # noqa: E800 capture_error: Optional[bool] = None, ): """Add full exception object as metadata for given subsegment @@ -723,7 +722,6 @@ def __build_config( auto_patch: bool = None, patch_modules: List = None, provider: Any = None, - # provider: aws_xray_sdk.core.xray_recorder = None, ): """ Populates Tracer config for new and existing initializations """ is_disabled = disabled if disabled is not None else self._is_tracer_disabled() From 90dab81134b93c22bce8448de213c36924108a5b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 1 Mar 2021 17:51:44 +0100 Subject: [PATCH 07/17] feat: add minimum generic interface for Tracing Provider and Segment --- aws_lambda_powertools/tracing/base.py | 145 ++++++++++++++++++ aws_lambda_powertools/tracing/tracer.py | 41 +++-- .../utilities/parser/envelopes/base.py | 2 +- 3 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 aws_lambda_powertools/tracing/base.py diff --git a/aws_lambda_powertools/tracing/base.py b/aws_lambda_powertools/tracing/base.py new file mode 100644 index 00000000000..1857ed52a73 --- /dev/null +++ b/aws_lambda_powertools/tracing/base.py @@ -0,0 +1,145 @@ +import abc +import numbers +import traceback +from contextlib import contextmanager +from typing import Any, AsyncContextManager, ContextManager, List, NoReturn, Set, Union + + +class BaseProvider(abc.ABC): + @abc.abstractmethod + @contextmanager + def in_subsegment(self, name=None, **kwargs) -> ContextManager: + """Return a subsegment context manger. + + Parameters + ---------- + name: str + Subsegment name + kwargs: Optional[dict] + Optional parameters to be propagated to segment + """ + + @abc.abstractmethod + @contextmanager + def in_subsegment_async(self, name=None, **kwargs) -> AsyncContextManager: + """Return a subsegment async context manger. + + Parameters + ---------- + name: str + Subsegment name + kwargs: Optional[dict] + Optional parameters to be propagated to segment + """ + + @abc.abstractmethod + def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]) -> NoReturn: + """Annotate current active trace entity with a key-value pair. + + Note: Annotations will be indexed for later search query. + + Parameters + ---------- + key: str + Metadata key + value: Union[str, numbers.Number, bool] + Annotation value + """ + + @abc.abstractmethod + def put_metadata(self, key: str, value: Any, namespace: str = "default") -> NoReturn: + """Add metadata to the current active trace entity. + + Note: Metadata is not indexed but can be later retrieved by BatchGetTraces API. + + Parameters + ---------- + key: str + Metadata key + value: Any + Any object that can be serialized into a JSON string + namespace: Set[str] + Metadata namespace, by default 'default' + """ + + @abc.abstractmethod + def patch(self, modules: Set[str]) -> NoReturn: + """Instrument a set of supported libraries + + Parameters + ---------- + modules: Set[str] + Set of modules to be patched + """ + + @abc.abstractmethod + def patch_all(self) -> NoReturn: + """Instrument all supported libraries""" + + +class BaseSegment(abc.ABC): + """Holds common properties and methods on segment and subsegment.""" + + @abc.abstractmethod + def close(self, end_time: int = None): + """Close the trace entity by setting `end_time` + and flip the in progress flag to False. + + Parameters + ---------- + end_time: int + Time in epoch seconds, by default current time will be used. + """ + + @abc.abstractmethod + def add_subsegment(self, subsegment: Any): + """Add input subsegment as a child subsegment.""" + + @abc.abstractmethod + def remove_subsegment(self, subsegment: Any): + """Remove input subsegment from child subsegments.""" + + @abc.abstractmethod + def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]) -> NoReturn: + """Annotate segment or subsegment with a key-value pair. + + Note: Annotations will be indexed for later search query. + + Parameters + ---------- + key: str + Metadata key + value: Union[str, numbers.Number, bool] + Annotation value + """ + + @abc.abstractmethod + def put_metadata(self, key: str, value: Any, namespace: str = "default") -> NoReturn: + """Add metadata to segment or subsegment. Metadata is not indexed + but can be later retrieved by BatchGetTraces API. + + Parameters + ---------- + key: str + Metadata key + value: Any + Any object that can be serialized into a JSON string + namespace: Set[str] + Metadata namespace, by default 'default' + """ + + @abc.abstractmethod + def add_exception(self, exception: BaseException, stack: List[traceback.StackSummary], remote: bool = False): + """Add an exception to trace entities. + + Parameters + ---------- + exception: Exception + Caught exception + stack: List[traceback.StackSummary] + List of traceback summaries + + Output from `traceback.extract_stack()`. + remote: bool + Whether it's a client error (False) or downstream service error (True), by default False + """ diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 4d251b515dd..6e1643dd535 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -3,19 +3,17 @@ import functools import inspect import logging +import numbers import os -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from ..shared import constants from ..shared.functions import resolve_truthy_env_var_choice from ..shared.lazy_import import LazyLoader +from .base import BaseProvider, BaseSegment is_cold_start = True logger = logging.getLogger(__name__) -# Set the streaming threshold to 0 on the default recorder to force sending -# subsegments individually, rather than batching them. -# See https://github.com/awslabs/aws-lambda-powertools-python/issues/283 -# aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE) aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) @@ -135,11 +133,6 @@ def handler(event: dict, context: Any) -> Dict: * Async handler not supported """ - # aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 - # Types - # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/models/subsegment.py - # https://github.com/aws/aws-xray-sdk-python/blob/96134f6d5b0a0be1dc51a9171c3a478a9fe07f79/aws_xray_sdk/core/async_recorder.py - _default_config = { "service": "service_undefined", "disabled": False, @@ -154,10 +147,9 @@ def __init__( service: str = None, disabled: bool = None, auto_patch: bool = None, - patch_modules: List = None, - provider: Any = None, + patch_modules: Optional[Tuple[str]] = None, + provider: BaseProvider = None, ): - self.__build_config( service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider ) @@ -172,14 +164,19 @@ def __init__( if self.auto_patch: self.patch(modules=patch_modules) - def put_annotation(self, key: str, value: Any): + # Set the streaming threshold to 0 on the default recorder to force sending + # subsegments individually, rather than batching them. + # See https://github.com/awslabs/aws-lambda-powertools-python/issues/283 + aws_xray_sdk.core.xray_recorder.configure(streaming_threshold=0) # noqa: E800 + + def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]): """Adds annotation to existing segment or subsegment Parameters ---------- key : str Annotation key - value : any + value : Union[str, numbers.Number, bool] Value for annotation Example @@ -632,8 +629,7 @@ def _add_response_as_metadata( self, method_name: str = None, data: Any = None, - subsegment: Any = None, - # subsegment: aws_xray_sdk.core.models.subsegment = None, # noqa: E800 + subsegment: BaseSegment = None, capture_response: Optional[bool] = None, ): """Add response as metadata for given subsegment @@ -644,7 +640,7 @@ def _add_response_as_metadata( method name to add as metadata key, by default None data : Any, optional data to add as subsegment metadata, by default None - subsegment : aws_xray_sdk.core.models.subsegment, optional + subsegment : BaseSegment, optional existing subsegment to add metadata on, by default None capture_response : bool, optional Do not include response as metadata @@ -658,8 +654,7 @@ def _add_full_exception_as_metadata( self, method_name: str = None, error: Exception = None, - subsegment: Any = None, - # subsegment: aws_xray_sdk.core.models.subsegment = None, # noqa: E800 + subsegment: BaseSegment = None, capture_error: Optional[bool] = None, ): """Add full exception object as metadata for given subsegment @@ -670,7 +665,7 @@ def _add_full_exception_as_metadata( method name to add as metadata key, by default None error : Exception, optional error to add as subsegment metadata, by default None - subsegment : aws_xray_sdk.core.models.subsegment, optional + subsegment : BaseSegment, optional existing subsegment to add metadata on, by default None capture_error : bool, optional Do not include error as metadata, by default True @@ -684,7 +679,7 @@ def _add_full_exception_as_metadata( def _disable_tracer_provider(): """Forcefully disables tracing""" logger.debug("Disabling tracer provider...") - aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) # noqa: F821 + aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) @staticmethod def _is_tracer_disabled() -> bool: @@ -721,7 +716,7 @@ def __build_config( disabled: bool = None, auto_patch: bool = None, patch_modules: List = None, - provider: Any = None, + provider: BaseProvider = None, ): """ Populates Tracer config for new and existing initializations """ is_disabled = disabled if disabled is not None else self._is_tracer_disabled() diff --git a/aws_lambda_powertools/utilities/parser/envelopes/base.py b/aws_lambda_powertools/utilities/parser/envelopes/base.py index 484d589fcc8..06e78160d87 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/base.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/base.py @@ -59,5 +59,5 @@ def parse(...): # Generic to support type annotations throughout parser -# Note: Can't be defined under types.py due to circular dependency +# Note: Can't be defined under base.py due to circular dependency Envelope = TypeVar("Envelope", bound=BaseEnvelope) From 988c6479390ac04e1016ce18287c968d7745f07b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 09:03:03 +0100 Subject: [PATCH 08/17] fix: type hints --- aws_lambda_powertools/tracing/tracer.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 6e1643dd535..f5b9ac92728 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -242,7 +242,7 @@ def patch(self, modules: Tuple[str] = None): def capture_lambda_handler( self, - lambda_handler: Callable[[Dict, Any], Any] = None, + lambda_handler: Callable[[Dict, Any, Optional[Dict]], Any] = None, capture_response: Optional[bool] = None, capture_error: Optional[bool] = None, ): @@ -516,8 +516,8 @@ async def async_tasks(): def _decorate_async_function( self, method: Callable = None, - capture_response: Optional[bool] = None, - capture_error: Optional[bool] = None, + capture_response: Optional[Union[bool, str]] = None, + capture_error: Optional[Union[bool, str]] = None, method_name: str = None, ): @functools.wraps(method) @@ -543,8 +543,8 @@ async def decorate(*args, **kwargs): def _decorate_generator_function( self, method: Callable = None, - capture_response: Optional[bool] = None, - capture_error: Optional[bool] = None, + capture_response: Optional[Union[bool, str]] = None, + capture_error: Optional[Union[bool, str]] = None, method_name: str = None, ): @functools.wraps(method) @@ -570,8 +570,8 @@ def decorate(*args, **kwargs): def _decorate_generator_function_with_context_manager( self, method: Callable = None, - capture_response: Optional[bool] = None, - capture_error: Optional[bool] = None, + capture_response: Optional[Union[bool, str]] = None, + capture_error: Optional[Union[bool, str]] = None, method_name: str = None, ): @functools.wraps(method) @@ -598,8 +598,8 @@ def decorate(*args, **kwargs): def _decorate_sync_function( self, method: Callable = None, - capture_response: Optional[bool] = None, - capture_error: Optional[bool] = None, + capture_response: Optional[Union[bool, str]] = None, + capture_error: Optional[Union[bool, str]] = None, method_name: str = None, ): @functools.wraps(method) @@ -630,7 +630,7 @@ def _add_response_as_metadata( method_name: str = None, data: Any = None, subsegment: BaseSegment = None, - capture_response: Optional[bool] = None, + capture_response: Optional[Union[bool, str]] = None, ): """Add response as metadata for given subsegment @@ -682,7 +682,7 @@ def _disable_tracer_provider(): aws_xray_sdk.global_sdk_config.set_sdk_enabled(False) @staticmethod - def _is_tracer_disabled() -> bool: + def _is_tracer_disabled() -> Union[bool, str]: """Detects whether trace has been disabled Tracing is automatically disabled in the following conditions: @@ -693,7 +693,7 @@ def _is_tracer_disabled() -> bool: Returns ------- - bool + Union[bool, str] """ logger.debug("Verifying whether Tracing has been disabled") is_lambda_sam_cli = os.getenv(constants.SAM_LOCAL_ENV) From 8b6e1e8b4740f6990532ee4bdb087485504a3f14 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 10:44:11 +0100 Subject: [PATCH 09/17] refactor: use JSON Schema as dict to reduce I/O latency --- aws_lambda_powertools/metrics/base.py | 6 +- .../metrics/{schema.json => schema.py} | 144 ++++++++---------- tests/functional/test_metrics.py | 6 +- 3 files changed, 66 insertions(+), 90 deletions(-) rename aws_lambda_powertools/metrics/{schema.json => schema.py} (68%) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index ecc44edf0fa..bddfc41d40d 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -3,7 +3,6 @@ import logging import numbers import os -import pathlib from collections import defaultdict from enum import Enum from typing import Any, Dict, List, Union @@ -13,13 +12,10 @@ from ..shared import constants from ..shared.functions import resolve_env_var_choice from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError +from .schema import CLOUDWATCH_EMF_SCHEMA logger = logging.getLogger(__name__) -_schema_path = pathlib.Path(__file__).parent / "./schema.json" -with _schema_path.open() as f: - CLOUDWATCH_EMF_SCHEMA = json.load(f) - MAX_METRICS = 100 diff --git a/aws_lambda_powertools/metrics/schema.json b/aws_lambda_powertools/metrics/schema.py similarity index 68% rename from aws_lambda_powertools/metrics/schema.json rename to aws_lambda_powertools/metrics/schema.py index f948ed979fa..9d6a033c618 100644 --- a/aws_lambda_powertools/metrics/schema.json +++ b/aws_lambda_powertools/metrics/schema.py @@ -1,114 +1,94 @@ -{ - "type": "object", - "title": "Root Node", - "required": [ - "_aws" - ], +# flake8: noqa +CLOUDWATCH_EMF_SCHEMA = { "properties": { "_aws": { "$id": "#/properties/_aws", - "type": "object", - "title": "Metadata", - "required": [ - "Timestamp", - "CloudWatchMetrics" - ], "properties": { - "Timestamp": { - "$id": "#/properties/_aws/properties/Timestamp", - "type": "integer", - "title": "The Timestamp Schema", - "examples": [ - 1565375354953 - ] - }, "CloudWatchMetrics": { "$id": "#/properties/_aws/properties/CloudWatchMetrics", - "type": "array", - "title": "MetricDirectives", "items": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items", - "type": "object", - "title": "MetricDirective", - "required": [ - "Namespace", - "Dimensions", - "Metrics" - ], "properties": { - "Namespace": { - "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace", - "type": "string", - "title": "CloudWatch Metrics Namespace", - "examples": [ - "MyApp" - ], - "pattern": "^(.*)$", - "minLength": 1 - }, "Dimensions": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions", - "type": "array", - "title": "The Dimensions Schema", - "minItems": 1, "items": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items", - "type": "array", - "title": "DimensionSet", - "minItems": 1, - "maxItems": 9, "items": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items/items", - "type": "string", - "title": "DimensionReference", - "examples": [ - "Operation" - ], + "examples": ["Operation"], + "minItems": 1, "pattern": "^(.*)$", - "minItems": 1 - } - } + "title": "DimensionReference", + "type": "string", + }, + "maxItems": 9, + "minItems": 1, + "title": "DimensionSet", + "type": "array", + }, + "minItems": 1, + "title": "The " "Dimensions " "Schema", + "type": "array", }, "Metrics": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics", - "type": "array", - "title": "MetricDefinitions", - "minItems": 1, "items": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items", - "type": "object", - "title": "MetricDefinition", - "required": [ - "Name" - ], "minItems": 1, "properties": { "Name": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Name", - "type": "string", - "title": "MetricName", - "examples": [ - "ProcessingLatency" - ], + "examples": ["ProcessingLatency"], + "minLength": 1, "pattern": "^(.*)$", - "minLength": 1 + "title": "MetricName", + "type": "string", }, "Unit": { "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Unit", - "type": "string", + "examples": ["Milliseconds"], + "pattern": "^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$", "title": "MetricUnit", - "examples": [ - "Milliseconds" - ], - "pattern": "^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$" - } - } - } - } - } - } - } - } + "type": "string", + }, + }, + "required": ["Name"], + "title": "MetricDefinition", + "type": "object", + }, + "minItems": 1, + "title": "MetricDefinitions", + "type": "array", + }, + "Namespace": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace", + "examples": ["MyApp"], + "minLength": 1, + "pattern": "^(.*)$", + "title": "CloudWatch " "Metrics " "Namespace", + "type": "string", + }, + }, + "required": ["Namespace", "Dimensions", "Metrics"], + "title": "MetricDirective", + "type": "object", + }, + "title": "MetricDirectives", + "type": "array", + }, + "Timestamp": { + "$id": "#/properties/_aws/properties/Timestamp", + "examples": [1565375354953], + "title": "The Timestamp " "Schema", + "type": "integer", + }, + }, + "required": ["Timestamp", "CloudWatchMetrics"], + "title": "Metadata", + "type": "object", } - } + }, + "required": ["_aws"], + "title": "Root Node", + "type": "object", } diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index a3d471ab305..a8dc4e61656 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -258,8 +258,8 @@ def test_schema_validation_no_namespace(metric, dimension): # WHEN we attempt to serialize a valid EMF object # THEN it should fail namespace validation with pytest.raises(SchemaValidationError, match=".*Namespace must be string"): - with single_metric(**metric): - pass + with single_metric(**metric) as my_metric: + my_metric.add_dimension(**dimension) def test_schema_validation_incorrect_metric_value(metric, dimension, namespace): @@ -268,7 +268,7 @@ def test_schema_validation_incorrect_metric_value(metric, dimension, namespace): # WHEN we attempt to serialize a valid EMF object # THEN it should fail validation and raise SchemaValidationError - with pytest.raises(MetricValueError): + with pytest.raises(MetricValueError, match=".*is not a valid number"): with single_metric(**metric): pass From ec768414d3f7231673c4703eff0fdb1f31c519a7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 10:48:20 +0100 Subject: [PATCH 10/17] docs: changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75aa5a18c45..3e77d594c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) fo ## [Unreleased] +### Fixed + +* **Tracer**: Lazy loads X-Ray SDK to improve import perf for those not instantiating Tracer +* **Metrics**: Convert EMF JSON Schema as Dictionary to reduce I/O and improve import perf + ## [1.10.5] - 2021-02-17 No changes. Bumped version to trigger new pipeline build for layer publishing. From 79fa14d24b0f82595372beb4b2bfb7c66ee20818 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 14:42:01 +0100 Subject: [PATCH 11/17] test: add perf tests for import --- Makefile | 3 +- pytest.ini | 2 + tests/performance/__init__.py | 0 tests/performance/test_high_level_imports.py | 86 ++++++++++++++++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/performance/__init__.py create mode 100644 tests/performance/test_high_level_imports.py diff --git a/Makefile b/Makefile index 27a2896d812..6fb309f87d4 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,8 @@ lint: format poetry run flake8 aws_lambda_powertools/* tests/* test: - poetry run pytest -vvv --cov=./ --cov-report=xml + poetry run pytest -vvv -m "not perf" --cov=./ --cov-report=xml + poetry run pytest --cache-clear -vvv tests/performance coverage-html: poetry run pytest --cov-report html diff --git a/pytest.ini b/pytest.ini index 45345cbd365..af5bfcdd3a5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,5 @@ [pytest] addopts = -ra --cov --cov-config=.coveragerc testpaths = ./tests +markers = + perf: marks perf tests to be deselected (deselect with '-m "not perf"') diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/performance/test_high_level_imports.py b/tests/performance/test_high_level_imports.py new file mode 100644 index 00000000000..edfcea23846 --- /dev/null +++ b/tests/performance/test_high_level_imports.py @@ -0,0 +1,86 @@ +import importlib +import time +from contextlib import contextmanager +from types import ModuleType +from typing import Generator, Tuple + +import pytest + + +@contextmanager +def timing() -> Generator: + """ "Generator to quickly time operations. It can add 5ms so take that into account in elapsed time + + Examples + -------- + + with timing() as t: + print("something") + elapsed = t() + """ + start = time.perf_counter() + yield lambda: time.perf_counter() - start # gen as lambda to calculate elapsed time + + +def core_utilities() -> Tuple[ModuleType, ModuleType, ModuleType]: + """Return Tracing, Logging, and Metrics module""" + tracing = importlib.import_module("aws_lambda_powertools.tracing") + logging = importlib.import_module("aws_lambda_powertools.logging") + metrics = importlib.import_module("aws_lambda_powertools.metrics") + + return tracing, logging, metrics + + +@pytest.mark.perf +def test_import_times_ceiling(): + # GIVEN Core utilities are imported + # WHEN none are used + # THEN import and any global initialization perf should be below 30ms + with timing() as t: + core_utilities() + + elapsed = t() + if elapsed > 0.030: + pytest.fail(f"High level imports should be below 40ms: {elapsed}") + + +@pytest.mark.perf +def test_tracer_init(): + # GIVEN Tracer is initialized + # WHEN default options are used + # THEN initialization X-Ray SDK perf should be below 400ms + with timing() as t: + tracing, _, _ = core_utilities() + tracing.Tracer() # boto3 takes ~200ms, and remaining is X-Ray SDK init + + elapsed = t() + if elapsed > 0.4: + pytest.fail(f"High level imports should be below 40ms: {elapsed}") + + +@pytest.mark.perf +def test_metrics_init(): + # GIVEN Metrics is initialized + # WHEN default options are used + # THEN initialization perf should be below 5ms + with timing() as t: + _, _, metrics = core_utilities() + metrics.Metrics() + + elapsed = t() + if elapsed > 0.005: + pytest.fail(f"High level imports should be below 40ms: {elapsed}") + + +@pytest.mark.perf +def test_logger_init(): + # GIVEN Logger is initialized + # WHEN default options are used + # THEN initialization perf should be below 5ms + with timing() as t: + _, logging, _ = core_utilities() + logging.Logger() + + elapsed = t() + if elapsed > 0.001: + pytest.fail(f"High level imports should be below 40ms: {elapsed}") From 28c12eecc984577ebfa6fd446518fb62caddfb4b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 14:55:07 +0100 Subject: [PATCH 12/17] test: adjust perf bar to flaky/CI machines --- tests/performance/test_high_level_imports.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/performance/test_high_level_imports.py b/tests/performance/test_high_level_imports.py index edfcea23846..e8ca7af4f8c 100644 --- a/tests/performance/test_high_level_imports.py +++ b/tests/performance/test_high_level_imports.py @@ -36,11 +36,13 @@ def test_import_times_ceiling(): # GIVEN Core utilities are imported # WHEN none are used # THEN import and any global initialization perf should be below 30ms + # though we adjust to 35ms to take into account different CI machines, etc. + # instead of re-running tests which can lead to false positives with timing() as t: core_utilities() elapsed = t() - if elapsed > 0.030: + if elapsed > 0.035: pytest.fail(f"High level imports should be below 40ms: {elapsed}") @@ -48,13 +50,15 @@ def test_import_times_ceiling(): def test_tracer_init(): # GIVEN Tracer is initialized # WHEN default options are used - # THEN initialization X-Ray SDK perf should be below 400ms + # THEN initialization X-Ray SDK perf should be below 450ms + # though we adjust to 500ms to take into account different CI machines, etc. + # instead of re-running tests which can lead to false positives with timing() as t: tracing, _, _ = core_utilities() - tracing.Tracer() # boto3 takes ~200ms, and remaining is X-Ray SDK init + tracing.Tracer(disabled=True) # boto3 takes ~200ms, and remaining is X-Ray SDK init elapsed = t() - if elapsed > 0.4: + if elapsed > 0.5: pytest.fail(f"High level imports should be below 40ms: {elapsed}") From 33518bad8d5073819cb2483bd9350659afb0f797 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 2 Mar 2021 16:15:22 +0100 Subject: [PATCH 13/17] fix(pytest): enforce coverage upon request only Signed-off-by: heitorlessa --- Makefile | 4 ++-- pytest.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 6fb309f87d4..e56eb4bb266 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,8 @@ lint: format poetry run flake8 aws_lambda_powertools/* tests/* test: - poetry run pytest -vvv -m "not perf" --cov=./ --cov-report=xml - poetry run pytest --cache-clear -vvv tests/performance + poetry run pytest -m "not perf" --cov=./ --cov-report=xml + poetry run pytest --cache-clear tests/performance coverage-html: poetry run pytest --cov-report html diff --git a/pytest.ini b/pytest.ini index af5bfcdd3a5..4f01361ce5e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -ra --cov --cov-config=.coveragerc +addopts = -ra -vvv testpaths = ./tests markers = perf: marks perf tests to be deselected (deselect with '-m "not perf"') From 538af2acc1b3ca68acfa007006ddaffb39ad9d4b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 3 Mar 2021 09:17:55 +0100 Subject: [PATCH 14/17] chore: address PR's review --- tests/performance/test_high_level_imports.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/performance/test_high_level_imports.py b/tests/performance/test_high_level_imports.py index e8ca7af4f8c..70a8d993bdf 100644 --- a/tests/performance/test_high_level_imports.py +++ b/tests/performance/test_high_level_imports.py @@ -6,6 +6,11 @@ import pytest +LOGGER_INIT_SLA: float = 0.001 +METRICS_INIT_SLA: float = 0.005 +TRACER_INIT_SLA: float = 0.5 +IMPORT_INIT_SLA: float = 0.035 + @contextmanager def timing() -> Generator: @@ -42,8 +47,8 @@ def test_import_times_ceiling(): core_utilities() elapsed = t() - if elapsed > 0.035: - pytest.fail(f"High level imports should be below 40ms: {elapsed}") + if elapsed > IMPORT_INIT_SLA: + pytest.fail(f"High level imports should be below 35ms: {elapsed}") @pytest.mark.perf @@ -58,8 +63,8 @@ def test_tracer_init(): tracing.Tracer(disabled=True) # boto3 takes ~200ms, and remaining is X-Ray SDK init elapsed = t() - if elapsed > 0.5: - pytest.fail(f"High level imports should be below 40ms: {elapsed}") + if elapsed > TRACER_INIT_SLA: + pytest.fail(f"High level imports should be below 50ms: {elapsed}") @pytest.mark.perf @@ -72,7 +77,7 @@ def test_metrics_init(): metrics.Metrics() elapsed = t() - if elapsed > 0.005: + if elapsed > METRICS_INIT_SLA: pytest.fail(f"High level imports should be below 40ms: {elapsed}") @@ -86,5 +91,5 @@ def test_logger_init(): logging.Logger() elapsed = t() - if elapsed > 0.001: + if elapsed > LOGGER_INIT_SLA: pytest.fail(f"High level imports should be below 40ms: {elapsed}") From c586b3c038ebb1f92d2dd04afa831cfb5a7f6c71 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 3 Mar 2021 09:44:51 +0100 Subject: [PATCH 15/17] chore: correctly redistribute apache 2.0 unmodified code --- LICENSE | 179 ++++++++++++++++++++ aws_lambda_powertools/shared/lazy_import.py | 17 ++ 2 files changed, 196 insertions(+) diff --git a/LICENSE b/LICENSE index 9e30e05ab6d..17c63bac7fb 100644 --- a/LICENSE +++ b/LICENSE @@ -12,3 +12,182 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +---------------- + +** Tensorflow - https://github.com/tensorflow/tensorflow/ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/aws_lambda_powertools/shared/lazy_import.py b/aws_lambda_powertools/shared/lazy_import.py index 42ace91a719..e860a650f31 100644 --- a/aws_lambda_powertools/shared/lazy_import.py +++ b/aws_lambda_powertools/shared/lazy_import.py @@ -1,3 +1,20 @@ +# Copyright 2015 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""A LazyLoader class.""" + import importlib import types From 01445a3a2e14c973092a2e6641c577751a00b7cb Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 3 Mar 2021 09:58:20 +0100 Subject: [PATCH 16/17] chore: test labeler From 50c89e90d42abdc36a5ca0a4e026be2d19b728f8 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 3 Mar 2021 10:17:47 +0100 Subject: [PATCH 17/17] refactor: lazy load fastjsonschema to prevent unnecessary http.client sessions --- aws_lambda_powertools/metrics/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index bddfc41d40d..281070323d7 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -7,13 +7,13 @@ from enum import Enum from typing import Any, Dict, List, Union -import fastjsonschema - from ..shared import constants from ..shared.functions import resolve_env_var_choice +from ..shared.lazy_import import LazyLoader from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError from .schema import CLOUDWATCH_EMF_SCHEMA +fastjsonschema = LazyLoader("fastjsonschema", globals(), "fastjsonschema") logger = logging.getLogger(__name__) MAX_METRICS = 100