diff --git a/python/HISTORY.md b/python/HISTORY.md index 41120a69554..9d9d296c1c2 100644 --- a/python/HISTORY.md +++ b/python/HISTORY.md @@ -1,5 +1,12 @@ # HISTORY +## April 20th, 2020 + +**0.7.0** + +* Introduces Middleware Factory to build your own middleware +* Fixes Metrics dimensions not being included correctly in EMF + ## April 9th, 2020 **0.6.3** diff --git a/python/Makefile b/python/Makefile index b3c1cbd476f..fac2a8af791 100644 --- a/python/Makefile +++ b/python/Makefile @@ -17,7 +17,7 @@ lint: format test: poetry run pytest -vvv -test-html: +coverage-html: poetry run pytest --cov-report html pr: lint test diff --git a/python/README.md b/python/README.md index d312db8028a..5a96dc80b63 100644 --- a/python/README.md +++ b/python/README.md @@ -1,6 +1,6 @@ # Lambda Powertools -![PackageStatus](https://img.shields.io/static/v1?label=status&message=beta&color=blueviolet?style=flat-square) ![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) +![PackageStatus](https://img.shields.io/static/v1?label=status&message=beta&color=blueviolet?style=flat-square) ![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) ![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master) A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier - Currently available for Python only and compatible with Python >=3.6. @@ -32,12 +32,20 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, * Validate against common metric definitions mistakes (metric unit, values, max dimensions, max metrics, etc) * No stack, custom resource, data collection needed — Metrics are created async by CloudWatch EMF +**Bring your own middleware** + +* Utility to easily create your own middleware +* Run logic before, after, and handle exceptions +* Receive lambda handler, event, context +* Optionally create sub-segment for each custom middleware + **Environment variables** used across suite of utilities Environment variable | Description | Default | Utility ------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- POWERTOOLS_SERVICE_NAME | Sets service name used for tracing namespace, metrics dimensions and structured logging | "service_undefined" | all POWERTOOLS_TRACE_DISABLED | Disables tracing | "false" | tracing +POWERTOOLS_TRACE_MIDDLEWARES | Creates sub-segment for each middleware created by lambda_handler_decorator | "false" | middleware_factory POWERTOOLS_LOGGER_LOG_EVENT | Logs incoming event | "false" | logging POWERTOOLS_LOGGER_SAMPLE_RATE | Debug log sampling | 0 | logging POWERTOOLS_METRICS_NAMESPACE | Metrics namespace | None | metrics @@ -85,6 +93,23 @@ def handler(event, context) ... ``` +**Fetching a pre-configured tracer anywhere** + +```python +# handler.py +from aws_lambda_powertools.tracing import Tracer +tracer = Tracer(service="payment") + +@tracer.capture_lambda_handler +def handler(event, context) + charge_id = event.get('charge_id') + payment = collect_payment(charge_id) + ... + +# another_file.py +from aws_lambda_powertools.tracing import Tracer +tracer = Tracer(auto_patch=False) # new instance using existing configuration with auto patching overriden +``` ### Logging @@ -154,7 +179,7 @@ def handler(event, context) } ``` -#### Custom Metrics async +### Custom Metrics async > **NOTE** `log_metric` will be removed once it's GA. @@ -204,6 +229,97 @@ with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1) as metric: metric.add_dimension(name="function_context", value="$LATEST") ``` + +### Utilities + +#### Bring your own middleware + +This feature allows you to create your own middleware as a decorator with ease by following a simple signature. + +* Accept 3 mandatory args - `handler, event, context` +* Always return the handler with event/context or response if executed + - Supports nested middleware/decorators use case + +**Middleware with no params** + +```python +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + +@lambda_handler_decorator +def middleware_name(handler, event, context): + return handler(event, context) + +@lambda_handler_decorator +def middleware_before_after(handler, event, context): + logic_before_handler_execution() + response = handler(event, context) + logic_after_handler_execution() + return response + + +# middleware_name will wrap Lambda handler +# and simply return the handler as we're not pre/post-processing anything +# then middleware_before_after will wrap middleware_name +# run some code before/after calling the handler returned by middleware_name +# This way, lambda_handler is only actually called once (top-down) +@middleware_before_after # This will run last +@middleware_name # This will run first +def lambda_handler(event, context): + return True +``` + +**Middleware with params** + +```python +@lambda_handler_decorator +def obfuscate_sensitive_data(handler, event, context, fields=None): + # Obfuscate email before calling Lambda handler + if fields: + for field in fields: + field = event.get(field, "") + event[field] = obfuscate_pii(field) + + return handler(event, context) + +@obfuscate_sensitive_data(fields=["email"]) +def lambda_handler(event, context): + return True +``` + +**Optionally trace middleware execution** + +This makes use of an existing Tracer instance that you may have initialized anywhere in your code, otherwise it'll initialize one using default options and provider (X-Ray). + +```python +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + +@lambda_handler_decorator(trace_execution=True) +def middleware_name(handler, event, context): + return handler(event, context) + +@middleware_name +def lambda_handler(event, context): + return True +``` + +Optionally, you can enrich the final trace with additional annotations and metadata by retrieving a copy of the Tracer used. + +```python +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.tracing import Tracer + +@lambda_handler_decorator(trace_execution=True) +def middleware_name(handler, event, context): + tracer = Tracer() # Takes a copy of an existing tracer instance + tracer.add_anotation... + tracer.metadata... + return handler(event, context) + +@middleware_name +def lambda_handler(event, context): + return True +``` + ## Beta > **[Progress towards GA](https://github.com/awslabs/aws-lambda-powertools/projects/1)** diff --git a/python/aws_lambda_powertools/logging/logger.py b/python/aws_lambda_powertools/logging/logger.py index de344f1f569..32dc1be6c1a 100644 --- a/python/aws_lambda_powertools/logging/logger.py +++ b/python/aws_lambda_powertools/logging/logger.py @@ -93,32 +93,34 @@ def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = No Environment variables --------------------- POWERTOOLS_LOGGER_LOG_EVENT : str - instruct logger to log Lambda Event (e.g. "true", "True", "TRUE") + instruct logger to log Lambda Event (e.g. `"true", "True", "TRUE"`) Example ------- - Captures Lambda contextual runtime info (e.g memory, arn, req_id) - >>> from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context - >>> import logging - >>> - >>> logger = logging.getLogger(__name__) - >>> logging.setLevel(logging.INFO) - >>> logger_setup() - >>> - >>> @logger_inject_lambda_context - >>> def handler(event, context): + **Captures Lambda contextual runtime info (e.g memory, arn, req_id)** + + from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context + import logging + + logger = logging.getLogger(__name__) + logging.setLevel(logging.INFO) + logger_setup() + + @logger_inject_lambda_context + def handler(event, context): logger.info("Hello") - Captures Lambda contextual runtime info and logs incoming request - >>> from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context - >>> import logging - >>> - >>> logger = logging.getLogger(__name__) - >>> logging.setLevel(logging.INFO) - >>> logger_setup() - >>> - >>> @logger_inject_lambda_context(log_event=True) - >>> def handler(event, context): + **Captures Lambda contextual runtime info and logs incoming request** + + from aws_lambda_powertools.logging import logger_setup, logger_inject_lambda_context + import logging + + logger = logging.getLogger(__name__) + logging.setLevel(logging.INFO) + logger_setup() + + @logger_inject_lambda_context(log_event=True) + def handler(event, context): logger.info("Hello") Returns @@ -128,9 +130,7 @@ def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = No """ # If handler is None we've been called with parameters - # We then return a partial function with args filled - # Next time we're called we'll call our Lambda - # This allows us to avoid writing wrapper_wrapper type of fn + # Return a partial function with args filled if lambda_handler is None: logger.debug("Decorator called with parameters") return functools.partial(logger_inject_lambda_context, log_event=log_event) @@ -178,6 +178,8 @@ def log_metric( ): """Logs a custom metric in a statsD-esque format to stdout. + **This will be removed when GA - Use `aws_lambda_powertools.metrics.metrics.Metrics` instead** + Creating Custom Metrics synchronously impact on performance/execution time. Instead, log_metric prints a metric to CloudWatch Logs. That allows us to pick them up asynchronously via another Lambda function and create them as a metric. @@ -185,7 +187,7 @@ def log_metric( NOTE: It takes up to 9 dimensions by default, and Metric units are conveniently available via MetricUnit Enum. If service is not passed as arg or via env var, "service_undefined" will be used as dimension instead. - Output in CloudWatch Logs: MONITORING||||| + **Output in CloudWatch Logs**: `MONITORING|||||` Serverless Application Repository App that creates custom metric from this log output: https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:374852340823:applications~async-custom-metrics @@ -195,23 +197,39 @@ def log_metric( POWERTOOLS_SERVICE_NAME: str service name + Parameters + ---------- + name : str + metric name, by default None + namespace : str + metric namespace (e.g. application name), by default None + unit : MetricUnit, by default MetricUnit.Count + metric unit enum value (e.g. MetricUnit.Seconds), by default None\n + API Info: https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html + value : float, optional + metric value, by default 0 + service : str, optional + service name used as dimension, by default "service_undefined" + dimensions: dict, optional + keyword arguments as additional dimensions (e.g. `customer=customerId`) + Example ------- - Log metric to count number of successful payments; define service via env var + **Log metric to count number of successful payments; define service via env var** $ export POWERTOOLS_SERVICE_NAME="payment" - >>> from aws_lambda_powertools.logging import MetricUnit, log_metric - >>> log_metric( + from aws_lambda_powertools.logging import MetricUnit, log_metric + log_metric( name="SuccessfulPayments", unit=MetricUnit.Count, value=1, namespace="DemoApp" ) - Log metric to count number of successful payments per campaign & customer + **Log metric to count number of successful payments per campaign & customer** - >>> from aws_lambda_powertools.logging import MetricUnit, log_metric - >>> log_metric( + from aws_lambda_powertools.logging import MetricUnit, log_metric + log_metric( name="SuccessfulPayments", service="payment", unit=MetricUnit.Count, @@ -220,22 +238,6 @@ def log_metric( campaign=campaign_id, customer=customer_id ) - - Parameters - ---------- - name : str - metric name, by default None - namespace : str - metric namespace (e.g. application name), by default None - unit : MetricUnit, by default MetricUnit.Count - metric unit enum value (e.g. MetricUnit.Seconds), by default None - API Info: https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html - value : float, optional - metric value, by default 0 - service : str, optional - service name used as dimension, by default "service_undefined" - dimensions: dict, optional - keyword arguments as additional dimensions (e.g. customer=customerId) """ warnings.warn(message="This method will be removed in GA; use Metrics instead", category=DeprecationWarning) diff --git a/python/aws_lambda_powertools/metrics/base.py b/python/aws_lambda_powertools/metrics/base.py index 3c45bc619f8..448bfc37e02 100644 --- a/python/aws_lambda_powertools/metrics/base.py +++ b/python/aws_lambda_powertools/metrics/base.py @@ -177,6 +177,7 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None) -> } metrics_timestamp = {"Timestamp": int(datetime.datetime.now().timestamp() * 1000)} metric_set["_aws"] = {**metrics_timestamp, **metrics_definition} + metric_set.update(**dimensions) try: logger.debug("Validating serialized metrics against CloudWatch EMF schema", metric_set) diff --git a/python/aws_lambda_powertools/middleware_factory/__init__.py b/python/aws_lambda_powertools/middleware_factory/__init__.py new file mode 100644 index 00000000000..9d57d843ec2 --- /dev/null +++ b/python/aws_lambda_powertools/middleware_factory/__init__.py @@ -0,0 +1,4 @@ +""" Utilities to enhance middlewares """ +from .factory import lambda_handler_decorator + +__all__ = ["lambda_handler_decorator"] diff --git a/python/aws_lambda_powertools/middleware_factory/factory.py b/python/aws_lambda_powertools/middleware_factory/factory.py new file mode 100644 index 00000000000..4dcab2adf33 --- /dev/null +++ b/python/aws_lambda_powertools/middleware_factory/factory.py @@ -0,0 +1,138 @@ +import functools +import inspect +import logging +import os +from distutils.util import strtobool +from typing import Callable + +from ..tracing import Tracer + +logger = logging.getLogger(__name__) +logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) + + +def lambda_handler_decorator(decorator: Callable = None, trace_execution=False): + """Decorator factory for decorating Lambda handlers. + + You can use lambda_handler_decorator to create your own middlewares, + where your function signature follows: `fn(handler, event, context)` + + Custom keyword arguments are also supported e.g. `fn(handler, event, context, option=value)` + + Middlewares created by this factory supports tracing to help you quickly troubleshoot + any overhead that custom middlewares may cause - They will appear as custom subsegments. + + **Non-key value params are not supported** e.g. `fn(handler, event, context, option)` + + Environment variables + --------------------- + POWERTOOLS_TRACE_MIDDLEWARES : str + uses `aws_lambda_powertools.tracing.Tracer` + to create sub-segments per middleware (e.g. `"true", "True", "TRUE"`) + + Parameters + ---------- + decorator: Callable + Middleware to be wrapped by this factory + trace_execution: bool + Flag to explicitly enable trace execution for middlewares.\n + `Env POWERTOOLS_TRACE_MIDDLEWARES="true"` + + Example + ------- + **Create a middleware no params** + + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + + @lambda_handler_decorator + def log_response(handler, event, context): + any_code_to_execute_before_lambda_handler() + response = handler(event, context) + any_code_to_execute_after_lambda_handler() + print(f"Lambda handler response: {response}") + + @log_response + def lambda_handler(event, context): + return True + + **Create a middleware with params** + + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + + @lambda_handler_decorator + def obfuscate_sensitive_data(handler, event, context, fields=None): + # Obfuscate email before calling Lambda handler + if fields: + for field in fields: + field = event.get(field, "") + event[field] = obfuscate_pii(field) + + response = handler(event, context) + print(f"Lambda handler response: {response}") + + @obfuscate_sensitive_data(fields=["email"]) + def lambda_handler(event, context): + return True + + **Trace execution of custom middleware** + + from aws_lambda_powertools.tracing import Tracer + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + + tracer = Tracer(service="payment") # or via env var + ... + @lambda_handler_decorator(trace_execution=True) + def log_response(handler, event, context): + ... + + @tracer.capture_lambda_handler + @log_response + def lambda_handler(event, context): + return True + + Limitations + ----------- + * Async middlewares not supported + * Classes, class methods middlewares not supported + + Raises + ------ + TypeError + When middleware receives non keyword=arguments + """ + + 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))) + + @functools.wraps(decorator) + def final_decorator(func: Callable = None, **kwargs): + # If called with kwargs return new func with kwargs + if func is None: + return functools.partial(final_decorator, **kwargs) + + if not inspect.isfunction(func): + raise TypeError( + f"Only keyword arguments is supported for middlewares: {decorator.__qualname__} received {func}" + ) + + @functools.wraps(func) + def wrapper(event, context): + try: + middleware = functools.partial(decorator, func, event, context, **kwargs) + if trace_execution: + tracer = Tracer(auto_patch=False) + tracer.create_subsegment(name=f"## {decorator.__qualname__}") + response = middleware() + tracer.end_subsegment() + else: + response = middleware() + return response + except Exception as err: + logger.error(f"Caught exception in {decorator.__qualname__}") + raise err + + return wrapper + + return final_decorator diff --git a/python/aws_lambda_powertools/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 20a5a4b096c..0f3e3cff8bb 100644 --- a/python/aws_lambda_powertools/tracing/tracer.py +++ b/python/aws_lambda_powertools/tracing/tracer.py @@ -1,3 +1,4 @@ +import copy import functools import logging import os @@ -14,48 +15,60 @@ class Tracer: """Tracer using AWS-XRay to provide decorators with known defaults for Lambda functions - When running locally, it honours POWERTOOLS_TRACE_DISABLED environment variable - so end user code doesn't have to be modified to run it locally - instead Tracer returns dummy segments/subsegments. + When running locally, it detects whether it's running via SAM CLI, + and if it is it returns dummy segments/subsegments instead. - Tracing is automatically disabled when running locally via via SAM CLI. - - It patches all available libraries supported by X-Ray SDK + By default, it patches all available libraries supported by X-Ray SDK. Patching is + automatically disabled when running locally via SAM CLI or by any other means. \n Ref: https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/thirdparty.html + Tracer keeps a copy of its configuration as it can be instantiated more than once. This + is useful when you are using your own middlewares and want to utilize an existing Tracer. + Make sure to set `auto_patch=False` in subsequent Tracer instances to avoid double patching. + Environment variables --------------------- POWERTOOLS_TRACE_DISABLED : str - disable tracer (e.g. "true", "True", "TRUE") + disable tracer (e.g. `"true", "True", "TRUE"`) POWERTOOLS_SERVICE_NAME : str service name + Parameters + ---------- + service: str + Service name that will be appended in all tracing metadata + auto_patch: bool + Patch existing imported modules during initialization, by default True + disabled: bool + Flag to explicitly disable tracing, useful when running/testing locally. + `Env POWERTOOLS_TRACE_DISABLED="true"` + Example ------- - A Lambda function using Tracer + **A Lambda function using Tracer** - >>> from aws_lambda_powertools.tracing import Tracer - >>> tracer = Tracer(service="greeting") + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer(service="greeting") - >>> @tracer.capture_method - >>> def greeting(name: str) -> Dict: - return { - "name": name - } + @tracer.capture_method + def greeting(name: str) -> Dict: + return { + "name": name + } - >>> @tracer.capture_lambda_handler - >>> def handler(event: dict, context: Any) -> Dict: - >>> print("Received event from Lambda...") - >>> response = greeting(name="Heitor") - >>> return response + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + print("Received event from Lambda...") + response = greeting(name="Heitor") + return response - Booking Lambda function using Tracer that adds additional annotation/metadata + **Booking Lambda function using Tracer that adds additional annotation/metadata** - >>> from aws_lambda_powertools.tracing import Tracer - >>> tracer = Tracer(service="booking") + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer(service="booking") - >>> @tracer.capture_method - >>> def confirm_booking(booking_id: str) -> Dict: + @tracer.capture_method + def confirm_booking(booking_id: str) -> Dict: resp = add_confirmation(booking_id) tracer.put_annotation("BookingConfirmation", resp['requestId']) @@ -63,49 +76,67 @@ class Tracer: return resp - >>> @tracer.capture_lambda_handler - >>> def handler(event: dict, context: Any) -> Dict: - >>> print("Received event from Lambda...") - >>> response = greeting(name="Heitor") - >>> return response + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + print("Received event from Lambda...") + response = greeting(name="Heitor") + return response - A Lambda function using service name via POWERTOOLS_SERVICE_NAME + **A Lambda function using service name via POWERTOOLS_SERVICE_NAME** - >>> export POWERTOOLS_SERVICE_NAME="booking" - >>> from aws_lambda_powertools.tracing import Tracer - >>> tracer = Tracer() + export POWERTOOLS_SERVICE_NAME="booking" + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer() - >>> @tracer.capture_lambda_handler - >>> def handler(event: dict, context: Any) -> Dict: - >>> print("Received event from Lambda...") - >>> response = greeting(name="Lessa") - >>> return response + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + print("Received event from Lambda...") + response = greeting(name="Lessa") + return response - Parameters - ---------- - service: str - Service name that will be appended in all tracing metadata - disabled: bool - Flag to explicitly disable tracing, useful when running locally. - Env: POWERTOOLS_TRACE_DISABLED="true" + **Reuse an existing instance of Tracer anywhere in the code** + + # lambda_handler.py + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer() + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + ... + + # utils.py + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer() + ... Returns ------- Tracer Tracer instance with imported modules patched + + Limitations + ----------- + * Async handler and methods not supported + """ + _default_config = {"service": "service_undefined", "disabled": False, "provider": xray_recorder, "auto_patch": True} + _config = copy.copy(_default_config) + def __init__( - self, service: str = "service_undefined", disabled: bool = False, provider: xray_recorder = xray_recorder, + self, service: str = None, disabled: bool = None, provider: xray_recorder = None, auto_patch: bool = None ): - self.provider = provider - self.disabled = self.__is_trace_disabled() or disabled - self.service = os.getenv("POWERTOOLS_SERVICE_NAME") or service + self.__build_config(service=service, disabled=disabled, provider=provider, auto_patch=auto_patch) + self.provider = self._config["provider"] + self.disabled = self._config["disabled"] + self.service = self._config["service"] + self.auto_patch = self._config["auto_patch"] if self.disabled: self.__disable_tracing_provider() - self.__patch() + if self.auto_patch: + self.patch() def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None): """Decorator to create subsegment for lambda handlers @@ -115,11 +146,11 @@ def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = No Example ------- - Lambda function using capture_lambda_handler decorator + **Lambda function using capture_lambda_handler decorator** - >>> tracer = Tracer(service="payment") - >>> @tracer.capture_lambda_handler - def handler(event, context) + tracer = Tracer(service="payment") + @tracer.capture_lambda_handler + def handler(event, context) Parameters ---------- @@ -134,7 +165,7 @@ def handler(event, context) @functools.wraps(lambda_handler) def decorate(event, context): - self.__create_subsegment(name=f"## {lambda_handler.__name__}") + self.create_subsegment(name=f"## {lambda_handler.__name__}") try: logger.debug("Calling lambda handler") @@ -148,7 +179,7 @@ def decorate(event, context): self.put_metadata(f"{self.service}_error", err) raise err finally: - self.__end_subsegment() + self.end_subsegment() return response @@ -162,12 +193,11 @@ def capture_method(self, method: Callable = None): Example ------- - Custom function using capture_method decorator - - >>> tracer = Tracer(service="payment") + **Custom function using capture_method decorator** - >>> @tracer.capture_method - def some_function() + tracer = Tracer(service="payment") + @tracer.capture_method + def some_function() Parameters ---------- @@ -183,7 +213,7 @@ def some_function() @functools.wraps(method) def decorate(*args, **kwargs): method_name = f"{method.__name__}" - self.__create_subsegment(name=f"## {method_name}") + self.create_subsegment(name=f"## {method_name}") try: logger.debug(f"Calling method: {method_name}") @@ -197,7 +227,7 @@ def decorate(*args, **kwargs): self.put_metadata(f"{method_name} error", err) raise err finally: - self.__end_subsegment() + self.end_subsegment() return response @@ -210,8 +240,8 @@ def put_annotation(self, key: str, value: Any): ------- Custom annotation for a pseudo service named payment - >>> tracer = Tracer(service="payment") - >>> tracer.put_annotation("PaymentStatus", "CONFIRMED") + tracer = Tracer(service="payment") + tracer.put_annotation("PaymentStatus", "CONFIRMED") Parameters ---------- @@ -244,9 +274,9 @@ def put_metadata(self, key: str, value: object, namespace: str = None): ------- Custom metadata for a pseudo service named payment - >>> tracer = Tracer(service="payment") - >>> response = collect_payment() - >>> tracer.put_metadata("Payment collection", response) + tracer = Tracer(service="payment") + response = collect_payment() + tracer.put_metadata("Payment collection", response) """ # Will no longer be needed once #155 is resolved # https://github.com/aws/aws-xray-sdk-python/issues/155 @@ -257,7 +287,7 @@ def put_metadata(self, key: str, value: object, namespace: str = None): logger.debug(f"Adding metadata on key '{key}'' with '{value}'' at namespace '{namespace}''") self.provider.put_metadata(key=key, value=value, namespace=_namespace) - def __create_subsegment(self, name: str) -> models.subsegment: + def create_subsegment(self, name: str) -> models.subsegment: """Creates subsegment or a dummy segment plus subsegment if tracing is disabled It also assumes Tracer would be instantiated statically so that cold starts are captured. @@ -271,7 +301,7 @@ def __create_subsegment(self, name: str) -> models.subsegment: ------- Creates a genuine subsegment - >>> self.__create_subsegment(name="a meaningful name") + self.create_subsegment(name="a meaningful name") Returns ------- @@ -296,7 +326,7 @@ def __create_subsegment(self, name: str) -> models.subsegment: return subsegment - def __end_subsegment(self): + def end_subsegment(self): """Ends an existing subsegment Parameters @@ -310,19 +340,18 @@ def __end_subsegment(self): self.provider.end_subsegment() - def __patch(self): - """Patch modules for instrumentation - """ + def patch(self): + """Patch modules for instrumentation""" logger.debug("Patching modules...") - is_lambda_emulator = os.getenv("AWS_SAM_LOCAL") - is_lambda_env = os.getenv("LAMBDA_TASK_ROOT") + is_lambda_emulator = os.getenv("AWS_SAM_LOCAL", False) + is_lambda_env = os.getenv("LAMBDA_TASK_ROOT", False) if self.disabled: logger.debug("Tracing has been disabled, aborting patch") return - if is_lambda_emulator or not is_lambda_env: + if is_lambda_emulator or is_lambda_env: logger.debug("Running under SAM CLI env or not in Lambda; aborting patch") return @@ -339,9 +368,9 @@ def __is_trace_disabled(self) -> bool: Tracing is automatically disabled in the following conditions: - 1. Explicitly disabled via TRACE_DISABLED environment variable + 1. Explicitly disabled via `TRACE_DISABLED` environment variable 2. Running in Lambda Emulators where X-Ray Daemon will not be listening - 3. Explicitly disabled via constructor e.g Tracer(disabled=True) + 3. Explicitly disabled via constructor e.g `Tracer(disabled=True)` Returns ------- @@ -361,3 +390,19 @@ def __is_trace_disabled(self) -> bool: return is_lambda_emulator return False + + def __build_config( + self, service: str = None, disabled: bool = None, provider: xray_recorder = None, auto_patch: bool = None + ): + """ Populates Tracer config for new and existing initializations """ + is_disabled = disabled if disabled is not None else self.__is_trace_disabled() + is_service = service if service is not None else os.getenv("POWERTOOLS_SERVICE_NAME") + + 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"] + self._config["service"] = is_service if is_service else self._config["service"] + self._config["disabled"] = is_disabled if is_disabled else self._config["disabled"] + + @classmethod + def _reset_config(cls): + cls._config = copy.copy(cls._default_config) diff --git a/python/example/hello_world/app.py b/python/example/hello_world/app.py index 033e55dcb03..8836b542476 100644 --- a/python/example/hello_world/app.py +++ b/python/example/hello_world/app.py @@ -1,9 +1,11 @@ import json +import requests + from aws_lambda_powertools.logging import logger_inject_lambda_context, logger_setup -from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.metrics import Metrics, MetricUnit, single_metric -import requests +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.tracing import Tracer tracer = Tracer() logger = logger_setup() @@ -13,8 +15,22 @@ metrics.add_dimension(name="operation", value="example") + +@lambda_handler_decorator(trace_execution=True) +def my_middleware(handler, event, context, say_hello=False): + if say_hello: + print("========= HELLO PARAM DETECTED =========") + print("========= Logging event before Handler is called =========") + print(event) + ret = handler(event, context) + print("========= Logging response after Handler is called =========") + print(ret) + return ret + + @metrics.log_metrics @tracer.capture_lambda_handler +@my_middleware(say_hello=True) @logger_inject_lambda_context def lambda_handler(event, context): """Sample pure Lambda function @@ -41,7 +57,7 @@ def lambda_handler(event, context): if _cold_start: logger.debug("Recording cold start metric") metrics.add_metric(name="ColdStart", unit=MetricUnit.Count, value=1) - metrics.add_dimension(name="function_name", value=context.function_name) + metrics.add_dimension(name="function_name", value=context.function_name) _cold_start = False try: @@ -49,17 +65,14 @@ def lambda_handler(event, context): metrics.add_metric(name="SuccessfulLocations", unit="Count", value=1) except requests.RequestException as e: # Send some context about this error to Lambda Logs - logger.error(e) - raise e - + logger.exception(e, exc_info=True) + raise + with single_metric(name="UniqueMetricDimension", unit="Seconds", value=1) as metric: metric.add_dimension(name="unique_dimension", value="for_unique_metric") logger.info("Returning message to the caller") return { "statusCode": 200, - "body": json.dumps({ - "message": "hello world", - "location": ip.text.replace("\n", "") - }), + "body": json.dumps({"message": "hello world", "location": ip.text.replace("\n", "")}), } diff --git a/python/poetry.lock b/python/poetry.lock index 118fd5b1823..2ca7efeb460 100644 --- a/python/poetry.lock +++ b/python/poetry.lock @@ -35,7 +35,7 @@ description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers name = "aws-xray-sdk" optional = false python-versions = "*" -version = "2.4.3" +version = "2.5.0" [package.dependencies] botocore = ">=1.11.3" @@ -69,7 +69,7 @@ description = "Low-level, data-driven core of boto 3." name = "botocore" optional = false python-versions = "*" -version = "1.15.37" +version = "1.15.41" [package.dependencies] docutils = ">=0.10,<0.16" @@ -111,7 +111,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.0.4" +version = "5.1" [package.dependencies] [package.dependencies.toml] @@ -322,9 +322,8 @@ version = "1.4.14" license = ["editdistance"] [[package]] -category = "dev" +category = "main" description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" @@ -385,8 +384,16 @@ category = "main" description = "Python library for serializing any arbitrary object graph into JSON" name = "jsonpickle" optional = false -python-versions = "*" -version = "1.3" +python-versions = ">=2.7" +version = "1.4" + +[package.dependencies] +importlib-metadata = "*" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["coverage (<5)", "pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-black-multipy", "pytest-cov", "ecdsa", "feedparser", "numpy", "pandas", "pymongo", "sqlalchemy", "enum34", "jsonlib"] +"testing.libs" = ["demjson", "simplejson", "ujson", "yajl"] [[package]] category = "dev" @@ -467,7 +474,7 @@ description = "Utility library for gitignore style pattern matching of file path name = "pathspec" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.7.0" +version = "0.8.0" [[package]] category = "dev" @@ -649,7 +656,7 @@ description = "A collection of helpers and mock objects for unit tests and doc t name = "testfixtures" optional = false python-versions = "*" -version = "6.14.0" +version = "6.14.1" [package.extras] build = ["setuptools-git", "wheel", "twine"] @@ -679,11 +686,11 @@ marker = "python_version != \"3.4\"" name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.8" +version = "1.25.9" [package.extras] brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] @@ -692,7 +699,7 @@ description = "Virtual Python Environment builder" name = "virtualenv" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "20.0.16" +version = "20.0.18" [package.dependencies] appdirs = ">=1.4.3,<2" @@ -710,7 +717,7 @@ version = ">=1.0,<2" [package.extras] docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"] -testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.13,<1)"] +testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "pytest-timeout (>=1.3.4,<2)", "packaging (>=20.0)", "xonsh (>=0.9.16,<1)"] [[package]] category = "dev" @@ -729,7 +736,7 @@ python-versions = "*" version = "1.12.1" [[package]] -category = "dev" +category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" @@ -759,16 +766,16 @@ attrs = [ {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, ] aws-xray-sdk = [ - {file = "aws-xray-sdk-2.4.3.tar.gz", hash = "sha256:263a38f3920d9dc625e3acb92e6f6d300f4250b70f538bd009ce6e485676ab74"}, - {file = "aws_xray_sdk-2.4.3-py2.py3-none-any.whl", hash = "sha256:612dba6efc3704ef224ac0747b05488b8aad94e71be3ece4edbc051189d50482"}, + {file = "aws-xray-sdk-2.5.0.tar.gz", hash = "sha256:8dfa785305fc8dc720d8d4c2ec6a58e85e467ddc3a53b1506a2ed8b5801c8fc7"}, + {file = "aws_xray_sdk-2.5.0-py2.py3-none-any.whl", hash = "sha256:ae57baeb175993bdbf31f83843e2c0958dd5aa8cb691ab5628aafb6ccc78a0fc"}, ] black = [ {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] botocore = [ - {file = "botocore-1.15.37-py2.py3-none-any.whl", hash = "sha256:30055e9a3e313400d92ca4ad599e6506d71fb1addc75f075ab7179973ac52de6"}, - {file = "botocore-1.15.37.tar.gz", hash = "sha256:51422695a5a39ca9320acd3edaf7b337bed75bbc7d260deb76c1d801adc0daa2"}, + {file = "botocore-1.15.41-py2.py3-none-any.whl", hash = "sha256:b12a5b642aa210a72d84204da18618276eeae052fbff58958f57d28ef3193034"}, + {file = "botocore-1.15.41.tar.gz", hash = "sha256:a45a65ba036bc980decfc3ce6c2688a2d5fffd76e4b02ea4d59e63ff0f6896d4"}, ] cfgv = [ {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, @@ -783,37 +790,37 @@ colorama = [ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] coverage = [ - {file = "coverage-5.0.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307"}, - {file = "coverage-5.0.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8"}, - {file = "coverage-5.0.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31"}, - {file = "coverage-5.0.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441"}, - {file = "coverage-5.0.4-cp27-cp27m-win32.whl", hash = "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac"}, - {file = "coverage-5.0.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435"}, - {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037"}, - {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a"}, - {file = "coverage-5.0.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5"}, - {file = "coverage-5.0.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30"}, - {file = "coverage-5.0.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7"}, - {file = "coverage-5.0.4-cp35-cp35m-win32.whl", hash = "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de"}, - {file = "coverage-5.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1"}, - {file = "coverage-5.0.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"}, - {file = "coverage-5.0.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0"}, - {file = "coverage-5.0.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd"}, - {file = "coverage-5.0.4-cp36-cp36m-win32.whl", hash = "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0"}, - {file = "coverage-5.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b"}, - {file = "coverage-5.0.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78"}, - {file = "coverage-5.0.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6"}, - {file = "coverage-5.0.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014"}, - {file = "coverage-5.0.4-cp37-cp37m-win32.whl", hash = "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732"}, - {file = "coverage-5.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006"}, - {file = "coverage-5.0.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2"}, - {file = "coverage-5.0.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe"}, - {file = "coverage-5.0.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9"}, - {file = "coverage-5.0.4-cp38-cp38-win32.whl", hash = "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1"}, - {file = "coverage-5.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0"}, - {file = "coverage-5.0.4-cp39-cp39-win32.whl", hash = "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7"}, - {file = "coverage-5.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892"}, - {file = "coverage-5.0.4.tar.gz", hash = "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, + {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, + {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, + {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, + {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, + {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, + {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, + {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, + {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, + {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, + {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, + {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, + {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, + {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, + {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, + {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, + {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, + {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, ] distlib = [ {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, @@ -899,8 +906,8 @@ jmespath = [ {file = "jmespath-0.9.5.tar.gz", hash = "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"}, ] jsonpickle = [ - {file = "jsonpickle-1.3-py2.py3-none-any.whl", hash = "sha256:efc6839cb341985f0c24f98650a4c1063a2877c236ffd3d7e1662f0c482bac93"}, - {file = "jsonpickle-1.3.tar.gz", hash = "sha256:71bca2b80ae28af4e3f86629ef247100af7f97032b5ca8d791c1f8725b411d95"}, + {file = "jsonpickle-1.4-py2.py3-none-any.whl", hash = "sha256:3d71018794242f6b1640f779a94a192500f73ceed9ef579b4f01799171ec3fb3"}, + {file = "jsonpickle-1.4.tar.gz", hash = "sha256:e8ca6ec3f379f5eee6e11380d48db220aacc282b480dea46b11cc6f6009d1cdb"}, ] mako = [ {file = "Mako-1.1.2-py2.py3-none-any.whl", hash = "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"}, @@ -961,8 +968,8 @@ packaging = [ {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, ] pathspec = [ - {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, - {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] pdoc3 = [ {file = "pdoc3-0.7.5.tar.gz", hash = "sha256:ebca75b7fcf23f3b4320abe23339834d3f08c28517718e9d29e555fc38eeb33c"}, @@ -1048,8 +1055,8 @@ six = [ {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, ] testfixtures = [ - {file = "testfixtures-6.14.0-py2.py3-none-any.whl", hash = "sha256:799144b3cbef7b072452d9c36cbd024fef415ab42924b96aad49dfd9c763de66"}, - {file = "testfixtures-6.14.0.tar.gz", hash = "sha256:cdfc3d73cb6d3d4dc3c67af84d912e86bf117d30ae25f02fe823382ef99383d2"}, + {file = "testfixtures-6.14.1-py2.py3-none-any.whl", hash = "sha256:30566e24a1b34e4d3f8c13abf62557d01eeb4480bcb8f1745467bfb0d415a7d9"}, + {file = "testfixtures-6.14.1.tar.gz", hash = "sha256:58d2b3146d93bc5ddb0cd24e0ccacb13e29bdb61e5c81235c58f7b8ee4470366"}, ] toml = [ {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, @@ -1080,12 +1087,12 @@ typed-ast = [ {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] urllib3 = [ - {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, - {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, ] virtualenv = [ - {file = "virtualenv-20.0.16-py2.py3-none-any.whl", hash = "sha256:94f647e12d1e6ced2541b93215e51752aecbd1bbb18eb1816e2867f7532b1fe1"}, - {file = "virtualenv-20.0.16.tar.gz", hash = "sha256:6ea131d41c477f6c4b7863948a9a54f7fa196854dbef73efbdff32b509f4d8bf"}, + {file = "virtualenv-20.0.18-py2.py3-none-any.whl", hash = "sha256:5021396e8f03d0d002a770da90e31e61159684db2859d0ba4850fbea752aa675"}, + {file = "virtualenv-20.0.18.tar.gz", hash = "sha256:ac53ade75ca189bc97b6c1d9ec0f1a50efe33cbf178ae09452dcd9fd309013c1"}, ] wcwidth = [ {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, diff --git a/python/pyproject.toml b/python/pyproject.toml index 851feac4cbb..3360e7fd7ed 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "0.6.3" +version = "0.7.0" description = "Python utilities for AWS Lambda functions including but not limited to tracing, logging and custom metric" authors = ["Amazon Web Services"] classifiers=[ diff --git a/python/tests/functional/test_logger.py b/python/tests/functional/test_logger.py index cd2327231a9..db8a09e8d57 100644 --- a/python/tests/functional/test_logger.py +++ b/python/tests/functional/test_logger.py @@ -150,7 +150,7 @@ def test_inject_lambda_context_log_event_request_env_var(monkeypatch, root_logge logger = logger_setup() - @logger_inject_lambda_context() + @logger_inject_lambda_context def handler(event, context): logger.info("Hello") @@ -177,7 +177,7 @@ def test_inject_lambda_context_log_no_request_by_default(monkeypatch, root_logge logger = logger_setup() - @logger_inject_lambda_context() + @logger_inject_lambda_context def handler(event, context): logger.info("Hello") diff --git a/python/tests/functional/test_metrics.py b/python/tests/functional/test_metrics.py index 95b0422c9f2..0feaf3303ff 100644 --- a/python/tests/functional/test_metrics.py +++ b/python/tests/functional/test_metrics.py @@ -155,6 +155,8 @@ def lambda_handler(evt, handler): remove_timestamp(metrics=[output, expected]) # Timestamp will always be different assert expected["_aws"] == output["_aws"] + for dimension in dimensions: + assert dimension["name"] in output def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): @@ -208,7 +210,7 @@ def test_log_metrics_schema_error(metrics, dimensions, namespace): my_metrics = Metrics() @my_metrics.log_metrics - def lambda_handler(evt, handler): + def lambda_handler(evt, context): my_metrics.add_namespace(namespace) for metric in metrics: my_metrics.add_metric(**metric) diff --git a/python/tests/functional/test_tracing.py b/python/tests/functional/test_tracing.py index b77710090e8..8ceb479190a 100644 --- a/python/tests/functional/test_tracing.py +++ b/python/tests/functional/test_tracing.py @@ -8,70 +8,31 @@ def dummy_response(): return {"test": "succeeds"} -@pytest.fixture -def xray_stub(mocker): - class XRayStub: - def __init__( - self, - put_metadata_mock: mocker.MagicMock = None, - put_annotation_mock: mocker.MagicMock = None, - begin_subsegment_mock: mocker.MagicMock = None, - end_subsegment_mock: mocker.MagicMock = None, - ): - self.put_metadata_mock = put_metadata_mock or mocker.MagicMock() - self.put_annotation_mock = put_annotation_mock or mocker.MagicMock() - self.begin_subsegment_mock = begin_subsegment_mock or mocker.MagicMock() - self.end_subsegment_mock = end_subsegment_mock or mocker.MagicMock() - - def put_metadata(self, *args, **kwargs): - return self.put_metadata_mock(*args, **kwargs) - - def put_annotation(self, *args, **kwargs): - return self.put_annotation_mock(*args, **kwargs) - - def begin_subsegment(self, *args, **kwargs): - return self.begin_subsegment_mock(*args, **kwargs) - - def end_subsegment(self, *args, **kwargs): - return self.end_subsegment_mock(*args, **kwargs) - - return XRayStub - - -def test_tracer_lambda_handler(mocker, dummy_response, xray_stub): - put_metadata_mock = mocker.MagicMock() - begin_subsegment_mock = mocker.MagicMock() - end_subsegment_mock = mocker.MagicMock() - - xray_provider = xray_stub( - put_metadata_mock=put_metadata_mock, - begin_subsegment_mock=begin_subsegment_mock, - end_subsegment_mock=end_subsegment_mock, - ) - tracer = Tracer(provider=xray_provider, service="booking") +@pytest.fixture(scope="function", autouse=True) +def reset_tracing_config(): + Tracer._reset_config() + yield + + +def test_capture_lambda_handler(dummy_response): + # GIVEN tracer is disabled, and decorator is used + # WHEN a lambda handler is run + # THEN tracer should not raise an Exception + tracer = Tracer(disabled=True) @tracer.capture_lambda_handler def handler(event, context): return dummy_response - handler({}, mocker.MagicMock()) + handler({}, {}) - assert begin_subsegment_mock.call_count == 1 - assert begin_subsegment_mock.call_args == mocker.call(name="## handler") - assert end_subsegment_mock.call_count == 1 - assert put_metadata_mock.call_args == mocker.call( - key="lambda handler response", value=dummy_response, namespace="booking" - ) +def test_capture_method(dummy_response): + # GIVEN tracer is disabled, and method decorator is used + # WHEN a function is run + # THEN tracer should not raise an Exception -def test_tracer_method(mocker, dummy_response, xray_stub): - put_metadata_mock = mocker.MagicMock() - put_annotation_mock = mocker.MagicMock() - begin_subsegment_mock = mocker.MagicMock() - end_subsegment_mock = mocker.MagicMock() - - xray_provider = xray_stub(put_metadata_mock, put_annotation_mock, begin_subsegment_mock, end_subsegment_mock) - tracer = Tracer(provider=xray_provider, service="booking") + tracer = Tracer(disabled=True) @tracer.capture_method def greeting(name, message): @@ -79,51 +40,87 @@ def greeting(name, message): greeting(name="Foo", message="Bar") - assert begin_subsegment_mock.call_count == 1 - assert begin_subsegment_mock.call_args == mocker.call(name="## greeting") - assert end_subsegment_mock.call_count == 1 - assert put_metadata_mock.call_args == mocker.call( - key="greeting response", value=dummy_response, namespace="booking" - ) +def test_tracer_lambda_emulator(monkeypatch, dummy_response): + # GIVEN tracer is run locally + # WHEN a lambda function is run through SAM CLI + # THEN tracer should not raise an Exception + monkeypatch.setenv("AWS_SAM_LOCAL", "true") + tracer = Tracer() + + @tracer.capture_lambda_handler + def handler(event, context): + return dummy_response -def test_tracer_custom_annotation(mocker, dummy_response, xray_stub): - put_annotation_mock = mocker.MagicMock() + handler({}, {}) + monkeypatch.delenv("AWS_SAM_LOCAL") - xray_provider = xray_stub(put_annotation_mock=put_annotation_mock) - tracer = Tracer(provider=xray_provider, service="booking") - annotation_key = "BookingId" - annotation_value = "123456" +def test_tracer_metadata_disabled(dummy_response): + # GIVEN tracer is disabled, and annotations/metadata are used + # WHEN a lambda handler is run + # THEN tracer should not raise an Exception and simply ignore + tracer = Tracer(disabled=True) @tracer.capture_lambda_handler def handler(event, context): - tracer.put_annotation(annotation_key, annotation_value) + tracer.put_annotation("PaymentStatus", "SUCCESS") + tracer.put_metadata("PaymentMetadata", "Metadata") return dummy_response - handler({}, mocker.MagicMock()) + handler({}, {}) + + +def test_tracer_env_vars(monkeypatch): + # GIVEN tracer disabled, is run without parameters + # WHEN service is explicitly defined + # THEN tracer should have use that service name + service_name = "booking" + monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", service_name) + tracer_env_var = Tracer(disabled=True) - assert put_annotation_mock.call_count == 1 - assert put_annotation_mock.call_args == mocker.call(key=annotation_key, value=annotation_value) + assert tracer_env_var.service == service_name + tracer_explicit = Tracer(disabled=True, service=service_name) + assert tracer_explicit.service == service_name -def test_tracer_custom_metadata(mocker, dummy_response, xray_stub): - put_metadata_mock = mocker.MagicMock() + monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") + tracer = Tracer() - xray_provider = xray_stub(put_metadata_mock=put_metadata_mock) + assert bool(tracer.disabled) is True - tracer = Tracer(provider=xray_provider, service="booking") - annotation_key = "Booking response" - annotation_value = {"bookingStatus": "CONFIRMED"} + +def test_tracer_with_exception(mocker): + # GIVEN tracer is disabled, decorator is used + # WHEN a lambda handler or method returns an Exception + # THEN tracer should reraise the same Exception + class CustomException(Exception): + pass + + tracer = Tracer(disabled=True) @tracer.capture_lambda_handler def handler(event, context): - tracer.put_metadata(annotation_key, annotation_value) - return dummy_response + raise CustomException("test") + + @tracer.capture_method + def greeting(name, message): + raise CustomException("test") + + with pytest.raises(CustomException): + handler({}, {}) + + with pytest.raises(CustomException): + greeting(name="Foo", message="Bar") + - handler({}, mocker.MagicMock()) +def test_tracer_reuse(): + # GIVEN tracer A, B were initialized + # WHEN tracer B explicitly reuses A config + # THEN tracer B attributes should be equal to tracer A + service_name = "booking" + tracer_a = Tracer(disabled=True, service=service_name) + tracer_b = Tracer() - assert put_metadata_mock.call_count == 2 - assert put_metadata_mock.call_args_list[0] == mocker.call( - key=annotation_key, value=annotation_value, namespace="booking" - ) + assert id(tracer_a) != id(tracer_b) + assert tracer_a.__dict__.items() == tracer_b.__dict__.items() diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py new file mode 100644 index 00000000000..141acf9d96f --- /dev/null +++ b/python/tests/functional/test_utils.py @@ -0,0 +1,122 @@ +import json +from typing import Callable + +import pytest + +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator + + +@pytest.fixture +def say_hi_middleware() -> Callable: + @lambda_handler_decorator + def say_hi(handler, event, context): + print("hi before lambda handler is executed") + return handler(event, context) + + return say_hi + + +@pytest.fixture +def say_bye_middleware() -> Callable: + @lambda_handler_decorator + def say_bye(handler, event, context): + ret = handler(event, context) + print("goodbye after lambda handler is executed") + return ret + + return say_bye + + +def test_factory_single_decorator(capsys, say_hi_middleware): + @say_hi_middleware + def lambda_handler(evt, ctx): + return True + + lambda_handler({}, {}) + output = capsys.readouterr().out.strip() + assert "hi before lambda handler is executed" in output + + +def test_factory_nested_decorator(capsys, say_hi_middleware, say_bye_middleware): + @say_bye_middleware + @say_hi_middleware + def lambda_handler(evt, ctx): + return True + + lambda_handler({}, {}) + output = capsys.readouterr().out.strip() + assert "hi before lambda handler is executed" in output + assert "goodbye after lambda handler is executed" in output + + +def test_factory_exception_propagation(say_bye_middleware, say_hi_middleware): + @say_bye_middleware + @say_hi_middleware + def lambda_handler(evt, ctx): + raise ValueError("Something happened") + + with pytest.raises(ValueError): + lambda_handler({}, {}) + + +def test_factory_explicit_tracing(monkeypatch): + monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") + + @lambda_handler_decorator(trace_execution=True) + def no_op(handler, event, context): + ret = handler(event, context) + return ret + + @no_op + def lambda_handler(evt, ctx): + return True + + lambda_handler({}, {}) + + +def test_factory_explicit_tracing_env_var(monkeypatch): + monkeypatch.setenv("POWERTOOLS_TRACE_MIDDLEWARES", "true") + monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") + + @lambda_handler_decorator + def no_op(handler, event, context): + ret = handler(event, context) + return ret + + @no_op + def lambda_handler(evt, ctx): + return True + + lambda_handler({}, {}) + + +def test_factory_decorator_with_kwarg_params(capsys): + @lambda_handler_decorator + def log_event(handler, event, context, log_event=False): + if log_event: + print(json.dumps(event)) + return handler(event, context) + + @log_event(log_event=True) + def lambda_handler(evt, ctx): + return True + + event = {"message": "hello"} + lambda_handler(event, {}) + output = json.loads(capsys.readouterr().out.strip()) + + assert event == output + + +def test_factory_decorator_with_non_kwarg_params(): + @lambda_handler_decorator + def log_event(handler, event, context, log_event=False): + if log_event: + print(json.dumps(event)) + return handler(event, context) + + with pytest.raises(TypeError): + + @log_event(True) + def lambda_handler(evt, ctx): + return True diff --git a/python/tests/unit/test_tracing.py b/python/tests/unit/test_tracing.py index 91144d64f9d..a7b98389e33 100644 --- a/python/tests/unit/test_tracing.py +++ b/python/tests/unit/test_tracing.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest from aws_lambda_powertools.tracing import Tracer @@ -8,100 +10,146 @@ def dummy_response(): return {"test": "succeeds"} -def test_capture_lambda_handler(mocker, dummy_response): - # GIVEN tracer is disabled, and decorator is used - # WHEN a lambda handler is run - # THEN tracer should not raise an Exception - tracer = Tracer(disabled=True) +@pytest.fixture +def xray_stub(mocker): + class XRayStub: + def __init__( + self, + put_metadata_mock: mocker.MagicMock = None, + put_annotation_mock: mocker.MagicMock = None, + begin_subsegment_mock: mocker.MagicMock = None, + end_subsegment_mock: mocker.MagicMock = None, + ): + self.put_metadata_mock = put_metadata_mock or mocker.MagicMock() + self.put_annotation_mock = put_annotation_mock or mocker.MagicMock() + self.begin_subsegment_mock = begin_subsegment_mock or mocker.MagicMock() + self.end_subsegment_mock = end_subsegment_mock or mocker.MagicMock() - @tracer.capture_lambda_handler - def handler(event, context): - return dummy_response + def put_metadata(self, *args, **kwargs): + return self.put_metadata_mock(*args, **kwargs) - handler({}, mocker.MagicMock()) + def put_annotation(self, *args, **kwargs): + return self.put_annotation_mock(*args, **kwargs) + def begin_subsegment(self, *args, **kwargs): + return self.begin_subsegment_mock(*args, **kwargs) -def test_capture_method(mocker, dummy_response): - # GIVEN tracer is disabled, and method decorator is used - # WHEN a function is run - # THEN tracer should not raise an Exception + def end_subsegment(self, *args, **kwargs): + return self.end_subsegment_mock(*args, **kwargs) - tracer = Tracer(disabled=True) + return XRayStub - @tracer.capture_method - def greeting(name, message): - return dummy_response - greeting(name="Foo", message="Bar") +@pytest.fixture(scope="function", autouse=True) +def reset_tracing_config(): + Tracer._reset_config() + yield -def test_tracer_with_exception(mocker): - # GIVEN tracer is disabled, decorator is used - # WHEN a lambda handler or method returns an Exception - # THEN tracer should reraise the same Exception - class CustomException(Exception): - pass +def test_tracer_lambda_handler(mocker, dummy_response, xray_stub): + put_metadata_mock = mocker.MagicMock() + begin_subsegment_mock = mocker.MagicMock() + end_subsegment_mock = mocker.MagicMock() - tracer = Tracer(disabled=True) + xray_provider = xray_stub( + put_metadata_mock=put_metadata_mock, + begin_subsegment_mock=begin_subsegment_mock, + end_subsegment_mock=end_subsegment_mock, + ) + tracer = Tracer(provider=xray_provider, service="booking") @tracer.capture_lambda_handler def handler(event, context): - raise CustomException("test") + return dummy_response + + handler({}, mocker.MagicMock()) + + assert begin_subsegment_mock.call_count == 1 + assert begin_subsegment_mock.call_args == mocker.call(name="## handler") + assert end_subsegment_mock.call_count == 1 + assert put_metadata_mock.call_args == mocker.call( + key="lambda handler response", value=dummy_response, namespace="booking" + ) + + +def test_tracer_method(mocker, dummy_response, xray_stub): + put_metadata_mock = mocker.MagicMock() + put_annotation_mock = mocker.MagicMock() + begin_subsegment_mock = mocker.MagicMock() + end_subsegment_mock = mocker.MagicMock() + + xray_provider = xray_stub(put_metadata_mock, put_annotation_mock, begin_subsegment_mock, end_subsegment_mock) + tracer = Tracer(provider=xray_provider, service="booking") @tracer.capture_method def greeting(name, message): - raise CustomException("test") + return dummy_response - with pytest.raises(CustomException): - handler({}, mocker.MagicMock()) + greeting(name="Foo", message="Bar") - with pytest.raises(CustomException): - greeting(name="Foo", message="Bar") + assert begin_subsegment_mock.call_count == 1 + assert begin_subsegment_mock.call_args == mocker.call(name="## greeting") + assert end_subsegment_mock.call_count == 1 + assert put_metadata_mock.call_args == mocker.call( + key="greeting response", value=dummy_response, namespace="booking" + ) -def test_tracer_lambda_emulator(monkeypatch, mocker, dummy_response): - # GIVEN tracer is run locally - # WHEN a lambda function is run through SAM CLI - # THEN tracer should not raise an Exception - monkeypatch.setenv("AWS_SAM_LOCAL", "true") - tracer = Tracer() +def test_tracer_custom_metadata(mocker, dummy_response, xray_stub): + put_metadata_mock = mocker.MagicMock() + + xray_provider = xray_stub(put_metadata_mock=put_metadata_mock) + + tracer = Tracer(provider=xray_provider, service="booking") + annotation_key = "Booking response" + annotation_value = {"bookingStatus": "CONFIRMED"} @tracer.capture_lambda_handler def handler(event, context): + tracer.put_metadata(annotation_key, annotation_value) return dummy_response handler({}, mocker.MagicMock()) + assert put_metadata_mock.call_count == 2 + assert put_metadata_mock.call_args_list[0] == mocker.call( + key=annotation_key, value=annotation_value, namespace="booking" + ) -def test_tracer_env_vars(monkeypatch): - # GIVEN tracer disabled, is run without parameters - # WHEN service is explicitly defined - # THEN tracer should have use that service name - service_name = "booking" - monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", service_name) - tracer_env_var = Tracer(disabled=True) - - assert tracer_env_var.service == service_name - tracer_explicit = Tracer(disabled=True, service=service_name) - assert tracer_explicit.service == service_name +def test_tracer_custom_annotation(mocker, dummy_response, xray_stub): + put_annotation_mock = mocker.MagicMock() - monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") - tracer = Tracer() + xray_provider = xray_stub(put_annotation_mock=put_annotation_mock) - assert bool(tracer.disabled) is True - - -def test_tracer_metadata_disabled(mocker, dummy_response): - # GIVEN tracer is disabled, and annotations/metadata are used - # WHEN a lambda handler is run - # THEN tracer should not raise an Exception and simply ignore - tracer = Tracer(disabled=True) + tracer = Tracer(provider=xray_provider, service="booking") + annotation_key = "BookingId" + annotation_value = "123456" @tracer.capture_lambda_handler def handler(event, context): - tracer.put_annotation("PaymentStatus", "SUCCESS") - tracer.put_metadata("PaymentMetadata", "Metadata") + tracer.put_annotation(annotation_key, annotation_value) return dummy_response handler({}, mocker.MagicMock()) + + assert put_annotation_mock.call_count == 1 + assert put_annotation_mock.call_args == mocker.call(key=annotation_key, value=annotation_value) + + +@mock.patch("aws_lambda_powertools.tracing.Tracer.patch") +def test_tracer_autopatch(patch_mock): + # GIVEN tracer is instantiated + # WHEN default options were used, or patch() was called + # THEN tracer should patch all modules + Tracer(disabled=True) + assert patch_mock.call_count == 1 + + +@mock.patch("aws_lambda_powertools.tracing.Tracer.patch") +def test_tracer_no_autopatch(patch_mock): + # GIVEN tracer is instantiated + # WHEN auto_patch is disabled + # THEN tracer should not patch any module + Tracer(disabled=True, auto_patch=False) + assert patch_mock.call_count == 0