From 3555b4345bb9b3325224025276f252c6cd486fea Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 10 Apr 2020 15:59:08 +0100 Subject: [PATCH 01/36] feat(utils): add decorator factory --- .../aws_lambda_powertools/utils/__init__.py | 1 + python/aws_lambda_powertools/utils/factory.py | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 python/aws_lambda_powertools/utils/__init__.py create mode 100644 python/aws_lambda_powertools/utils/factory.py diff --git a/python/aws_lambda_powertools/utils/__init__.py b/python/aws_lambda_powertools/utils/__init__.py new file mode 100644 index 00000000000..ab899abff25 --- /dev/null +++ b/python/aws_lambda_powertools/utils/__init__.py @@ -0,0 +1 @@ +""" Utilities to enhance middlewares """ \ No newline at end of file diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py new file mode 100644 index 00000000000..fbf6d374e3e --- /dev/null +++ b/python/aws_lambda_powertools/utils/factory.py @@ -0,0 +1,67 @@ +import functools + + +def lambda_handler_decorator(decorator): + """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) + + You can also set your own key=value params: fn(handler, event, context, option=value) + + Example + ------- + **Create a middleware no params** + + from aws_lambda_powertools.utils 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.utils import lambda_handler_decorator + + @lambda_handler_decorator + def obfuscate_sensitive_data(handler, event, context, obfuscate_email=None): + # Obfuscate email before calling Lambda handler + if obfuscate_email: + email = event.get('email', "") + event['email'] = obfuscate_pii(email) + + response = handler(event, context) + print(f"Lambda handler response: {response}") + + @obfuscate_sensitive_data(obfuscate_email=True) + def lambda_handler(event, context): + return True + """ + + @functools.wraps(decorator) + def final_decorator(func=None, **kwargs): + def decorated(func): + """This is the decorator function that will be called when setting a decorator.""" + + @functools.wraps(func) + def wrapper(event, context): + """Return decorator with decorated function incl. event/context/params""" + return decorator(func, event, context, **kwargs) + + return wrapper + + # Return decorator if no params + # Otherwise return decorator with params received + if func is None: + return decorated + else: + return decorated(func) + + return final_decorator From 11ff22921883fe5d181f53c47916f183bbc5f5bc Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sun, 12 Apr 2020 19:35:58 +0100 Subject: [PATCH 02/36] improv: use partial to reduce complexity --- python/aws_lambda_powertools/utils/factory.py | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index fbf6d374e3e..124c66c2cfc 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -17,7 +17,7 @@ def lambda_handler_decorator(decorator): @lambda_handler_decorator def log_response(handler, event, context): - any_code_to_execute_before_lambda_handler() + any_code_to_execute_before_lambda_handler() response = handler(event, context) any_code_to_execute_after_lambda_handler() print(f"Lambda handler response: {response}") @@ -31,37 +31,31 @@ def lambda_handler(event, context): from aws_lambda_powertools.utils import lambda_handler_decorator @lambda_handler_decorator - def obfuscate_sensitive_data(handler, event, context, obfuscate_email=None): + def obfuscate_sensitive_data(handler, event, context, fields=None): # Obfuscate email before calling Lambda handler - if obfuscate_email: - email = event.get('email', "") - event['email'] = obfuscate_pii(email) - + 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(obfuscate_email=True) + @obfuscate_sensitive_data(fields=["email"]) def lambda_handler(event, context): return True """ @functools.wraps(decorator) def final_decorator(func=None, **kwargs): - def decorated(func): - """This is the decorator function that will be called when setting a decorator.""" - - @functools.wraps(func) - def wrapper(event, context): - """Return decorator with decorated function incl. event/context/params""" - return decorator(func, event, context, **kwargs) + # If called with args return new func with args + if func is None: + return functools.partial(final_decorator, **kwargs) - return wrapper + @functools.wraps(func) + def wrapper(event, context): + return decorator(func, event, context, **kwargs) - # Return decorator if no params - # Otherwise return decorator with params received - if func is None: - return decorated - else: - return decorated(func) + return wrapper return final_decorator From 774ee3d0276e1679ce5d35a3f98f7dff5a7f7cc7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sun, 12 Apr 2020 20:01:39 +0100 Subject: [PATCH 03/36] improv: add error handling --- python/aws_lambda_powertools/utils/factory.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index 124c66c2cfc..399500983ce 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -1,4 +1,9 @@ import functools +import logging +import os + +logger = logging.getLogger(__name__) +logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) def lambda_handler_decorator(decorator): @@ -54,7 +59,11 @@ def final_decorator(func=None, **kwargs): @functools.wraps(func) def wrapper(event, context): - return decorator(func, event, context, **kwargs) + try: + return decorator(func, event, context, **kwargs) + except Exception as err: + logger.error(f"Caught exception in {decorator.__qualname__}") + raise err return wrapper From 5364ca9305a193c9d5b96f7700a8a21ad8a588b5 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sun, 12 Apr 2020 20:08:36 +0100 Subject: [PATCH 04/36] chore: type hint --- python/aws_lambda_powertools/utils/factory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index 399500983ce..e3492b603bd 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -1,12 +1,13 @@ import functools import logging import os +from typing import Callable logger = logging.getLogger(__name__) logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) -def lambda_handler_decorator(decorator): +def lambda_handler_decorator(decorator: Callable): """Decorator factory for decorating Lambda handlers. You can use lambda_handler_decorator to create your own middlewares, @@ -52,7 +53,7 @@ def lambda_handler(event, context): """ @functools.wraps(decorator) - def final_decorator(func=None, **kwargs): + def final_decorator(func: Callable = None, **kwargs): # If called with args return new func with args if func is None: return functools.partial(final_decorator, **kwargs) From 55c23d4c962848d1538e4917b8f3f52e8f93fc5a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 14:08:57 +0100 Subject: [PATCH 05/36] docs: include pypi downloads badge --- python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/README.md b/python/README.md index 033bdeb787c..d312db8028a 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)](https://badge.fury.io/py/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) 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. From 31eb0e253308c08b52324cfa7b320727a8af60a1 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 15:01:34 +0100 Subject: [PATCH 06/36] feat: opt in to trace each middleware that runs --- .../aws_lambda_powertools/tracing/tracer.py | 14 ++++++------- python/aws_lambda_powertools/utils/factory.py | 20 ++++++++++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/python/aws_lambda_powertools/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 20a5a4b096c..318506107ff 100644 --- a/python/aws_lambda_powertools/tracing/tracer.py +++ b/python/aws_lambda_powertools/tracing/tracer.py @@ -134,7 +134,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 +148,7 @@ def decorate(event, context): self.put_metadata(f"{self.service}_error", err) raise err finally: - self.__end_subsegment() + self.end_subsegment() return response @@ -183,7 +183,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 +197,7 @@ def decorate(*args, **kwargs): self.put_metadata(f"{method_name} error", err) raise err finally: - self.__end_subsegment() + self.end_subsegment() return response @@ -257,7 +257,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 +271,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 +296,7 @@ def __create_subsegment(self, name: str) -> models.subsegment: return subsegment - def __end_subsegment(self): + def end_subsegment(self): """Ends an existing subsegment Parameters diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index e3492b603bd..40161af2c3a 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -1,6 +1,7 @@ import functools import logging import os +from contextlib import contextmanager from typing import Callable logger = logging.getLogger(__name__) @@ -61,7 +62,12 @@ def final_decorator(func: Callable = None, **kwargs): @functools.wraps(func) def wrapper(event, context): try: - return decorator(func, event, context, **kwargs) + if os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False): + with _trace_middleware(middleware=decorator): + response = decorator(func, event, context, **kwargs) + else: + response = decorator(func, event, context, **kwargs) + return response except Exception as err: logger.error(f"Caught exception in {decorator.__qualname__}") raise err @@ -69,3 +75,15 @@ def wrapper(event, context): return wrapper return final_decorator + + +@contextmanager +def _trace_middleware(middleware): + try: + from ..tracing import Tracer + + tracer = Tracer() + tracer.create_subsegment(name=f"## middleware {middleware.__qualname__}") + yield + finally: + tracer.end_subsegment() From 5a79b129133f0aa532b25f2808a6adede4facf8f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 16:08:15 +0100 Subject: [PATCH 07/36] improv: add initial util tests --- python/tests/functional/test_utils.py | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 python/tests/functional/test_utils.py diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py new file mode 100644 index 00000000000..52bc69e8a0b --- /dev/null +++ b/python/tests/functional/test_utils.py @@ -0,0 +1,59 @@ +import json +from typing import Callable + +import pytest + +from aws_lambda_powertools.utils.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(capsys, 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({}, {}) From ee52a15dce30d2502628a3d3c5a34bd9ce56cb2f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 16:25:17 +0100 Subject: [PATCH 08/36] improv: test explicit and implicit trace_execution --- python/aws_lambda_powertools/utils/factory.py | 9 ++++-- python/tests/functional/test_utils.py | 30 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index 40161af2c3a..1b75a4fa421 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -8,7 +8,7 @@ logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) -def lambda_handler_decorator(decorator: Callable): +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, @@ -53,6 +53,11 @@ def lambda_handler(event, context): return True """ + if decorator is None: + return functools.partial(lambda_handler_decorator, trace_execution=trace_execution) + + trace_execution = trace_execution or os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False) + @functools.wraps(decorator) def final_decorator(func: Callable = None, **kwargs): # If called with args return new func with args @@ -62,7 +67,7 @@ def final_decorator(func: Callable = None, **kwargs): @functools.wraps(func) def wrapper(event, context): try: - if os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False): + if trace_execution: with _trace_middleware(middleware=decorator): response = decorator(func, event, context, **kwargs) else: diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py index 52bc69e8a0b..3a7e198eb76 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -49,7 +49,7 @@ def lambda_handler(evt, ctx): assert "goodbye after lambda handler is executed" in output -def test_factory_exception_propagation(capsys, say_bye_middleware, say_hi_middleware): +def test_factory_exception_propagation(say_bye_middleware, say_hi_middleware): @say_bye_middleware @say_hi_middleware def lambda_handler(evt, ctx): @@ -57,3 +57,31 @@ def lambda_handler(evt, ctx): 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({}, {}) From f57d000d02deb4ef216994bbf26c6e45ac3694cc Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 17:09:20 +0100 Subject: [PATCH 09/36] improv: test decorator with params --- python/tests/functional/test_utils.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py index 3a7e198eb76..66c4ed6ec47 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -58,12 +58,14 @@ def lambda_handler(evt, ctx): 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 + return ret @no_op def lambda_handler(evt, ctx): @@ -71,17 +73,36 @@ def lambda_handler(evt, ctx): 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 + return ret @no_op def lambda_handler(evt, ctx): return True lambda_handler({}, {}) + + +def test_factory_decorator_with_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 From 19d02dcb389b7b2d923346a78cc2381d4441ec8d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 17:12:25 +0100 Subject: [PATCH 10/36] chore: linting --- python/aws_lambda_powertools/utils/__init__.py | 5 ++++- python/tests/functional/test_utils.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/python/aws_lambda_powertools/utils/__init__.py b/python/aws_lambda_powertools/utils/__init__.py index ab899abff25..9d57d843ec2 100644 --- a/python/aws_lambda_powertools/utils/__init__.py +++ b/python/aws_lambda_powertools/utils/__init__.py @@ -1 +1,4 @@ -""" Utilities to enhance middlewares """ \ No newline at end of file +""" Utilities to enhance middlewares """ +from .factory import lambda_handler_decorator + +__all__ = ["lambda_handler_decorator"] diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py index 66c4ed6ec47..5214ee4460e 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -3,7 +3,7 @@ import pytest -from aws_lambda_powertools.utils.factory import lambda_handler_decorator +from aws_lambda_powertools.utils import lambda_handler_decorator @pytest.fixture From 021a32415df9ccb36ede860f8262e7fe68ee75c1 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Tue, 14 Apr 2020 17:25:55 +0100 Subject: [PATCH 11/36] docs: include utilities --- python/README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/python/README.md b/python/README.md index d312db8028a..2963832f67a 100644 --- a/python/README.md +++ b/python/README.md @@ -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" | utils 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 @@ -154,7 +162,7 @@ def handler(event, context) } ``` -#### Custom Metrics async +### Custom Metrics async > **NOTE** `log_metric` will be removed once it's GA. @@ -204,6 +212,72 @@ 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.utils 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_before_after +@middleware_name +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) + + response = handler(event, context) + return response + +@obfuscate_sensitive_data(fields=["email"]) +def lambda_handler(event, context): + return True +``` + +**Optionally trace middleware execution** + +```python +from aws_lambda_powertools.utils 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 +``` + ## Beta > **[Progress towards GA](https://github.com/awslabs/aws-lambda-powertools/projects/1)** From b3cee6c95d942bd6336f881e27717e8480c34639 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 16:49:38 +0100 Subject: [PATCH 12/36] improv: correct tests, dec_factory only for func --- .../aws_lambda_powertools/logging/logger.py | 31 ++++++------------- python/aws_lambda_powertools/utils/factory.py | 18 +++++++++-- python/tests/functional/test_logger.py | 4 +-- python/tests/functional/test_metrics.py | 2 +- python/tests/functional/test_utils.py | 15 ++++++++- 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/python/aws_lambda_powertools/logging/logger.py b/python/aws_lambda_powertools/logging/logger.py index de344f1f569..9ffba795c6b 100644 --- a/python/aws_lambda_powertools/logging/logger.py +++ b/python/aws_lambda_powertools/logging/logger.py @@ -1,4 +1,3 @@ -import functools import itertools import logging import os @@ -8,6 +7,7 @@ from typing import Any, Callable, Dict from ..helper.models import MetricUnit, build_lambda_context_model, build_metric_unit_from_str +from ..utils import lambda_handler_decorator from . import aws_lambda_logging logger = logging.getLogger(__name__) @@ -82,7 +82,8 @@ def logger_setup(service: str = "service_undefined", level: str = "INFO", sampli return logger -def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = False): +@lambda_handler_decorator +def logger_inject_lambda_context(lambda_handler: Callable, event: Dict, context: Any, log_event: bool = False): """Decorator to capture Lambda contextual info and inject into struct logging Parameters @@ -127,31 +128,19 @@ def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = No Decorated lambda handler """ - # 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 - if lambda_handler is None: - logger.debug("Decorator called with parameters") - return functools.partial(logger_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 - @functools.wraps(lambda_handler) - def decorate(event, context): - if log_event: - logger.debug("Event received") - logger.info(event) - - lambda_context = build_lambda_context_model(context) - cold_start = __is_cold_start() + if log_event: + logger.debug("Event received") + logger.info(event) - logger_setup(cold_start=cold_start, **lambda_context.__dict__) + lambda_context = build_lambda_context_model(context) + cold_start = __is_cold_start() - return lambda_handler(event, context) + logger_setup(cold_start=cold_start, **lambda_context.__dict__) - return decorate + return lambda_handler(event, context) def __is_cold_start() -> str: diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/factory.py index 1b75a4fa421..4c02c7cf655 100644 --- a/python/aws_lambda_powertools/utils/factory.py +++ b/python/aws_lambda_powertools/utils/factory.py @@ -1,4 +1,5 @@ import functools +import inspect import logging import os from contextlib import contextmanager @@ -51,6 +52,11 @@ def obfuscate_sensitive_data(handler, event, context, fields=None): @obfuscate_sensitive_data(fields=["email"]) def lambda_handler(event, context): return True + + Raises + ------ + TypeError + When middleware receives non keyword=arguments """ if decorator is None: @@ -60,18 +66,24 @@ def lambda_handler(event, context): @functools.wraps(decorator) def final_decorator(func: Callable = None, **kwargs): - # If called with args return new func with args + # 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: with _trace_middleware(middleware=decorator): - response = decorator(func, event, context, **kwargs) + response = middleware() else: - response = decorator(func, event, context, **kwargs) + response = middleware() return response except Exception as err: logger.error(f"Caught exception in {decorator.__qualname__}") 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..408dc93bc8f 100644 --- a/python/tests/functional/test_metrics.py +++ b/python/tests/functional/test_metrics.py @@ -208,7 +208,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_utils.py b/python/tests/functional/test_utils.py index 5214ee4460e..bdfbb941f1b 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -90,7 +90,7 @@ def lambda_handler(evt, ctx): lambda_handler({}, {}) -def test_factory_decorator_with_params(capsys): +def test_factory_decorator_with_kwarg_params(capsys): @lambda_handler_decorator def log_event(handler, event, context, log_event=False): if log_event: @@ -106,3 +106,16 @@ def lambda_handler(evt, ctx): 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 From 3390c525a883055ee558582241d8551a0c5d2694 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 16:51:18 +0100 Subject: [PATCH 13/36] improv: make util name more explicit --- python/aws_lambda_powertools/utils/__init__.py | 2 +- .../utils/{factory.py => middleware_factory.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename python/aws_lambda_powertools/utils/{factory.py => middleware_factory.py} (100%) diff --git a/python/aws_lambda_powertools/utils/__init__.py b/python/aws_lambda_powertools/utils/__init__.py index 9d57d843ec2..81833d777de 100644 --- a/python/aws_lambda_powertools/utils/__init__.py +++ b/python/aws_lambda_powertools/utils/__init__.py @@ -1,4 +1,4 @@ """ Utilities to enhance middlewares """ -from .factory import lambda_handler_decorator +from .middleware_factory import lambda_handler_decorator __all__ = ["lambda_handler_decorator"] diff --git a/python/aws_lambda_powertools/utils/factory.py b/python/aws_lambda_powertools/utils/middleware_factory.py similarity index 100% rename from python/aws_lambda_powertools/utils/factory.py rename to python/aws_lambda_powertools/utils/middleware_factory.py From 4baf1185c444f7a4e9ab5b66ec5011cec8f20152 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 17:35:37 +0100 Subject: [PATCH 14/36] improv: doc trace_execution, fix casting --- .../utils/middleware_factory.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/python/aws_lambda_powertools/utils/middleware_factory.py b/python/aws_lambda_powertools/utils/middleware_factory.py index 4c02c7cf655..e80050974da 100644 --- a/python/aws_lambda_powertools/utils/middleware_factory.py +++ b/python/aws_lambda_powertools/utils/middleware_factory.py @@ -3,6 +3,7 @@ import logging import os from contextlib import contextmanager +from distutils.util import strtobool from typing import Callable logger = logging.getLogger(__name__) @@ -15,7 +16,25 @@ def lambda_handler_decorator(decorator: Callable = None, trace_execution=False): You can use lambda_handler_decorator to create your own middlewares, where your function signature follows: fn(handler, event, context) - You can also set your own key=value params: fn(handler, event, context, option=value) + You can also set your own key=value params e.g. fn(handler, event, context, option=value) + Non-key value params are not supported e.g. fn(handler, event, context, option) + + 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. + + Environment variables + --------------------- + POWERTOOLS_TRACE_MIDDLEWARES : str + uses 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. + Env: POWERTOOLS_TRACE_MIDDLEWARES="true" + Example ------- @@ -53,6 +72,18 @@ def obfuscate_sensitive_data(handler, event, context, fields=None): def lambda_handler(event, context): return True + **Trace execution of custom middleware** + + from aws_lambda_powertools.utils import lambda_handler_decorator + + @lambda_handler_decorator(trace_execution=True) + def log_response(handler, event, context): + ... + + @log_response + def lambda_handler(event, context): + return True + Raises ------ TypeError @@ -62,7 +93,7 @@ def lambda_handler(event, context): if decorator is None: return functools.partial(lambda_handler_decorator, trace_execution=trace_execution) - trace_execution = trace_execution or os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False) + trace_execution = trace_execution or strtobool(str(os.getenv("POWERTOOLS_TRACE_MIDDLEWARES", False))) @functools.wraps(decorator) def final_decorator(func: Callable = None, **kwargs): From e7c2bfe609c3ce4c94f99cb762acd9a0e3f85187 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 17:50:35 +0100 Subject: [PATCH 15/36] docs: add limitations, improve syntax --- .../utils/middleware_factory.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/python/aws_lambda_powertools/utils/middleware_factory.py b/python/aws_lambda_powertools/utils/middleware_factory.py index e80050974da..76674183625 100644 --- a/python/aws_lambda_powertools/utils/middleware_factory.py +++ b/python/aws_lambda_powertools/utils/middleware_factory.py @@ -14,27 +14,27 @@ 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) + where your function signature follows: `fn(handler, event, context)` - You can also set your own key=value params e.g. fn(handler, event, context, option=value) - Non-key value params are not supported e.g. fn(handler, event, context, option) + 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 Tracer to create sub-segments per middleware (e.g. "true", "True", "TRUE") + 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. - Env: POWERTOOLS_TRACE_MIDDLEWARES="true" - + Flag to explicitly enable trace execution for middlewares.\n + `Env POWERTOOLS_TRACE_MIDDLEWARES="true"` Example ------- @@ -84,6 +84,11 @@ def log_response(handler, event, context): def lambda_handler(event, context): return True + Limitations + ----------- + * Async middlewares not supported + * Classes, class methods middlewares not supported + Raises ------ TypeError From 1e8af14fb6d9ca71cfe1ea7c918142d86454093e Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:05:28 +0100 Subject: [PATCH 16/36] docs: use new docs syntax --- .../aws_lambda_powertools/logging/logger.py | 92 ++++++------- .../aws_lambda_powertools/tracing/tracer.py | 122 +++++++++--------- 2 files changed, 111 insertions(+), 103 deletions(-) diff --git a/python/aws_lambda_powertools/logging/logger.py b/python/aws_lambda_powertools/logging/logger.py index 9ffba795c6b..60030c853c6 100644 --- a/python/aws_lambda_powertools/logging/logger.py +++ b/python/aws_lambda_powertools/logging/logger.py @@ -94,32 +94,34 @@ def logger_inject_lambda_context(lambda_handler: Callable, event: Dict, context: 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 @@ -167,6 +169,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. @@ -174,7 +178,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 @@ -184,23 +188,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, @@ -209,22 +229,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/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 318506107ff..0a8fdc30c28 100644 --- a/python/aws_lambda_powertools/tracing/tracer.py +++ b/python/aws_lambda_powertools/tracing/tracer.py @@ -14,7 +14,7 @@ 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 + 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. @@ -26,36 +26,44 @@ class Tracer: 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 + disabled: bool + Flag to explicitly disable tracing, useful when running 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,36 +71,33 @@ 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 - - A Lambda function using service name via POWERTOOLS_SERVICE_NAME + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + print("Received event from Lambda...") + response = greeting(name="Heitor") + return response - >>> export POWERTOOLS_SERVICE_NAME="booking" - >>> from aws_lambda_powertools.tracing import Tracer - >>> tracer = Tracer() + **A Lambda function using service name via POWERTOOLS_SERVICE_NAME** - >>> @tracer.capture_lambda_handler - >>> def handler(event: dict, context: Any) -> Dict: - >>> print("Received event from Lambda...") - >>> response = greeting(name="Lessa") - >>> return response + export POWERTOOLS_SERVICE_NAME="booking" + from aws_lambda_powertools.tracing import Tracer + tracer = Tracer() - 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" + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + print("Received event from Lambda...") + response = greeting(name="Lessa") + return response Returns ------- Tracer Tracer instance with imported modules patched + + Limitations + ----------- + * Async handler and methods not supported + """ def __init__( @@ -115,11 +120,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 ---------- @@ -162,12 +167,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 ---------- @@ -210,8 +214,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 +248,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 @@ -271,7 +275,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 ------- @@ -339,9 +343,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 ------- From 1a62571cc6320aca90cc90043004f157729d77ca Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:11:26 +0100 Subject: [PATCH 17/36] fix: remove middleware decorator from libs --- .../aws_lambda_powertools/logging/logger.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/python/aws_lambda_powertools/logging/logger.py b/python/aws_lambda_powertools/logging/logger.py index 60030c853c6..32dc1be6c1a 100644 --- a/python/aws_lambda_powertools/logging/logger.py +++ b/python/aws_lambda_powertools/logging/logger.py @@ -1,3 +1,4 @@ +import functools import itertools import logging import os @@ -7,7 +8,6 @@ from typing import Any, Callable, Dict from ..helper.models import MetricUnit, build_lambda_context_model, build_metric_unit_from_str -from ..utils import lambda_handler_decorator from . import aws_lambda_logging logger = logging.getLogger(__name__) @@ -82,8 +82,7 @@ def logger_setup(service: str = "service_undefined", level: str = "INFO", sampli return logger -@lambda_handler_decorator -def logger_inject_lambda_context(lambda_handler: Callable, event: Dict, context: Any, log_event: bool = False): +def logger_inject_lambda_context(lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = False): """Decorator to capture Lambda contextual info and inject into struct logging Parameters @@ -130,19 +129,29 @@ def handler(event, context): Decorated lambda handler """ + # If handler is None we've been called with parameters + # 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) + log_event_env_option = str(os.getenv("POWERTOOLS_LOGGER_LOG_EVENT", "false")) log_event = strtobool(log_event_env_option) or log_event - if log_event: - logger.debug("Event received") - logger.info(event) + @functools.wraps(lambda_handler) + def decorate(event, context): + if log_event: + logger.debug("Event received") + logger.info(event) + + lambda_context = build_lambda_context_model(context) + cold_start = __is_cold_start() - lambda_context = build_lambda_context_model(context) - cold_start = __is_cold_start() + logger_setup(cold_start=cold_start, **lambda_context.__dict__) - logger_setup(cold_start=cold_start, **lambda_context.__dict__) + return lambda_handler(event, context) - return lambda_handler(event, context) + return decorate def __is_cold_start() -> str: From 9d08ea0b524e9c7a9e02b549b5f52f5dcf611341 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:21:04 +0100 Subject: [PATCH 18/36] feat: build docs in CI --- .github/workflows/python_docs.yml | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/python_docs.yml diff --git a/.github/workflows/python_docs.yml b/.github/workflows/python_docs.yml new file mode 100644 index 00000000000..aad141b7f69 --- /dev/null +++ b/.github/workflows/python_docs.yml @@ -0,0 +1,40 @@ +name: deploy + +on: + pull_request: + branches: + - develop + - master + paths: + - "python/**" + push: + branches: + - develop + - master + paths: + - "python/**" + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + make dev + working-directory: ./python/ + - name: build docs + run: | + make docs + working-directory: ./python/ + - name: deploy docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/aws_lambda_powertools/ + working-directory: ./python/ From 333e5c830218e98278972bc44707369f2ecfa169 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:22:29 +0100 Subject: [PATCH 19/36] chore: linting --- python/aws_lambda_powertools/utils/middleware_factory.py | 3 ++- python/tests/functional/test_utils.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python/aws_lambda_powertools/utils/middleware_factory.py b/python/aws_lambda_powertools/utils/middleware_factory.py index 76674183625..2ed25c11234 100644 --- a/python/aws_lambda_powertools/utils/middleware_factory.py +++ b/python/aws_lambda_powertools/utils/middleware_factory.py @@ -26,7 +26,8 @@ def lambda_handler_decorator(decorator: Callable = None, trace_execution=False): Environment variables --------------------- POWERTOOLS_TRACE_MIDDLEWARES : str - uses `aws_lambda_powertools.tracing.Tracer` to create sub-segments per middleware (e.g. `"true", "True", "TRUE"`) + uses `aws_lambda_powertools.tracing.Tracer` + to create sub-segments per middleware (e.g. `"true", "True", "TRUE"`) Parameters ---------- diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py index bdfbb941f1b..c5d590afa8c 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -116,6 +116,7 @@ def log_event(handler, event, context, log_event=False): return handler(event, context) with pytest.raises(TypeError): + @log_event(True) def lambda_handler(evt, ctx): return True From abbb12df44ce978cba263fb38704855c35d231cb Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:31:25 +0100 Subject: [PATCH 20/36] fix: CI python-version type --- .github/workflows/python_docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python_docs.yml b/.github/workflows/python_docs.yml index aad141b7f69..26b5fb4a615 100644 --- a/.github/workflows/python_docs.yml +++ b/.github/workflows/python_docs.yml @@ -1,4 +1,4 @@ -name: deploy +name: Powertools Docs Python on: pull_request: @@ -22,7 +22,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: "3.8" - name: Install dependencies run: | python -m pip install --upgrade pip From 600c9b1aee4cda65bec5ef160df43c3f73e82423 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Wed, 15 Apr 2020 18:39:00 +0100 Subject: [PATCH 21/36] chore: remove docs CI --- .github/workflows/python_docs.yml | 40 ------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/python_docs.yml diff --git a/.github/workflows/python_docs.yml b/.github/workflows/python_docs.yml deleted file mode 100644 index 26b5fb4a615..00000000000 --- a/.github/workflows/python_docs.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Powertools Docs Python - -on: - pull_request: - branches: - - develop - - master - paths: - - "python/**" - push: - branches: - - develop - - master - paths: - - "python/**" - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: "3.8" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - make dev - working-directory: ./python/ - - name: build docs - run: | - make docs - working-directory: ./python/ - - name: deploy docs - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/aws_lambda_powertools/ - working-directory: ./python/ From 4f1ed1fff5fcaa2d093b710228a1b9f75619290c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 16 Apr 2020 08:30:24 +0100 Subject: [PATCH 22/36] chore: kick CI --- .github/workflows/pythonpackage.yml | 1 - python/Makefile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f52ff952361..5e4dcc57f74 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -29,7 +29,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip make dev working-directory: ./python/ - name: Formatting and Linting diff --git a/python/Makefile b/python/Makefile index a6ce4aefeb9..b3c1cbd476f 100644 --- a/python/Makefile +++ b/python/Makefile @@ -3,7 +3,7 @@ target: @$(MAKE) pr dev: - pip install --upgrade poetry + pip install --upgrade pip poetry poetry install format: From 4d1548fd01d9f20bfa626132dbb7b3fb83d3248f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 16 Apr 2020 08:59:29 +0100 Subject: [PATCH 23/36] chore: include build badge master branch --- python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/README.md b/python/README.md index 2963832f67a..6fec0077b16 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. From c0e32272bacd44cb4e657d2e50774d7cd90d5f9e Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 17 Apr 2020 09:32:17 +0100 Subject: [PATCH 24/36] chore: refactor naming --- python/README.md | 4 ++-- .../{utils => middleware_factory}/__init__.py | 2 +- .../middleware_factory.py => middleware_factory/factory.py} | 6 +++--- python/tests/functional/test_utils.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename python/aws_lambda_powertools/{utils => middleware_factory}/__init__.py (58%) rename python/aws_lambda_powertools/{utils/middleware_factory.py => middleware_factory/factory.py} (94%) diff --git a/python/README.md b/python/README.md index 6fec0077b16..3058d5df565 100644 --- a/python/README.md +++ b/python/README.md @@ -226,7 +226,7 @@ This feature allows you to create your own middleware as a decorator with ease b **Middleware with no params** ```python -from aws_lambda_powertools.utils import lambda_handler_decorator +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @lambda_handler_decorator def middleware_name(handler, event, context): @@ -267,7 +267,7 @@ def lambda_handler(event, context): **Optionally trace middleware execution** ```python -from aws_lambda_powertools.utils import lambda_handler_decorator +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @lambda_handler_decorator(trace_execution=True) def middleware_name(handler, event, context): diff --git a/python/aws_lambda_powertools/utils/__init__.py b/python/aws_lambda_powertools/middleware_factory/__init__.py similarity index 58% rename from python/aws_lambda_powertools/utils/__init__.py rename to python/aws_lambda_powertools/middleware_factory/__init__.py index 81833d777de..9d57d843ec2 100644 --- a/python/aws_lambda_powertools/utils/__init__.py +++ b/python/aws_lambda_powertools/middleware_factory/__init__.py @@ -1,4 +1,4 @@ """ Utilities to enhance middlewares """ -from .middleware_factory import lambda_handler_decorator +from .factory import lambda_handler_decorator __all__ = ["lambda_handler_decorator"] diff --git a/python/aws_lambda_powertools/utils/middleware_factory.py b/python/aws_lambda_powertools/middleware_factory/factory.py similarity index 94% rename from python/aws_lambda_powertools/utils/middleware_factory.py rename to python/aws_lambda_powertools/middleware_factory/factory.py index 2ed25c11234..056e94c9810 100644 --- a/python/aws_lambda_powertools/utils/middleware_factory.py +++ b/python/aws_lambda_powertools/middleware_factory/factory.py @@ -41,7 +41,7 @@ def lambda_handler_decorator(decorator: Callable = None, trace_execution=False): ------- **Create a middleware no params** - from aws_lambda_powertools.utils import lambda_handler_decorator + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @lambda_handler_decorator def log_response(handler, event, context): @@ -56,7 +56,7 @@ def lambda_handler(event, context): **Create a middleware with params** - from aws_lambda_powertools.utils import lambda_handler_decorator + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @lambda_handler_decorator def obfuscate_sensitive_data(handler, event, context, fields=None): @@ -75,7 +75,7 @@ def lambda_handler(event, context): **Trace execution of custom middleware** - from aws_lambda_powertools.utils import lambda_handler_decorator + from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @lambda_handler_decorator(trace_execution=True) def log_response(handler, event, context): diff --git a/python/tests/functional/test_utils.py b/python/tests/functional/test_utils.py index c5d590afa8c..141acf9d96f 100644 --- a/python/tests/functional/test_utils.py +++ b/python/tests/functional/test_utils.py @@ -3,7 +3,7 @@ import pytest -from aws_lambda_powertools.utils import lambda_handler_decorator +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator @pytest.fixture From 69b3529e91ac8695f2df1c0cb08a508e36296e17 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 17 Apr 2020 10:58:18 +0100 Subject: [PATCH 25/36] fix: rearrange tracing tests --- python/tests/functional/test_tracing.py | 148 ++++++++++-------------- python/tests/unit/test_tracing.py | 143 ++++++++++++++--------- 2 files changed, 151 insertions(+), 140 deletions(-) diff --git a/python/tests/functional/test_tracing.py b/python/tests/functional/test_tracing.py index b77710090e8..dc1022b1a53 100644 --- a/python/tests/functional/test_tracing.py +++ b/python/tests/functional/test_tracing.py @@ -8,70 +8,25 @@ 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") +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()) - - 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" - ) + handler({}, {}) -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() +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 - 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 +34,74 @@ 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() -def test_tracer_custom_annotation(mocker, dummy_response, xray_stub): - put_annotation_mock = mocker.MagicMock() + @tracer.capture_lambda_handler + def handler(event, context): + return dummy_response + + handler({}, {}) - 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({}, {}) + - assert put_annotation_mock.call_count == 1 - assert put_annotation_mock.call_args == mocker.call(key=annotation_key, value=annotation_value) +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 -def test_tracer_custom_metadata(mocker, dummy_response, xray_stub): - put_metadata_mock = mocker.MagicMock() + tracer_explicit = Tracer(disabled=True, service=service_name) + assert tracer_explicit.service == service_name - xray_provider = xray_stub(put_metadata_mock=put_metadata_mock) + monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") + tracer = Tracer() - tracer = Tracer(provider=xray_provider, service="booking") - annotation_key = "Booking response" - annotation_value = {"bookingStatus": "CONFIRMED"} + assert bool(tracer.disabled) is True + + +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") - handler({}, mocker.MagicMock()) + with pytest.raises(CustomException): + handler({}, {}) - 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" - ) + with pytest.raises(CustomException): + greeting(name="Foo", message="Bar") diff --git a/python/tests/unit/test_tracing.py b/python/tests/unit/test_tracing.py index 91144d64f9d..11243b914f3 100644 --- a/python/tests/unit/test_tracing.py +++ b/python/tests/unit/test_tracing.py @@ -8,11 +8,48 @@ 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() + + 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") @tracer.capture_lambda_handler def handler(event, context): @@ -20,13 +57,22 @@ def handler(event, context): 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_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 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() - tracer = Tracer(disabled=True) + 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): @@ -34,74 +80,61 @@ 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_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_custom_annotation(mocker, dummy_response, xray_stub): + put_annotation_mock = mocker.MagicMock() - tracer = Tracer(disabled=True) + xray_provider = xray_stub(put_annotation_mock=put_annotation_mock) + + tracer = Tracer(provider=xray_provider, service="booking") + annotation_key = "BookingId" + annotation_value = "123456" @tracer.capture_lambda_handler def handler(event, context): - raise CustomException("test") + tracer.put_annotation(annotation_key, annotation_value) + return dummy_response - @tracer.capture_method - def greeting(name, message): - raise CustomException("test") + handler({}, mocker.MagicMock()) + + assert put_annotation_mock.call_count == 1 + assert put_annotation_mock.call_args == mocker.call(key=annotation_key, value=annotation_value) - with pytest.raises(CustomException): - handler({}, mocker.MagicMock()) - with pytest.raises(CustomException): - greeting(name="Foo", message="Bar") +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) -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() + 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 - monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") - tracer = Tracer() - 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.capture_lambda_handler - def handler(event, context): - tracer.put_annotation("PaymentStatus", "SUCCESS") - tracer.put_metadata("PaymentMetadata", "Metadata") - return dummy_response - handler({}, mocker.MagicMock()) From c3419c6502e01090c6dcefda420e8d993c1dc97b Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 17 Apr 2020 13:04:33 +0100 Subject: [PATCH 26/36] improv(tracer): toggle default auto patching --- python/aws_lambda_powertools/tracing/tracer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/python/aws_lambda_powertools/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 0a8fdc30c28..9a3fb2e40ac 100644 --- a/python/aws_lambda_powertools/tracing/tracer.py +++ b/python/aws_lambda_powertools/tracing/tracer.py @@ -20,7 +20,7 @@ class Tracer: 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. \n Ref: https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/thirdparty.html Environment variables @@ -34,6 +34,8 @@ class Tracer: ---------- 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 locally. `Env POWERTOOLS_TRACE_DISABLED="true"` @@ -101,16 +103,22 @@ def handler(event: dict, context: Any) -> Dict: """ def __init__( - self, service: str = "service_undefined", disabled: bool = False, provider: xray_recorder = xray_recorder, + self, + service: str = "service_undefined", + disabled: bool = False, + provider: xray_recorder = xray_recorder, + auto_patch: bool = True, ): self.provider = provider self.disabled = self.__is_trace_disabled() or disabled self.service = os.getenv("POWERTOOLS_SERVICE_NAME") or service + self.auto_patch = auto_patch if self.disabled: self.__disable_tracing_provider() - self.__patch() + if auto_patch: + self.patch() def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None): """Decorator to create subsegment for lambda handlers @@ -314,7 +322,7 @@ def end_subsegment(self): self.provider.end_subsegment() - def __patch(self): + def patch(self): """Patch modules for instrumentation """ logger.debug("Patching modules...") From 365d561863bd8aa59fbe6edebe99b73c7810bbb7 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 17 Apr 2020 20:44:48 +0100 Subject: [PATCH 27/36] feat(tracer): retrieve registered class instance --- .../aws_lambda_powertools/helper/register.py | 33 +++++++++++++++++ .../middleware_factory/factory.py | 2 +- .../aws_lambda_powertools/tracing/tracer.py | 19 +++++++++- python/tests/functional/test_tracing.py | 35 ++++++++++++++++++ python/tests/unit/test_tracing.py | 37 +++++++------------ 5 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 python/aws_lambda_powertools/helper/register.py diff --git a/python/aws_lambda_powertools/helper/register.py b/python/aws_lambda_powertools/helper/register.py new file mode 100644 index 00000000000..d2711ea962a --- /dev/null +++ b/python/aws_lambda_powertools/helper/register.py @@ -0,0 +1,33 @@ +class RegisterMeta(type): + _instance = None + + def __call__(cls, *args, **kwargs): + """Register class instance at first initialization + + It only returns an existing instance via `instance` + method e.g. `Tracer.instance()`. + + Not a Singleton per se as it only returns an existing + instance via the `instance` method. + """ + if cls._instance is None: + cls._instance = super().__call__(*args, **kwargs) + return cls._instance + return super().__call__(*args, **kwargs) + + def instance(cls): + """Returns registered class instance + + This allows us to prevent double initialization + when needed, reuse previous instance and its attributes, + and still allow multiple inheritance and __new__. + """ + if cls._instance is None: + return cls.__call__(cls) + + return cls._instance + + def clear_instance(cls): + """Destroys registered class instance""" + if cls._instance is not None: + cls._instance = None diff --git a/python/aws_lambda_powertools/middleware_factory/factory.py b/python/aws_lambda_powertools/middleware_factory/factory.py index 056e94c9810..a939382dcc4 100644 --- a/python/aws_lambda_powertools/middleware_factory/factory.py +++ b/python/aws_lambda_powertools/middleware_factory/factory.py @@ -136,7 +136,7 @@ def _trace_middleware(middleware): try: from ..tracing import Tracer - tracer = Tracer() + tracer = Tracer.instance() tracer.create_subsegment(name=f"## middleware {middleware.__qualname__}") yield finally: diff --git a/python/aws_lambda_powertools/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 9a3fb2e40ac..92644b8ebea 100644 --- a/python/aws_lambda_powertools/tracing/tracer.py +++ b/python/aws_lambda_powertools/tracing/tracer.py @@ -6,12 +6,14 @@ from aws_xray_sdk.core import models, patch_all, xray_recorder +from ..helper.register import RegisterMeta + is_cold_start = True logger = logging.getLogger(__name__) logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) -class Tracer: +class Tracer(metaclass=RegisterMeta): """Tracer using AWS-XRay to provide decorators with known defaults for Lambda functions When running locally, it honours `POWERTOOLS_TRACE_DISABLED` environment variable @@ -91,6 +93,21 @@ def handler(event: dict, context: Any) -> Dict: response = greeting(name="Lessa") return response + **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.instance() + ... + Returns ------- Tracer diff --git a/python/tests/functional/test_tracing.py b/python/tests/functional/test_tracing.py index dc1022b1a53..2d7e968c78b 100644 --- a/python/tests/functional/test_tracing.py +++ b/python/tests/functional/test_tracing.py @@ -8,6 +8,12 @@ def dummy_response(): return {"test": "succeeds"} +@pytest.fixture(scope="function", autouse=True) +def reset_singleton(): + yield + Tracer.clear_instance() + + def test_capture_lambda_handler(dummy_response): # GIVEN tracer is disabled, and decorator is used # WHEN a lambda handler is run @@ -105,3 +111,32 @@ def greeting(name, message): with pytest.raises(CustomException): greeting(name="Foo", message="Bar") + + +def test_tracer_reuse(): + # GIVEN tracer A, B and C were initialized + # WHEN tracer B explicitly reuses A instance + # THEN tracer B attributes should be equal to tracer A + # and tracer C should use have defaults instead + service_name = "booking" + tracer_a = Tracer(disabled=True, service=service_name) + tracer_b = Tracer.instance() + tracer_c = Tracer() + + assert id(tracer_a) == id(tracer_b) + assert id(tracer_a) != id(tracer_c) + assert id(tracer_b) != id(tracer_c) + + +def test_tracer_reuse_no_instance(): + # GIVEN tracer A and B instance were not registered + # WHEN instance is method is explicitly called + # THEN we initialize a new instance and register it + # so that tracer B receives a copy of the instance + # and only one __init__ is executed + tracer_a = Tracer.instance() + tracer_b = Tracer.instance() + + assert id(tracer_a) == id(tracer_b) + + Tracer.clear_instance() diff --git a/python/tests/unit/test_tracing.py b/python/tests/unit/test_tracing.py index 11243b914f3..ea98b793a1c 100644 --- a/python/tests/unit/test_tracing.py +++ b/python/tests/unit/test_tracing.py @@ -38,7 +38,6 @@ def end_subsegment(self, *args, **kwargs): return XRayStub - def test_tracer_lambda_handler(mocker, dummy_response, xray_stub): put_metadata_mock = mocker.MagicMock() begin_subsegment_mock = mocker.MagicMock() @@ -88,26 +87,6 @@ def greeting(name, message): ) -def test_tracer_custom_annotation(mocker, dummy_response, xray_stub): - put_annotation_mock = mocker.MagicMock() - - xray_provider = xray_stub(put_annotation_mock=put_annotation_mock) - - tracer = Tracer(provider=xray_provider, service="booking") - annotation_key = "BookingId" - annotation_value = "123456" - - @tracer.capture_lambda_handler - def handler(event, context): - 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) - - def test_tracer_custom_metadata(mocker, dummy_response, xray_stub): put_metadata_mock = mocker.MagicMock() @@ -130,11 +109,21 @@ def handler(event, context): ) +def test_tracer_custom_annotation(mocker, dummy_response, xray_stub): + put_annotation_mock = mocker.MagicMock() + xray_provider = xray_stub(put_annotation_mock=put_annotation_mock) + tracer = Tracer(provider=xray_provider, service="booking") + annotation_key = "BookingId" + annotation_value = "123456" + @tracer.capture_lambda_handler + def handler(event, context): + 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) From c3aad5fb16f59f3f7665e9efea5128854cd24c36 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 17 Apr 2020 20:45:36 +0100 Subject: [PATCH 28/36] fix(Makefile): make cov target more explicit --- python/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 98aeb7721758c50d2022f0b7c9566dbeafeae79a Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 18 Apr 2020 13:01:18 +0100 Subject: [PATCH 29/36] improv(Register): support multiple classes reg. --- .../aws_lambda_powertools/helper/register.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/python/aws_lambda_powertools/helper/register.py b/python/aws_lambda_powertools/helper/register.py index d2711ea962a..1424a0df43a 100644 --- a/python/aws_lambda_powertools/helper/register.py +++ b/python/aws_lambda_powertools/helper/register.py @@ -1,8 +1,8 @@ class RegisterMeta(type): - _instance = None + _instances = {} def __call__(cls, *args, **kwargs): - """Register class instance at first initialization + """Register class instance at initialization It only returns an existing instance via `instance` method e.g. `Tracer.instance()`. @@ -10,10 +10,11 @@ def __call__(cls, *args, **kwargs): Not a Singleton per se as it only returns an existing instance via the `instance` method. """ - if cls._instance is None: - cls._instance = super().__call__(*args, **kwargs) - return cls._instance - return super().__call__(*args, **kwargs) + if cls not in RegisterMeta._instances: + RegisterMeta._instances[cls] = super().__call__(*args, **kwargs) + return RegisterMeta._instances[cls] + + return super().__call__(**kwargs) def instance(cls): """Returns registered class instance @@ -22,12 +23,12 @@ def instance(cls): when needed, reuse previous instance and its attributes, and still allow multiple inheritance and __new__. """ - if cls._instance is None: + if cls not in RegisterMeta._instances: return cls.__call__(cls) - return cls._instance + return RegisterMeta._instances[cls] def clear_instance(cls): """Destroys registered class instance""" - if cls._instance is not None: - cls._instance = None + if cls in RegisterMeta._instances: + del RegisterMeta._instances[cls] From 37d6a19dd800d1802d239915c3136ada913ae5ac Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 18 Apr 2020 13:31:26 +0100 Subject: [PATCH 30/36] improv(Register): inject class methods correctly --- .../aws_lambda_powertools/helper/register.py | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/python/aws_lambda_powertools/helper/register.py b/python/aws_lambda_powertools/helper/register.py index 1424a0df43a..951fe5ed2f5 100644 --- a/python/aws_lambda_powertools/helper/register.py +++ b/python/aws_lambda_powertools/helper/register.py @@ -1,7 +1,10 @@ +from typing import AnyStr, Dict, Tuple, Type + + class RegisterMeta(type): _instances = {} - def __call__(cls, *args, **kwargs): + def __call__(cls: Type, *args: Tuple, **kwargs: Dict): """Register class instance at initialization It only returns an existing instance via `instance` @@ -9,26 +12,54 @@ def __call__(cls, *args, **kwargs): Not a Singleton per se as it only returns an existing instance via the `instance` method. + + Parameters + ---------- + cls : type + Class using metaclass + args : tuple + Tuple with arguments for class instantiation + kwargs : dict + Dict with all keyword arguments """ if cls not in RegisterMeta._instances: RegisterMeta._instances[cls] = super().__call__(*args, **kwargs) return RegisterMeta._instances[cls] - + return super().__call__(**kwargs) - def instance(cls): - """Returns registered class instance + def __init__(cls: Type, cls_name: AnyStr, bases: Tuple, class_dict: Dict): + """Inject instance, clear_instance classmethods to newly built class - This allows us to prevent double initialization - when needed, reuse previous instance and its attributes, - and still allow multiple inheritance and __new__. + Parameters + ---------- + cls : type + Class using metaclass + cls_name : str + Class name + bases : tuple + Inherited classes + class_dict : dict + Class body as dict """ - if cls not in RegisterMeta._instances: - return cls.__call__(cls) + setattr(cls, instance.__name__, classmethod(instance)) + setattr(cls, clear_instance.__name__, classmethod(clear_instance)) + + +def instance(cls): + """Returns registered class instance + + This allows us to prevent double initialization + when needed, reuse previous instance and its attributes, + and still allow multiple inheritance and __new__. + """ + if cls not in RegisterMeta._instances: + return cls.__call__(cls) + + return RegisterMeta._instances[cls] - return RegisterMeta._instances[cls] - def clear_instance(cls): - """Destroys registered class instance""" - if cls in RegisterMeta._instances: - del RegisterMeta._instances[cls] +def clear_instance(cls): + """Destroys registered class instance""" + if cls in RegisterMeta._instances: + del RegisterMeta._instances[cls] From 250e47fae1267b7472992860307d5ba661b30d57 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 18 Apr 2020 13:48:40 +0100 Subject: [PATCH 31/36] docs: add how to reutilize Tracer --- python/README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/python/README.md b/python/README.md index 3058d5df565..654354675d3 100644 --- a/python/README.md +++ b/python/README.md @@ -45,7 +45,7 @@ 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" | utils +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 @@ -93,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.instance() +``` ### Logging @@ -266,6 +283,8 @@ def lambda_handler(event, context): **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 @@ -278,6 +297,24 @@ 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.instance() # 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)** From bfe3404a50e3df10ef340635e8baf474d739e61c Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 18 Apr 2020 21:06:46 +0100 Subject: [PATCH 32/36] improv(tracer): test auto patch method --- python/tests/unit/test_tracing.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/python/tests/unit/test_tracing.py b/python/tests/unit/test_tracing.py index ea98b793a1c..ce50de5b285 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 @@ -127,3 +129,27 @@ def handler(event, context): 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_a = Tracer(disabled=True) + + assert patch_mock.call_count == 1 + + tracer_b = Tracer(disabled=True, auto_patch=False) + tracer_b.patch() + + assert patch_mock.call_count == 2 + +@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_a = Tracer(disabled=True, auto_patch=False) + tracer_b = tracer_a.instance() + + assert patch_mock.call_count == 0 From 1f0a6c9de6b35e927790585d7711ec1cbb8dd564 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 20 Apr 2020 14:27:15 +0100 Subject: [PATCH 33/36] improv: address nicolas feedback --- python/README.md | 17 +++-- .../aws_lambda_powertools/helper/register.py | 65 ------------------ .../middleware_factory/factory.py | 21 ++---- .../aws_lambda_powertools/tracing/tracer.py | 68 ++++++++++++------- python/tests/functional/test_tracing.py | 32 +++------ python/tests/unit/test_tracing.py | 22 +++--- 6 files changed, 78 insertions(+), 147 deletions(-) delete mode 100644 python/aws_lambda_powertools/helper/register.py diff --git a/python/README.md b/python/README.md index 654354675d3..5a96dc80b63 100644 --- a/python/README.md +++ b/python/README.md @@ -108,7 +108,7 @@ def handler(event, context) # another_file.py from aws_lambda_powertools.tracing import Tracer -tracer = Tracer.instance() +tracer = Tracer(auto_patch=False) # new instance using existing configuration with auto patching overriden ``` ### Logging @@ -256,8 +256,14 @@ def middleware_before_after(handler, event, context): logic_after_handler_execution() return response -@middleware_before_after -@middleware_name + +# 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 ``` @@ -273,8 +279,7 @@ def obfuscate_sensitive_data(handler, event, context, fields=None): field = event.get(field, "") event[field] = obfuscate_pii(field) - response = handler(event, context) - return response + return handler(event, context) @obfuscate_sensitive_data(fields=["email"]) def lambda_handler(event, context): @@ -305,7 +310,7 @@ from aws_lambda_powertools.tracing import Tracer @lambda_handler_decorator(trace_execution=True) def middleware_name(handler, event, context): - tracer = Tracer.instance() # Takes a copy of an existing tracer instance + tracer = Tracer() # Takes a copy of an existing tracer instance tracer.add_anotation... tracer.metadata... return handler(event, context) diff --git a/python/aws_lambda_powertools/helper/register.py b/python/aws_lambda_powertools/helper/register.py deleted file mode 100644 index 951fe5ed2f5..00000000000 --- a/python/aws_lambda_powertools/helper/register.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import AnyStr, Dict, Tuple, Type - - -class RegisterMeta(type): - _instances = {} - - def __call__(cls: Type, *args: Tuple, **kwargs: Dict): - """Register class instance at initialization - - It only returns an existing instance via `instance` - method e.g. `Tracer.instance()`. - - Not a Singleton per se as it only returns an existing - instance via the `instance` method. - - Parameters - ---------- - cls : type - Class using metaclass - args : tuple - Tuple with arguments for class instantiation - kwargs : dict - Dict with all keyword arguments - """ - if cls not in RegisterMeta._instances: - RegisterMeta._instances[cls] = super().__call__(*args, **kwargs) - return RegisterMeta._instances[cls] - - return super().__call__(**kwargs) - - def __init__(cls: Type, cls_name: AnyStr, bases: Tuple, class_dict: Dict): - """Inject instance, clear_instance classmethods to newly built class - - Parameters - ---------- - cls : type - Class using metaclass - cls_name : str - Class name - bases : tuple - Inherited classes - class_dict : dict - Class body as dict - """ - setattr(cls, instance.__name__, classmethod(instance)) - setattr(cls, clear_instance.__name__, classmethod(clear_instance)) - - -def instance(cls): - """Returns registered class instance - - This allows us to prevent double initialization - when needed, reuse previous instance and its attributes, - and still allow multiple inheritance and __new__. - """ - if cls not in RegisterMeta._instances: - return cls.__call__(cls) - - return RegisterMeta._instances[cls] - - -def clear_instance(cls): - """Destroys registered class instance""" - if cls in RegisterMeta._instances: - del RegisterMeta._instances[cls] diff --git a/python/aws_lambda_powertools/middleware_factory/factory.py b/python/aws_lambda_powertools/middleware_factory/factory.py index a939382dcc4..d704425cce7 100644 --- a/python/aws_lambda_powertools/middleware_factory/factory.py +++ b/python/aws_lambda_powertools/middleware_factory/factory.py @@ -2,10 +2,11 @@ import inspect import logging import os -from contextlib import contextmanager from distutils.util import strtobool from typing import Callable +from ..tracing import Tracer + logger = logging.getLogger(__name__) logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) @@ -117,8 +118,10 @@ def wrapper(event, context): try: middleware = functools.partial(decorator, func, event, context, **kwargs) if trace_execution: - with _trace_middleware(middleware=decorator): - response = middleware() + tracer = Tracer(auto_patch=False) + tracer.create_subsegment(name=f"## middleware {decorator.__qualname__}") + response = middleware() + tracer.end_subsegment() else: response = middleware() return response @@ -129,15 +132,3 @@ def wrapper(event, context): return wrapper return final_decorator - - -@contextmanager -def _trace_middleware(middleware): - try: - from ..tracing import Tracer - - tracer = Tracer.instance() - tracer.create_subsegment(name=f"## middleware {middleware.__qualname__}") - yield - finally: - tracer.end_subsegment() diff --git a/python/aws_lambda_powertools/tracing/tracer.py b/python/aws_lambda_powertools/tracing/tracer.py index 92644b8ebea..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 @@ -6,25 +7,25 @@ from aws_xray_sdk.core import models, patch_all, xray_recorder -from ..helper.register import RegisterMeta - is_cold_start = True logger = logging.getLogger(__name__) logger.setLevel(os.getenv("LOG_LEVEL", "INFO")) -class Tracer(metaclass=RegisterMeta): +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. - - Tracing is automatically disabled when running locally via via SAM CLI. + When running locally, it detects whether it's running via SAM CLI, + and if it is it returns dummy segments/subsegments instead. - By default, it patches all available libraries supported by X-Ray SDK. \n + 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 @@ -39,7 +40,7 @@ class Tracer(metaclass=RegisterMeta): auto_patch: bool Patch existing imported modules during initialization, by default True disabled: bool - Flag to explicitly disable tracing, useful when running locally. + Flag to explicitly disable tracing, useful when running/testing locally. `Env POWERTOOLS_TRACE_DISABLED="true"` Example @@ -105,7 +106,7 @@ def handler(event: dict, context: Any) -> Dict: # utils.py from aws_lambda_powertools.tracing import Tracer - tracer = Tracer.instance() + tracer = Tracer() ... Returns @@ -119,22 +120,22 @@ def handler(event: dict, context: Any) -> Dict: """ + _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, - auto_patch: bool = True, + 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.auto_patch = auto_patch + 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() - if auto_patch: + if self.auto_patch: self.patch() def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None): @@ -340,18 +341,17 @@ def end_subsegment(self): self.provider.end_subsegment() def patch(self): - """Patch modules for instrumentation - """ + """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 @@ -390,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/tests/functional/test_tracing.py b/python/tests/functional/test_tracing.py index 2d7e968c78b..8ceb479190a 100644 --- a/python/tests/functional/test_tracing.py +++ b/python/tests/functional/test_tracing.py @@ -9,9 +9,9 @@ def dummy_response(): @pytest.fixture(scope="function", autouse=True) -def reset_singleton(): +def reset_tracing_config(): + Tracer._reset_config() yield - Tracer.clear_instance() def test_capture_lambda_handler(dummy_response): @@ -53,6 +53,7 @@ def handler(event, context): return dummy_response handler({}, {}) + monkeypatch.delenv("AWS_SAM_LOCAL") def test_tracer_metadata_disabled(dummy_response): @@ -114,29 +115,12 @@ def greeting(name, message): def test_tracer_reuse(): - # GIVEN tracer A, B and C were initialized - # WHEN tracer B explicitly reuses A instance + # GIVEN tracer A, B were initialized + # WHEN tracer B explicitly reuses A config # THEN tracer B attributes should be equal to tracer A - # and tracer C should use have defaults instead service_name = "booking" tracer_a = Tracer(disabled=True, service=service_name) - tracer_b = Tracer.instance() - tracer_c = Tracer() + tracer_b = Tracer() - assert id(tracer_a) == id(tracer_b) - assert id(tracer_a) != id(tracer_c) - assert id(tracer_b) != id(tracer_c) - - -def test_tracer_reuse_no_instance(): - # GIVEN tracer A and B instance were not registered - # WHEN instance is method is explicitly called - # THEN we initialize a new instance and register it - # so that tracer B receives a copy of the instance - # and only one __init__ is executed - tracer_a = Tracer.instance() - tracer_b = Tracer.instance() - - assert id(tracer_a) == id(tracer_b) - - Tracer.clear_instance() + assert id(tracer_a) != id(tracer_b) + assert tracer_a.__dict__.items() == tracer_b.__dict__.items() diff --git a/python/tests/unit/test_tracing.py b/python/tests/unit/test_tracing.py index ce50de5b285..a7b98389e33 100644 --- a/python/tests/unit/test_tracing.py +++ b/python/tests/unit/test_tracing.py @@ -40,6 +40,12 @@ def end_subsegment(self, *args, **kwargs): return XRayStub +@pytest.fixture(scope="function", autouse=True) +def reset_tracing_config(): + Tracer._reset_config() + yield + + def test_tracer_lambda_handler(mocker, dummy_response, xray_stub): put_metadata_mock = mocker.MagicMock() begin_subsegment_mock = mocker.MagicMock() @@ -130,26 +136,20 @@ def handler(event, context): 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') + +@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_a = Tracer(disabled=True) - + Tracer(disabled=True) assert patch_mock.call_count == 1 - tracer_b = Tracer(disabled=True, auto_patch=False) - tracer_b.patch() - assert patch_mock.call_count == 2 - -@mock.patch('aws_lambda_powertools.tracing.Tracer.patch') +@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_a = Tracer(disabled=True, auto_patch=False) - tracer_b = tracer_a.instance() - + Tracer(disabled=True, auto_patch=False) assert patch_mock.call_count == 0 From 55ce3a19b31f65d1596f707e06d59c735a9871b6 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 20 Apr 2020 15:57:00 +0100 Subject: [PATCH 34/36] improv: update example to reflect middleware feat --- .../middleware_factory/factory.py | 6 +++- python/example/hello_world/app.py | 33 +++++++++++++------ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/python/aws_lambda_powertools/middleware_factory/factory.py b/python/aws_lambda_powertools/middleware_factory/factory.py index d704425cce7..4dcab2adf33 100644 --- a/python/aws_lambda_powertools/middleware_factory/factory.py +++ b/python/aws_lambda_powertools/middleware_factory/factory.py @@ -76,12 +76,16 @@ def lambda_handler(event, context): **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 @@ -119,7 +123,7 @@ def wrapper(event, context): middleware = functools.partial(decorator, func, event, context, **kwargs) if trace_execution: tracer = Tracer(auto_patch=False) - tracer.create_subsegment(name=f"## middleware {decorator.__qualname__}") + tracer.create_subsegment(name=f"## {decorator.__qualname__}") response = middleware() tracer.end_subsegment() else: 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", "")}), } From fddc26bf593f40e8c8a899b600dbd5629933ea30 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 20 Apr 2020 16:26:14 +0100 Subject: [PATCH 35/36] fix: metric dimension in root blob --- python/aws_lambda_powertools/metrics/base.py | 1 + python/tests/functional/test_metrics.py | 2 ++ 2 files changed, 3 insertions(+) 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/tests/functional/test_metrics.py b/python/tests/functional/test_metrics.py index 408dc93bc8f..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): From 073ee4f2b19b43dddf096ede80e6032ddeb2a218 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Mon, 20 Apr 2020 16:42:21 +0100 Subject: [PATCH 36/36] chore: version bump --- python/HISTORY.md | 7 +++ python/poetry.lock | 125 ++++++++++++++++++++++-------------------- python/pyproject.toml | 2 +- 3 files changed, 74 insertions(+), 60 deletions(-) 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/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=[