From 9fe0d9ec154fc84196141d57a994be74d3a7845f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 15 Jan 2021 17:32:00 +0100 Subject: [PATCH 1/2] chore: shift vars to constant to ease changing --- aws_lambda_powertools/logging/formatter.py | 4 +- aws_lambda_powertools/logging/logger.py | 18 +++++--- aws_lambda_powertools/metrics/base.py | 6 ++- .../middleware_factory/factory.py | 9 ++-- aws_lambda_powertools/shared/constants.py | 17 ++++++-- aws_lambda_powertools/shared/functions.py | 6 ++- aws_lambda_powertools/tracing/tracer.py | 35 +++++++++------- tests/functional/test_logger.py | 6 +-- tests/functional/test_shared_functions.py | 6 +-- tests/unit/test_tracing.py | 41 ++++++++++++++++--- 10 files changed, 106 insertions(+), 42 deletions(-) diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py index 063b97ab21c..cfda64bc8e6 100644 --- a/aws_lambda_powertools/logging/formatter.py +++ b/aws_lambda_powertools/logging/formatter.py @@ -3,6 +3,8 @@ import os from typing import Dict, Iterable, Optional, Union +from ..shared import constants + STD_LOGGING_KEYS = ( "name", "msg", @@ -73,7 +75,7 @@ def _build_root_keys(**kwargs): @staticmethod def _get_latest_trace_id(): - xray_trace_id = os.getenv("_X_AMZN_TRACE_ID") + xray_trace_id = os.getenv(constants.XRAY_TRACE_ID_ENV) return xray_trace_id.split(";")[0].replace("Root=", "") if xray_trace_id else None def update_formatter(self, **kwargs): diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index e03f542e6c6..bc44b14b1e5 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -4,9 +4,10 @@ import os import random import sys -from distutils.util import strtobool from typing import Any, Callable, Dict, Union +from ..shared import constants +from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice from .exceptions import InvalidLoggerSamplingRateError from .filters import SuppressFilter from .formatter import JsonFormatter @@ -122,8 +123,12 @@ def __init__( stream: sys.stdout = None, **kwargs, ): - self.service = service or os.getenv("POWERTOOLS_SERVICE_NAME") or "service_undefined" - self.sampling_rate = sampling_rate or os.getenv("POWERTOOLS_LOGGER_SAMPLE_RATE") or 0.0 + self.service = resolve_env_var_choice( + choice=service, env=os.getenv(constants.SERVICE_NAME_ENV, "service_undefined") + ) + self.sampling_rate = resolve_env_var_choice( + choice=sampling_rate, env=os.getenv(constants.LOGGER_LOG_SAMPLING_RATE, 0.0) + ) self.log_level = self._get_log_level(level) self.child = child self._handler = logging.StreamHandler(stream) if stream is not None else logging.StreamHandler(sys.stdout) @@ -193,7 +198,7 @@ def _configure_sampling(self): f"Please review POWERTOOLS_LOGGER_SAMPLE_RATE environment variable." ) - def inject_lambda_context(self, lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = False): + def inject_lambda_context(self, lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = None): """Decorator to capture Lambda contextual info and inject into logger Parameters @@ -242,8 +247,9 @@ def handler(event, context): logger.debug("Decorator called with parameters") return functools.partial(self.inject_lambda_context, log_event=log_event) - log_event_env_option = str(os.getenv("POWERTOOLS_LOGGER_LOG_EVENT", "false")) - log_event = strtobool(log_event_env_option) or log_event + log_event = resolve_truthy_env_var_choice( + choice=log_event, env=os.getenv(constants.LOGGER_LOG_EVENT_ENV, "false") + ) @functools.wraps(lambda_handler) def decorate(event, context): diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index b54b72bf58a..ecc44edf0fa 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -10,6 +10,8 @@ import fastjsonschema +from ..shared import constants +from ..shared.functions import resolve_env_var_choice from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError logger = logging.getLogger(__name__) @@ -88,8 +90,8 @@ def __init__( ): self.metric_set = metric_set if metric_set is not None else {} self.dimension_set = dimension_set if dimension_set is not None else {} - self.namespace = namespace or os.getenv("POWERTOOLS_METRICS_NAMESPACE") - self.service = service or os.environ.get("POWERTOOLS_SERVICE_NAME") + self.namespace = resolve_env_var_choice(choice=namespace, env=os.getenv(constants.METRICS_NAMESPACE_ENV)) + self.service = resolve_env_var_choice(choice=service, env=os.getenv(constants.SERVICE_NAME_ENV)) self._metric_units = [unit.value for unit in MetricUnit] self._metric_unit_options = list(MetricUnit.__members__) self.metadata_set = self.metadata_set if metadata_set is not None else {} diff --git a/aws_lambda_powertools/middleware_factory/factory.py b/aws_lambda_powertools/middleware_factory/factory.py index d71c2e19d67..77277052272 100644 --- a/aws_lambda_powertools/middleware_factory/factory.py +++ b/aws_lambda_powertools/middleware_factory/factory.py @@ -2,16 +2,17 @@ import inspect import logging import os -from distutils.util import strtobool from typing import Callable +from ..shared import constants +from ..shared.functions import resolve_truthy_env_var_choice from ..tracing import Tracer from .exceptions import MiddlewareInvalidArgumentError logger = logging.getLogger(__name__) -def lambda_handler_decorator(decorator: Callable = None, trace_execution=False): +def lambda_handler_decorator(decorator: Callable = None, trace_execution: bool = None): """Decorator factory for decorating Lambda handlers. You can use lambda_handler_decorator to create your own middlewares, @@ -104,7 +105,9 @@ def lambda_handler(event, context): if decorator is None: return functools.partial(lambda_handler_decorator, trace_execution=trace_execution) - trace_execution = trace_execution or strtobool(str(os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False))) + trace_execution = resolve_truthy_env_var_choice( + choice=trace_execution, env=os.getenv(constants.MIDDLEWARE_FACTORY_TRACE_ENV, "false") + ) @functools.wraps(decorator) def final_decorator(func: Callable = None, **kwargs): diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index a3863c33286..27ab3c34197 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -1,4 +1,15 @@ -import os +TRACER_CAPTURE_RESPONSE_ENV: str = "POWERTOOLS_TRACER_CAPTURE_RESPONSE" +TRACER_CAPTURE_ERROR_ENV: str = "POWERTOOLS_TRACER_CAPTURE_ERROR" +TRACER_DISABLED_ENV: str = "POWERTOOLS_TRACE_DISABLED" -TRACER_CAPTURE_RESPONSE_ENV: str = os.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true") -TRACER_CAPTURE_ERROR_ENV: str = os.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR", "true") +LOGGER_LOG_SAMPLING_RATE: str = "POWERTOOLS_LOGGER_SAMPLE_RATE" +LOGGER_LOG_EVENT_ENV: str = "POWERTOOLS_LOGGER_LOG_EVENT" + +MIDDLEWARE_FACTORY_TRACE_ENV: str = "POWERTOOLS_TRACE_MIDDLEWARES" + +METRICS_NAMESPACE_ENV: str = "POWERTOOLS_METRICS_NAMESPACE" + +SAM_LOCAL_ENV: str = "AWS_SAM_LOCAL" +CHALICE_LOCAL_ENV: str = "AWS_CHALICE_CLI_MODE" +SERVICE_NAME_ENV: str = "POWERTOOLS_SERVICE_NAME" +XRAY_TRACE_ID_ENV: str = "_X_AMZN_TRACE_ID" diff --git a/aws_lambda_powertools/shared/functions.py b/aws_lambda_powertools/shared/functions.py index 2a3af7db5f3..e6a8ff10975 100644 --- a/aws_lambda_powertools/shared/functions.py +++ b/aws_lambda_powertools/shared/functions.py @@ -1,5 +1,9 @@ from distutils.util import strtobool -def resolve_env_var_choice(env: str, choice: bool = None) -> bool: +def resolve_truthy_env_var_choice(env: str, choice: bool = None) -> bool: return choice if choice is not None else strtobool(env) + + +def resolve_env_var_choice(env: str, choice: bool = None) -> bool: + return choice if choice is not None else env diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 2e083e5ebab..26e12a0fb63 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -4,14 +4,13 @@ import inspect import logging import os -from distutils.util import strtobool from typing import Any, Callable, Dict, List, Optional, Tuple import aws_xray_sdk import aws_xray_sdk.core -from aws_lambda_powertools.shared.constants import TRACER_CAPTURE_ERROR_ENV, TRACER_CAPTURE_RESPONSE_ENV -from aws_lambda_powertools.shared.functions import resolve_env_var_choice +from ..shared import constants +from ..shared.functions import resolve_truthy_env_var_choice is_cold_start = True logger = logging.getLogger(__name__) @@ -283,9 +282,12 @@ def handler(event, context): ) lambda_handler_name = lambda_handler.__name__ - - capture_response = resolve_env_var_choice(env=TRACER_CAPTURE_RESPONSE_ENV, choice=capture_response) - capture_error = resolve_env_var_choice(env=TRACER_CAPTURE_ERROR_ENV, choice=capture_error) + capture_response = resolve_truthy_env_var_choice( + env=os.getenv(constants.TRACER_CAPTURE_RESPONSE_ENV, "true"), choice=capture_response + ) + capture_error = resolve_truthy_env_var_choice( + env=os.getenv(constants.TRACER_CAPTURE_ERROR_ENV, "true"), choice=capture_error + ) @functools.wraps(lambda_handler) def decorate(event, context): @@ -478,8 +480,12 @@ async def async_tasks(): method_name = f"{method.__name__}" - capture_response = resolve_env_var_choice(env=TRACER_CAPTURE_RESPONSE_ENV, choice=capture_response) - capture_error = resolve_env_var_choice(env=TRACER_CAPTURE_ERROR_ENV, choice=capture_error) + capture_response = resolve_truthy_env_var_choice( + env=os.getenv(constants.TRACER_CAPTURE_RESPONSE_ENV, "true"), choice=capture_response + ) + capture_error = resolve_truthy_env_var_choice( + env=os.getenv(constants.TRACER_CAPTURE_ERROR_ENV, "true"), choice=capture_error + ) if inspect.iscoroutinefunction(method): return self._decorate_async_function( @@ -681,14 +687,13 @@ def _is_tracer_disabled() -> bool: bool """ logger.debug("Verifying whether Tracing has been disabled") - is_lambda_sam_cli = os.getenv("AWS_SAM_LOCAL") - is_chalice_cli = os.getenv("AWS_CHALICE_CLI_MODE") - env_option = str(os.getenv("POWERTOOLS_TRACE_DISABLED", "false")) - disabled_env = strtobool(env_option) + is_lambda_sam_cli = os.getenv(constants.SAM_LOCAL_ENV) + is_chalice_cli = os.getenv(constants.CHALICE_LOCAL_ENV) + is_disabled = resolve_truthy_env_var_choice(env=os.getenv(constants.TRACER_DISABLED_ENV, "false")) - if disabled_env: + if is_disabled: logger.debug("Tracing has been disabled via env var POWERTOOLS_TRACE_DISABLED") - return disabled_env + return is_disabled if is_lambda_sam_cli or is_chalice_cli: logger.debug("Running under SAM CLI env or not in Lambda env; disabling Tracing") @@ -706,7 +711,7 @@ def __build_config( ): """ Populates Tracer config for new and existing initializations """ is_disabled = disabled if disabled is not None else self._is_tracer_disabled() - is_service = service if service is not None else os.getenv("POWERTOOLS_SERVICE_NAME") + 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["auto_patch"] = auto_patch if auto_patch is not None else self._config["auto_patch"] diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index 8e8025a6cb8..6c7862896a3 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -373,9 +373,9 @@ def test_logger_do_not_log_twice_when_root_logger_is_setup(stdout, service_name) # WHEN we create a new Logger and child Logger logger = Logger(service=service_name, stream=stdout) - child_logger = Logger(child=True, stream=stdout) - logger.info("hello") - child_logger.info("hello again") + child_logger = Logger(service=service_name, child=True, stream=stdout) + logger.info("PARENT") + child_logger.info("CHILD") # THEN it should only contain only two log entries # since child's log records propagated to root logger should be rejected diff --git a/tests/functional/test_shared_functions.py b/tests/functional/test_shared_functions.py index ac05babb753..efc09262af7 100644 --- a/tests/functional/test_shared_functions.py +++ b/tests/functional/test_shared_functions.py @@ -1,9 +1,9 @@ -from aws_lambda_powertools.shared.functions import resolve_env_var_choice +from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice def test_resolve_env_var_choice_explicit_wins_over_env_var(): - assert resolve_env_var_choice(env="true", choice=False) is False + assert resolve_truthy_env_var_choice(env="true", choice=False) is False def test_resolve_env_var_choice_env_wins_over_absent_explicit(): - assert resolve_env_var_choice(env="true") == 1 + assert resolve_truthy_env_var_choice(env="true") == 1 diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py index 71188e216ec..fdfdf5c6d6e 100644 --- a/tests/unit/test_tracing.py +++ b/tests/unit/test_tracing.py @@ -506,11 +506,9 @@ def test_tracer_lambda_handler_override_response_as_metadata(mocker, provider_st # GIVEN tracer is initialized provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) - mocker.patch("aws_lambda_powertools.tracing.tracer.TRACER_CAPTURE_RESPONSE_ENV", return_value=True) tracer = Tracer(provider=provider, auto_patch=False) - # WHEN capture_lambda_handler decorator is used - # with capture_response set to False + # WHEN capture_lambda_handler decorator is used with capture_response set to False @tracer.capture_lambda_handler(capture_response=False) def handler(event, context): return "response" @@ -526,8 +524,7 @@ def test_tracer_method_override_response_as_metadata(provider_stub, in_subsegmen provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, auto_patch=False) - # WHEN capture_method decorator is used - # and the method response is empty + # WHEN capture_method decorator is used with capture_response set to False @tracer.capture_method(capture_response=False) def greeting(name, message): return "response" @@ -536,3 +533,37 @@ def greeting(name, message): # THEN we should not add any metadata assert in_subsegment_mock.put_metadata.call_count == 0 + + +def test_tracer_lambda_handler_override_error_as_metadata(mocker, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized + provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) + tracer = Tracer(provider=provider, auto_patch=False) + + # WHEN capture_lambda_handler decorator is used with capture_error set to False + @tracer.capture_lambda_handler(capture_error=False) + def handler(event, context): + raise ValueError("error") + + with pytest.raises(ValueError): + handler({}, mocker.MagicMock()) + + # THEN we should not add any metadata + assert in_subsegment_mock.put_metadata.call_count == 0 + + +def test_tracer_method_override_error_as_metadata(provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized + provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) + tracer = Tracer(provider=provider, auto_patch=False) + + # WHEN capture_method decorator is used with capture_error set to False + @tracer.capture_method(capture_error=False) + def greeting(name, message): + raise ValueError("error") + + with pytest.raises(ValueError): + greeting(name="Foo", message="Bar") + + # THEN we should not add any metadata + assert in_subsegment_mock.put_metadata.call_count == 0 From 4514cd072801198163cd2f810135e0aa549b2a50 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 15 Jan 2021 17:42:25 +0100 Subject: [PATCH 2/2] improv: update tests to include non-truthy choice var --- aws_lambda_powertools/shared/functions.py | 37 +++++++++++++++++++++-- tests/functional/test_shared_functions.py | 4 ++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/shared/functions.py b/aws_lambda_powertools/shared/functions.py index e6a8ff10975..c8744143832 100644 --- a/aws_lambda_powertools/shared/functions.py +++ b/aws_lambda_powertools/shared/functions.py @@ -1,9 +1,42 @@ from distutils.util import strtobool +from typing import Any, Union -def resolve_truthy_env_var_choice(env: str, choice: bool = None) -> bool: +def resolve_truthy_env_var_choice(env: Any, choice: bool = None) -> bool: + """ Pick explicit choice over truthy env value, if available, otherwise return truthy env value + + NOTE: Environment variable should be resolved by the caller. + + Parameters + ---------- + env : Any + environment variable actual value + choice : bool + explicit choice + + Returns + ------- + choice : str + resolved choice as either bool or environment value + """ return choice if choice is not None else strtobool(env) -def resolve_env_var_choice(env: str, choice: bool = None) -> bool: +def resolve_env_var_choice(env: Any, choice: bool = None) -> Union[bool, Any]: + """ Pick explicit choice over env, if available, otherwise return env value received + + NOTE: Environment variable should be resolved by the caller. + + Parameters + ---------- + env : Any + environment variable actual value + choice : bool + explicit choice + + Returns + ------- + choice : str + resolved choice as either bool or environment value + """ return choice if choice is not None else env diff --git a/tests/functional/test_shared_functions.py b/tests/functional/test_shared_functions.py index efc09262af7..cc4fd77fbe5 100644 --- a/tests/functional/test_shared_functions.py +++ b/tests/functional/test_shared_functions.py @@ -1,9 +1,11 @@ -from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice +from aws_lambda_powertools.shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice def test_resolve_env_var_choice_explicit_wins_over_env_var(): assert resolve_truthy_env_var_choice(env="true", choice=False) is False + assert resolve_env_var_choice(env="something", choice=False) is False def test_resolve_env_var_choice_env_wins_over_absent_explicit(): assert resolve_truthy_env_var_choice(env="true") == 1 + assert resolve_env_var_choice(env="something") == "something"