From 59beea6f9d726d96ef101c015e522c1fca09015f Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 9 Jul 2021 21:15:59 +0200 Subject: [PATCH 1/7] docs: add mkdocstrings for api reference Signed-off-by: heitorlessa --- docs/api.md | 1 + mkdocs.yml | 6 +++- poetry.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 docs/api.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000000..e853e77a5a3 --- /dev/null +++ b/docs/api.md @@ -0,0 +1 @@ +::: aws_lambda_powertools.tracing.Tracer diff --git a/mkdocs.yml b/mkdocs.yml index 7ee0fd56236..298fae3e503 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ nav: - Homepage: index.md - Changelog: changelog.md - Roadmap: https://github.com/awslabs/aws-lambda-powertools-roadmap/projects/1" target="_blank - - API reference: api/" target="_blank + - API reference: api.md - Core utilities: - core/tracer.md - core/logger.md @@ -26,6 +26,7 @@ nav: - utilities/parser.md - utilities/idempotency.md + theme: name: material palette: @@ -73,6 +74,9 @@ copyright: Copyright © 2021 Amazon Web Services plugins: - git-revision-date - search + - mkdocstrings: + watch: + - aws_lambda_powertools extra_css: - stylesheets/extra.css diff --git a/poetry.lock b/poetry.lock index 9a6f1fdbc04..5681d544eca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.6.1,<2.0" + [[package]] name = "atomicwrites" version = "1.4.0" @@ -108,6 +119,14 @@ urllib3 = ">=1.25.4,<1.27" [package.extras] crt = ["awscrt (==0.11.24)"] +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "certifi" version = "2020.12.5" @@ -577,6 +596,18 @@ Markdown = ">=3.2.1" PyYAML = ">=3.10" tornado = ">=5.0" +[[package]] +name = "mkdocs-autorefs" +version = "0.2.1" +description = "Automatically link across pages in MkDocs." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +Markdown = ">=3.3,<4.0" +mkdocs = ">=1.1,<2.0" + [[package]] name = "mkdocs-git-revision-date-plugin" version = "0.3.1" @@ -616,6 +647,23 @@ python-versions = ">=3.5" [package.dependencies] mkdocs-material = ">=5.0.0" +[[package]] +name = "mkdocstrings" +version = "0.15.2" +description = "Automatic documentation from sources, for MkDocs." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +Jinja2 = ">=2.11.1,<4.0" +Markdown = ">=3.3,<4.0" +MarkupSafe = ">=1.1,<3.0" +mkdocs = ">=1.1.1,<2.0.0" +mkdocs-autorefs = ">=0.1,<0.3" +pymdown-extensions = ">=6.3,<9.0" +pytkdocs = ">=0.2.0,<0.12.0" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -843,6 +891,23 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "pytkdocs" +version = "0.11.1" +description = "Load Python objects documentation." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +astunparse = {version = ">=1.6.3,<2.0.0", markers = "python_version < \"3.9\""} +cached-property = {version = ">=1.5.2,<2.0.0", markers = "python_version < \"3.8\""} +dataclasses = {version = ">=0.7,<0.9", markers = "python_version == \"3.6\""} +typing-extensions = {version = ">=3.7.4.3,<4.0.0.0", markers = "python_version < \"3.8\""} + +[package.extras] +numpy-style = ["docstring_parser (>=0.7.3,<0.8.0)"] + [[package]] name = "pyyaml" version = "5.4.1" @@ -1066,13 +1131,17 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "c07aad70013171c8bb000d163f997efef709189584089f68d471f69eb8bb38c0" +content-hash = "d2e09af1d7a6481f5063548e4207e0791dbd015d12b1519a5c7e1f43ff96affc" [metadata.files] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +astunparse = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1100,6 +1169,10 @@ botocore = [ {file = "botocore-1.20.102-py2.py3-none-any.whl", hash = "sha256:bdf08a4f7f01ead00d386848f089c08270499711447569c18d0db60023619c06"}, {file = "botocore-1.20.102.tar.gz", hash = "sha256:2f57f7ceed1598d96cc497aeb45317db5d3b21a5aafea4732d0e561d0fc2a8fa"}, ] +cached-property = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, @@ -1338,6 +1411,10 @@ mkdocs = [ {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] +mkdocs-autorefs = [ + {file = "mkdocs-autorefs-0.2.1.tar.gz", hash = "sha256:b8156d653ed91356e71675ce1fa1186d2b2c2085050012522895c9aa98fca3e5"}, + {file = "mkdocs_autorefs-0.2.1-py3-none-any.whl", hash = "sha256:f301b983a34259df90b3fcf7edc234b5e6c7065bd578781e66fd90b8cfbe76be"}, +] mkdocs-git-revision-date-plugin = [ {file = "mkdocs-git-revision-date-plugin-0.3.1.tar.gz", hash = "sha256:4abaef720763a64c952bed6829dcc180f67c97c60dd73914e90715e05d1cfb23"}, {file = "mkdocs_git_revision_date_plugin-0.3.1-py3-none-any.whl", hash = "sha256:8ae50b45eb75d07b150a69726041860801615aae5f4adbd6b1cf4d51abaa03d5"}, @@ -1350,6 +1427,10 @@ mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, ] +mkdocstrings = [ + {file = "mkdocstrings-0.15.2-py3-none-any.whl", hash = "sha256:8d6cbe64c07ae66739010979ca01d49dd2f64d1a45009f089d217b9cd2a65e36"}, + {file = "mkdocstrings-0.15.2.tar.gz", hash = "sha256:c2fee9a3a644647c06eb2044fdfede1073adfd1a55bf6752005d3db10705fe73"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -1445,6 +1526,10 @@ python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] +pytkdocs = [ + {file = "pytkdocs-0.11.1-py3-none-any.whl", hash = "sha256:89ca4926d0acc266235beb24cb0b0591aa6bf7adedfae54bf9421d529d782c8d"}, + {file = "pytkdocs-0.11.1.tar.gz", hash = "sha256:1ec7e028fe8361acc1ce909ada4e6beabec28ef31e629618549109e1d58549f0"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, diff --git a/pyproject.toml b/pyproject.toml index f7743f2eba2..46a77681b64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ flake8-bugbear = "^21.3.2" mkdocs-material = "^7.1.9" mkdocs-git-revision-date-plugin = "^0.3.1" mike = "^0.6.0" +mkdocstrings = "^0.15.2" [tool.poetry.extras] From 0bd41423ecf21b0b8895ad67b997f326eab5bdab Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Fri, 9 Jul 2021 21:24:57 +0200 Subject: [PATCH 2/7] docs(tracer): use supported autogen docstring fmt --- aws_lambda_powertools/tracing/tracer.py | 486 +++++++++++------------- 1 file changed, 212 insertions(+), 274 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 47568802202..60b0a3fb0fe 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -5,7 +5,7 @@ import logging import numbers import os -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, Optional, Sequence, Union from ..shared import constants from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice @@ -19,119 +19,51 @@ aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) -class Tracer: - """Tracer using AWS-XRay to provide decorators with known defaults for Lambda functions - - 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. Patching is - automatically disabled when running locally via SAM CLI or by any other means. \n - Ref: https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/thirdparty.html - - Tracer keeps a copy of its configuration as it can be instantiated more than once. This - is useful when you are using your own middlewares and want to utilize an existing Tracer. - Make sure to set `auto_patch=False` in subsequent Tracer instances to avoid double patching. - - Environment variables - --------------------- - POWERTOOLS_TRACE_DISABLED : str - disable tracer (e.g. `"true", "True", "TRUE"`) - POWERTOOLS_SERVICE_NAME : str - service name - POWERTOOLS_TRACER_CAPTURE_RESPONSE : str - disable auto-capture response as metadata (e.g. `"true", "True", "TRUE"`) - POWERTOOLS_TRACER_CAPTURE_ERROR : str - disable auto-capture error as metadata (e.g. `"true", "True", "TRUE"`) - - Parameters - ---------- - service: str - Service name that will be appended in all tracing metadata - auto_patch: bool - Patch existing imported modules during initialization, by default True - disabled: bool - Flag to explicitly disable tracing, useful when running/testing locally - `Env POWERTOOLS_TRACE_DISABLED="true"` - patch_modules: Tuple[str] - Tuple of modules supported by tracing provider to patch, by default all modules are patched - provider: BaseProvider - Tracing provider, by default it is aws_xray_sdk.core.xray_recorder - - Returns - ------- - Tracer - Tracer instance with imported modules patched - - Example - ------- - **A Lambda function using Tracer** +LambdaHandlerT = Union[Callable[[Dict, Any], Any], Callable[[Dict, Any, Optional[Dict]], Any]] - from aws_lambda_powertools import Tracer - tracer = Tracer(service="greeting") - @tracer.capture_method - def greeting(name: str) -> Dict: - return { - "name": name - } +class Tracer: + """Tracer provides opinionated decorators to trace Lambda functions with AWS X-Ray # noqa E501 - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - print("Received event from Lambda...") - response = greeting(name="Heitor") - return response + By default, it patches all [available libraries supported by X-Ray SDK](https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/thirdparty.html). - **Booking Lambda function using Tracer that adds additional annotation/metadata** + When running locally, it disables itself whether it's running via SAM CLI or Chalice. - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + Note: Reusing Tracer across the codebase. + Tracer keeps a copy of its configuration after the first initialization and reuses it across instances. - @tracer.capture_method - def confirm_booking(booking_id: str) -> Dict: - resp = add_confirmation(booking_id) + Additional instances can override configuration via the constructor. - tracer.put_annotation("BookingConfirmation", resp["requestId"]) - tracer.put_metadata("Booking confirmation", resp) + ## Environment variables - return resp + * `POWERTOOLS_TRACE_DISABLED`: disable tracer (`true`) + * `POWERTOOLS_SERVICE_NAME`: service name, (`payment`) + * `POWERTOOLS_TRACER_CAPTURE_RESPONSE`: disable auto-capture response as metadata, (`true`) + * `POWERTOOLS_TRACER_CAPTURE_ERROR`: disable auto-capture error as metadata, (`true`) - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - print("Received event from Lambda...") - booking_id = event.get("booking_id") - response = confirm_booking(booking_id=booking_id) - return response + ## Examples - **A Lambda function using service name via POWERTOOLS_SERVICE_NAME** + ### Reuse an existing instance of Tracer across the codebase - export POWERTOOLS_SERVICE_NAME="booking" - from aws_lambda_powertools import Tracer - tracer = Tracer() + ```python + # lambda_handler.py + from aws_lambda_powertools import Tracer - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - print("Received event from Lambda...") - response = greeting(name="Lessa") - return response + tracer = Tracer(service="booking") - **Reuse an existing instance of Tracer anywhere in the code** + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + ... - # lambda_handler.py - from aws_lambda_powertools import Tracer - tracer = Tracer() + # utils.py + from aws_lambda_powertools import Tracer - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - ... + tracer = Tracer(service="booking") + ... + ``` - # utils.py - from aws_lambda_powertools import Tracer - tracer = Tracer() - ... + ## Limitations - Limitations - ----------- * Async handler not supported """ @@ -146,19 +78,30 @@ def handler(event: dict, context: Any) -> Dict: def __init__( self, - service: str = None, - disabled: bool = None, - auto_patch: bool = None, - patch_modules: Optional[Tuple[str]] = None, - provider: BaseProvider = None, + service: Optional[str] = None, + disabled: Optional[bool] = None, + auto_patch: Optional[bool] = None, + patch_modules: Optional[Sequence[str]] = None, + provider: Optional[BaseProvider] = None, ): + """Tracer constructor + + Parameters: + + service (str): Service name that will be appended in all tracing metadata + auto_patch (bool): Patch existing imported modules during initialization, by default True + disabled (bool): Flag to explicitly disable tracing, useful when running/testing locally + patch_modules (Sequence[str]): List of modules supported by tracing provider to patch + provider (BaseProvider): Tracing provider, by default it is `aws_xray_sdk.core.xray_recorder` + + """ self.__build_config( service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider ) self.provider: BaseProvider = self._config["provider"] - self.disabled = self._config["disabled"] - self.service = self._config["service"] - self.auto_patch = self._config["auto_patch"] + self.disabled: bool = self._config["disabled"] + self.service: str = self._config["service"] + self.auto_patch: Optional[bool] = self._config["auto_patch"] if self.disabled: self._disable_tracer_provider() @@ -174,19 +117,32 @@ def __init__( def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]): """Adds annotation to existing segment or subsegment - Parameters - ---------- - key : str - Annotation key - value : Union[str, numbers.Number, bool] - Value for annotation + Parameters: + key (str): Annotation key + key (Union[str, numbers.Number, bool): Value for annotation - Example - ------- - Custom annotation for a pseudo service named payment + ## Example - tracer = Tracer(service="payment") - tracer.put_annotation("PaymentStatus", "CONFIRMED") + ```python + from aws_lambda_powertools import Tracer + + tracer = Tracer(service="booking") + + @tracer.capture_method + def confirm_booking(booking_id: str) -> Dict: + resp = add_confirmation(booking_id) + tracer.put_annotation("BookingConfirmation", resp["requestId"]) + + return resp + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id", "") + tracer.put_annotation("BookingId", booking_id) + response = confirm_booking(booking_id=booking_id) + + return response + ``` """ if self.disabled: logger.debug("Tracing has been disabled, aborting put_annotation") @@ -198,22 +154,33 @@ def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]): def put_metadata(self, key: str, value: Any, namespace: str = None): """Adds metadata to existing segment or subsegment - Parameters - ---------- - key : str - Metadata key - value : any - Value for metadata - namespace : str, optional - Namespace that metadata will lie under, by default None - - Example - ------- - Custom metadata for a pseudo service named payment + Parameters: + + key (str): Metadata key + value (any): Value for metadata + namespace (str): Namespace that metadata will lie under, by default None + + ## Example + + ```python + from aws_lambda_powertools import Tracer + + tracer = Tracer(service="booking") + + @tracer.capture_method + def confirm_booking(booking_id: str) -> Dict: + resp = add_confirmation(booking_id) + tracer.put_metadata("Booking request metadata", resp["Metadata"]) - tracer = Tracer(service="payment") - response = collect_payment() - tracer.put_metadata("Payment collection", response) + return resp["booking"] + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") + response = confirm_booking(booking_id=booking_id) + + return response + ``` """ if self.disabled: logger.debug("Tracing has been disabled, aborting put_metadata") @@ -223,15 +190,14 @@ def put_metadata(self, key: str, value: Any, 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 patch(self, modules: Tuple[str] = None): + def patch(self, modules: Sequence[str] = None): """Patch modules for instrumentation. Patches all supported modules by default if none are given. - Parameters - ---------- - modules : Tuple[str] - List of modules to be patched, optional by default + Parameters: + + modules (Sequence[str]): List of modules to be patched, optional by default """ if self.disabled: logger.debug("Tracing has been disabled, aborting patch") @@ -244,44 +210,30 @@ def patch(self, modules: Tuple[str] = None): def capture_lambda_handler( self, - lambda_handler: Union[Callable[[Dict, Any], Any], Callable[[Dict, Any, Optional[Dict]], Any]] = None, + lambda_handler: LambdaHandlerT = None, capture_response: Optional[bool] = None, capture_error: Optional[bool] = None, ): """Decorator to create subsegment for lambda handlers - As Lambda follows (event, context) signature we can remove some of the boilerplate - and also capture any exception any Lambda function throws or its response as metadata - - Parameters - ---------- - lambda_handler : Callable - Method to annotate on - capture_response : bool, optional - Instructs tracer to not include handler's response as metadata - capture_error : bool, optional - Instructs tracer to not include handler's error as metadata, by default True + By default, it automatically captures Lambda Handler's response or exception as metadata. - Example - ------- - **Lambda function using capture_lambda_handler decorator** + Parameters: + lambda_handler (LambdaHandlerT): Lambda function's handler + capture_response (bool): Instructs tracer to not include handler's response as metadata + capture_error (bool): Instructs tracer to not include handler's error as metadata, by default `True` - tracer = Tracer(service="payment") - @tracer.capture_lambda_handler - def handler(event, context): - ... + ## Example - **Preventing Tracer to log response as metadata** + ```python + from aws_lambda_powertools import Tracer - tracer = Tracer(service="payment") - @tracer.capture_lambda_handler(capture_response=False) - def handler(event, context): - ... + tracer = Tracer(service="booking") - Raises - ------ - err - Exception raised by method + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + ... + ``` """ # If handler is None we've been called with parameters # Return a partial function with args filled @@ -334,151 +286,137 @@ def capture_method( ): """Decorator to create subsegment for arbitrary functions - It also captures both response and exceptions as metadata - and creates a subsegment named `## ` - - When running [async functions concurrently](https://docs.python.org/3/library/asyncio-task.html#id6), - methods may impact each others subsegment, and can trigger - and AlreadyEndedException from X-Ray due to async nature. - - For this use case, either use `capture_method` only where - `async.gather` is called, or use `in_subsegment_async` - context manager via our escape hatch mechanism - See examples. - - Parameters - ---------- - method : Callable - Method to annotate on - capture_response : bool, optional - Instructs tracer to not include method's response as metadata - capture_error : bool, optional - Instructs tracer to not include handler's error as metadata, by default True - - Example - ------- - **Custom function using capture_method decorator** - - tracer = Tracer(service="payment") - @tracer.capture_method - def some_function() + By default, it automatically captures response or exception as metadata in a subsegment named `## `. # noqa E501 - **Custom async method using capture_method decorator** + Warning: Running [async functions concurrently](https://docs.python.org/3/library/asyncio-task.html#id6) + Methods may impact each others subsegment and can trigger `AlreadyEndedException` from X-Ray due to async nature. - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + For this use case, either use `capture_method` only where`async.gather` is called, + or use `in_subsegment_async` context manager via our escape hatch mechanism - See examples. - @tracer.capture_method - async def confirm_booking(booking_id: str) -> Dict: - resp = call_to_booking_service() + Parameters: + method (Callable): Method to annotate on + capture_response (bool): Instructs tracer to not include method's response as metadata, by default `True` + capture_error (bool): Instructs tracer to not include handler's error as metadata, by default `True` - tracer.put_annotation("BookingConfirmation", resp["requestId"]) - tracer.put_metadata("Booking confirmation", resp) + ## Example - return resp + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="greeting") - def lambda_handler(event: dict, context: Any) -> Dict: - booking_id = event.get("booking_id") - asyncio.run(confirm_booking(booking_id=booking_id)) + @tracer.capture_method + def greeting(name: str) -> Dict: + return { "name": name } - **Custom generator function using capture_method decorator** + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + response = greeting(name="Heitor") - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + return response + ``` - @tracer.capture_method - def bookings_generator(booking_id): - resp = call_to_booking_service() - yield resp[0] - yield resp[1] + **Tracing async method** - def lambda_handler(event: dict, context: Any) -> Dict: - gen = bookings_generator(booking_id=booking_id) - result = list(gen) + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - **Custom generator context manager using capture_method decorator** + @tracer.capture_method + async def confirm_booking(booking_id: str) -> Dict: + resp = call_to_booking_service() - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + tracer.put_annotation("BookingConfirmation", resp["requestId"]) + tracer.put_metadata("Booking confirmation", resp) - @tracer.capture_method - @contextlib.contextmanager - def booking_actions(booking_id): - resp = call_to_booking_service() - yield "example result" - cleanup_stuff() + return resp - def lambda_handler(event: dict, context: Any) -> Dict: - booking_id = event.get("booking_id") + def lambda_handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") + asyncio.run(confirm_booking(booking_id=booking_id)) + ``` - with booking_actions(booking_id=booking_id) as booking: - result = booking + **Tracing generator function** - **Tracing nested async calls** + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + @tracer.capture_method + def bookings_generator(booking_id): + resp = call_to_booking_service() + yield resp[0] + yield resp[1] - @tracer.capture_method - async def get_identity(): - ... + def lambda_handler(event: dict, context: Any) -> Dict: + gen = bookings_generator(booking_id=booking_id) + result = list(gen) + ``` - @tracer.capture_method - async def long_async_call(): - ... + **Tracing generator context manager** - @tracer.capture_method - async def async_tasks(): - await get_identity() - ret = await long_async_call() + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - return { "task": "done", **ret } + @tracer.capture_method + @contextlib.contextmanager + def booking_actions(booking_id): + resp = call_to_booking_service() + yield "example result" + cleanup_stuff() - **Safely tracing concurrent async calls with decorator** + def lambda_handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") - This may not needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) + with booking_actions(booking_id=booking_id) as booking: + result = booking + ``` - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + **Tracing nested async calls** - async def get_identity(): - async with aioboto3.client("sts") as sts: - account = await sts.get_caller_identity() - return account + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - async def long_async_call(): - ... + @tracer.capture_method + async def get_identity(): + ... - @tracer.capture_method - async def async_tasks(): - _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) + @tracer.capture_method + async def long_async_call(): + ... - return { "task": "done", **ret } + @tracer.capture_method + async def async_tasks(): + await get_identity() + ret = await long_async_call() - **Safely tracing each concurrent async calls with escape hatch** + return { "task": "done", **ret } + ``` - This may not needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) + **Safely tracing concurrent async calls with decorator** - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + > This may not be needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) - async def get_identity(): - async tracer.provider.in_subsegment_async("## get_identity"): - ... + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - async def long_async_call(): - async tracer.provider.in_subsegment_async("## long_async_call"): - ... + async def get_identity(): + async with aioboto3.client("sts") as sts: + account = await sts.get_caller_identity() + return account - @tracer.capture_method - async def async_tasks(): - _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) + async def long_async_call(): + ... - return { "task": "done", **ret } + @tracer.capture_method + async def async_tasks(): + _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) - Raises - ------ - err - Exception raised by method + return { "task": "done", **ret } + ``` """ # If method is None we've been called with parameters # Return a partial function with args filled @@ -717,7 +655,7 @@ def __build_config( service: str = None, disabled: bool = None, auto_patch: bool = None, - patch_modules: Union[List, Tuple] = None, + patch_modules: Sequence[str] = None, provider: BaseProvider = None, ): """Populates Tracer config for new and existing initializations""" From 07e1ed45ce982c0a52c6da34f58f1f6b45c0e30d Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 17 Jul 2021 13:14:47 +0200 Subject: [PATCH 3/7] feat: use undocumented numpy support in mkdocstrings --- mkdocs.yml | 4 ++++ poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 298fae3e503..6c3048f0d46 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -75,6 +75,10 @@ plugins: - git-revision-date - search - mkdocstrings: + handlers: + python: + selection: + docstring_style: numpy watch: - aws_lambda_powertools diff --git a/poetry.lock b/poetry.lock index 5681d544eca..7c29d803b01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -200,6 +200,14 @@ idna = ["idna (>=2.1)"] curio = ["curio (>=1.2)", "sniffio (>=1.1)"] trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] +[[package]] +name = "docstring-parser" +version = "0.7.3" +description = "" +category = "dev" +optional = false +python-versions = "~=3.5" + [[package]] name = "email-validator" version = "1.1.3" @@ -903,6 +911,7 @@ python-versions = ">=3.6.1,<4.0.0" astunparse = {version = ">=1.6.3,<2.0.0", markers = "python_version < \"3.9\""} cached-property = {version = ">=1.5.2,<2.0.0", markers = "python_version < \"3.8\""} dataclasses = {version = ">=0.7,<0.9", markers = "python_version == \"3.6\""} +docstring_parser = {version = ">=0.7.3,<0.8.0", optional = true, markers = "extra == \"numpy-style\""} typing-extensions = {version = ">=3.7.4.3,<4.0.0.0", markers = "python_version < \"3.8\""} [package.extras] @@ -1131,7 +1140,7 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.1" -content-hash = "d2e09af1d7a6481f5063548e4207e0791dbd015d12b1519a5c7e1f43ff96affc" +content-hash = "1873bfb09f928c38d678ecee00ab3d945fa758169e7d9629e6ac0cd53863eb57" [metadata.files] appdirs = [ @@ -1251,6 +1260,9 @@ dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] +docstring-parser = [ + {file = "docstring_parser-0.7.3.tar.gz", hash = "sha256:cde5fbf8b846433dfbde1e0f96b7f909336a634d5df34a38cb75050c7346734a"}, +] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, diff --git a/pyproject.toml b/pyproject.toml index 46a77681b64..0c2719b1b72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ mkdocs-material = "^7.1.9" mkdocs-git-revision-date-plugin = "^0.3.1" mike = "^0.6.0" mkdocstrings = "^0.15.2" +pytkdocs = {extras = ["numpy-style"], version = "^0.11.1"} [tool.poetry.extras] From f385310e69ec0a9ee455fe4488b23e3aecfd5864 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 17 Jul 2021 19:41:36 +0200 Subject: [PATCH 4/7] chore: remove awaitable generic ref --- aws_lambda_powertools/tracing/tracer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 5709b1956c2..e8d56192eed 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -5,7 +5,7 @@ import logging import numbers import os -from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, TypeVar, Union, cast, overload +from typing import Any, Callable, Dict, Optional, Sequence, TypeVar, Union, cast, overload from ..shared import constants from ..shared.functions import resolve_env_var_choice, resolve_truthy_env_var_choice @@ -19,7 +19,6 @@ aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) AnyCallableT = TypeVar("AnyCallableT", bound=Callable[..., Any]) # noqa: VNE001 -AnyAwaitableT = TypeVar("AnyAwaitableT", bound=Awaitable) class Tracer: From 2f10d5315b8b4d6c987f3e49cf0ca66d46559f61 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 17 Jul 2021 20:37:35 +0200 Subject: [PATCH 5/7] docs(tracer): use numpy autogen docstring fmt --- aws_lambda_powertools/tracing/tracer.py | 454 +++++++++++------------- 1 file changed, 211 insertions(+), 243 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index e8d56192eed..d8c52b06319 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -22,119 +22,48 @@ class Tracer: - """Tracer using AWS-XRay to provide decorators with known defaults for Lambda functions + """Tracer provides opinionated decorators to trace Lambda functions with AWS X-Ray - 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](https://amzn.to/36Jkkyo). - 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 + When running locally, it disables itself whether it's running via SAM CLI or Chalice. - 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. + !!! note "Reusing Tracer across the codebase" + Tracer keeps a copy of its configuration after the first initialization and reuses it across instances. + + Additional instances can override configuration via the constructor. Environment variables --------------------- - POWERTOOLS_TRACE_DISABLED : str - disable tracer (e.g. `"true", "True", "TRUE"`) - POWERTOOLS_SERVICE_NAME : str - service name - POWERTOOLS_TRACER_CAPTURE_RESPONSE : str - disable auto-capture response as metadata (e.g. `"true", "True", "TRUE"`) - POWERTOOLS_TRACER_CAPTURE_ERROR : str - disable auto-capture error as metadata (e.g. `"true", "True", "TRUE"`) - - Parameters - ---------- - service: str - Service name that will be appended in all tracing metadata - auto_patch: bool - Patch existing imported modules during initialization, by default True - disabled: bool - Flag to explicitly disable tracing, useful when running/testing locally - `Env POWERTOOLS_TRACE_DISABLED="true"` - patch_modules: Optional[Sequence[str]] - Tuple of modules supported by tracing provider to patch, by default all modules are patched - provider: BaseProvider - Tracing provider, by default it is aws_xray_sdk.core.xray_recorder - - Returns - ------- - Tracer - Tracer instance with imported modules patched + + * `POWERTOOLS_TRACE_DISABLED`: disable tracer, default `true` + * `POWERTOOLS_SERVICE_NAME`: service name, default `payment` + * `POWERTOOLS_TRACER_CAPTURE_RESPONSE`: disable auto-capture response as metadata, default `true` + * `POWERTOOLS_TRACER_CAPTURE_ERROR`: disable auto-capture error as metadata, default `true` Example ------- - **A Lambda function using Tracer** - - from aws_lambda_powertools import Tracer - tracer = Tracer(service="greeting") - - @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 - - **Booking Lambda function using Tracer that adds additional annotation/metadata** - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + **Reuse an existing instance of Tracer across the codebase** - @tracer.capture_method - def confirm_booking(booking_id: str) -> Dict: - resp = add_confirmation(booking_id) + ```python + # lambda_handler.py + from aws_lambda_powertools import Tracer - tracer.put_annotation("BookingConfirmation", resp["requestId"]) - tracer.put_metadata("Booking confirmation", resp) + tracer = Tracer(service="booking") - return resp + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: ... - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - print("Received event from Lambda...") - booking_id = event.get("booking_id") - response = confirm_booking(booking_id=booking_id) - return response - - **A Lambda function using service name via POWERTOOLS_SERVICE_NAME** - - export POWERTOOLS_SERVICE_NAME="booking" - from aws_lambda_powertools import Tracer - tracer = Tracer() + # utils.py + from aws_lambda_powertools import Tracer - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - print("Received event from Lambda...") - response = greeting(name="Lessa") - return response + tracer = Tracer(service="booking") + ``` - **Reuse an existing instance of Tracer anywhere in the code** + ## Limitations - # lambda_handler.py - from aws_lambda_powertools import Tracer - tracer = Tracer() - - @tracer.capture_lambda_handler - def handler(event: dict, context: Any) -> Dict: - ... - - # utils.py - from aws_lambda_powertools import Tracer - tracer = Tracer() - ... - - Limitations - ----------- - * Async handler not supported + * Async Lambda handler not supported """ _default_config: Dict[str, Any] = { @@ -154,6 +83,21 @@ def __init__( patch_modules: Optional[Sequence[str]] = None, provider: Optional[BaseProvider] = None, ): + """Tracer constructor + + Parameters + ---------- + service: str + Service name to be appended across tracing metadata + auto_patch: bool + Patch existing imported modules during initialization, by default True + disabled: bool + Flag to explicitly disable tracing, useful when running/testing locally + patch_modules: Optional[Sequence[str]] + List of supported modules by the tracing provider, by default all modules are patched + provider: BaseProvider + Tracing provider, by default `aws_xray_sdk.core.xray_recorder` + """ self.__build_config( service=service, disabled=disabled, auto_patch=auto_patch, patch_modules=patch_modules, provider=provider ) @@ -185,10 +129,27 @@ def put_annotation(self, key: str, value: Union[str, numbers.Number, bool]): Example ------- - Custom annotation for a pseudo service named payment - tracer = Tracer(service="payment") - tracer.put_annotation("PaymentStatus", "CONFIRMED") + ```python + from aws_lambda_powertools import Tracer + + tracer = Tracer(service="booking") + + @tracer.capture_method + def confirm_booking(booking_id: str) -> Dict: + resp = add_confirmation(booking_id) + tracer.put_annotation("BookingConfirmation", resp["requestId"]) + + return resp + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id", "") + tracer.put_annotation("BookingId", booking_id) + response = confirm_booking(booking_id=booking_id) + + return response + ``` """ if self.disabled: logger.debug("Tracing has been disabled, aborting put_annotation") @@ -206,16 +167,31 @@ def put_metadata(self, key: str, value: Any, namespace: Optional[str] = None): Metadata key value : any Value for metadata - namespace : str, optional - Namespace that metadata will lie under, by default None + namespace : Optional[str] + Namespace container to add tracing metadata Example ------- - Custom metadata for a pseudo service named payment - tracer = Tracer(service="payment") - response = collect_payment() - tracer.put_metadata("Payment collection", response) + ```python + from aws_lambda_powertools import Tracer + + tracer = Tracer(service="booking") + + @tracer.capture_method + def confirm_booking(booking_id: str) -> Dict: + resp = add_confirmation(booking_id) + tracer.put_metadata("Booking request metadata", resp["Metadata"]) + + return resp["booking"] + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") + response = confirm_booking(booking_id=booking_id) + + return response + ``` """ if self.disabled: logger.debug("Tracing has been disabled, aborting put_metadata") @@ -226,14 +202,14 @@ def put_metadata(self, key: str, value: Any, namespace: Optional[str] = None): self.provider.put_metadata(key=key, value=value, namespace=namespace) def patch(self, modules: Optional[Sequence[str]] = None): - """Patch modules for instrumentation. + """Patch modules for instrumentation Patches all supported modules by default if none are given. Parameters ---------- modules : Optional[Sequence[str]] - List of modules to be patched, optional by default + List of modules to patch """ if self.disabled: logger.debug("Tracing has been disabled, aborting patch") @@ -250,40 +226,41 @@ def capture_lambda_handler( capture_response: Optional[bool] = None, capture_error: Optional[bool] = None, ): - """Decorator to create subsegment for lambda handlers + """Decorator to create subsegment for Lambda handlers - As Lambda follows (event, context) signature we can remove some of the boilerplate - and also capture any exception any Lambda function throws or its response as metadata + It automatically captures Lambda Handler's response or exception as metadata. Parameters ---------- lambda_handler : Callable - Method to annotate on - capture_response : bool, optional - Instructs tracer to not include handler's response as metadata - capture_error : bool, optional - Instructs tracer to not include handler's error as metadata, by default True + Lambda handler function + capture_response : Optional[bool] + Whether to capture handler's response as metadata, by default `True` + capture_error : Optional[bool] + Whether to capture handler's error as metadata, by default `True` Example ------- - **Lambda function using capture_lambda_handler decorator** - tracer = Tracer(service="payment") - @tracer.capture_lambda_handler - def handler(event, context): - ... + ```python + from aws_lambda_powertools import Tracer + + tracer = Tracer(service="booking") + + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: ... + ``` **Preventing Tracer to log response as metadata** - tracer = Tracer(service="payment") - @tracer.capture_lambda_handler(capture_response=False) - def handler(event, context): - ... + ```python + tracer = Tracer(service="payment") + + @tracer.capture_lambda_handler(capture_response=False) - Raises - ------ - err - Exception raised by method + def handler(event, context): + return response_larger_than_64K_or_sensitive_data + ``` """ # If handler is None we've been called with parameters # Return a partial function with args filled @@ -353,151 +330,142 @@ def capture_method( ) -> AnyCallableT: """Decorator to create subsegment for arbitrary functions - It also captures both response and exceptions as metadata - and creates a subsegment named `## ` + It automatically captures response or exception as metadata. - When running [async functions concurrently](https://docs.python.org/3/library/asyncio-task.html#id6), - methods may impact each others subsegment, and can trigger - and AlreadyEndedException from X-Ray due to async nature. + !!! warning "Running [async functions concurrently](https://docs.python.org/3/library/asyncio-task.html#id6)" + Methods may impact each others subsegment and can trigger X-Ray `AlreadyEndedException` due to async nature. - For this use case, either use `capture_method` only where - `async.gather` is called, or use `in_subsegment_async` - context manager via our escape hatch mechanism - See examples. + For this use case, either use `capture_method` only where`async.gather` is called, + or use `in_subsegment_async` context manager via our escape hatch mechanism - See examples. Parameters ---------- method : Callable - Method to annotate on - capture_response : bool, optional - Instructs tracer to not include method's response as metadata - capture_error : bool, optional - Instructs tracer to not include handler's error as metadata, by default True + Any synchronous or asynchronous function + capture_response : Optional[bool] + Whether to capture function's response as metadata, by default `True` + capture_error : Optional[bool] + Whether to capture function's error as metadata, by default `True` Example ------- - **Custom function using capture_method decorator** - - tracer = Tracer(service="payment") - @tracer.capture_method - def some_function() - - **Custom async method using capture_method decorator** - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") - - @tracer.capture_method - async def confirm_booking(booking_id: str) -> Dict: - resp = call_to_booking_service() - - tracer.put_annotation("BookingConfirmation", resp["requestId"]) - tracer.put_metadata("Booking confirmation", resp) - - return resp + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="greeting") - def lambda_handler(event: dict, context: Any) -> Dict: - booking_id = event.get("booking_id") - asyncio.run(confirm_booking(booking_id=booking_id)) + @tracer.capture_method + def greeting(name: str) -> Dict: + return { "name": name } - **Custom generator function using capture_method decorator** + @tracer.capture_lambda_handler + def handler(event: dict, context: Any) -> Dict: + response = greeting(name="Heitor") - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + return response + ``` - @tracer.capture_method - def bookings_generator(booking_id): - resp = call_to_booking_service() - yield resp[0] - yield resp[1] + **Tracing async method** - def lambda_handler(event: dict, context: Any) -> Dict: - gen = bookings_generator(booking_id=booking_id) - result = list(gen) + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - **Custom generator context manager using capture_method decorator** + @tracer.capture_method + async def confirm_booking(booking_id: str) -> Dict: + resp = call_to_booking_service() - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + tracer.put_annotation("BookingConfirmation", resp["requestId"]) + tracer.put_metadata("Booking confirmation", resp) - @tracer.capture_method - @contextlib.contextmanager - def booking_actions(booking_id): - resp = call_to_booking_service() - yield "example result" - cleanup_stuff() + return resp - def lambda_handler(event: dict, context: Any) -> Dict: - booking_id = event.get("booking_id") + def lambda_handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") + asyncio.run(confirm_booking(booking_id=booking_id)) + ``` - with booking_actions(booking_id=booking_id) as booking: - result = booking + **Tracing generators** - **Tracing nested async calls** + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + @tracer.capture_method + def bookings_generator(booking_id): + resp = call_to_booking_service() + yield resp[0] + yield resp[1] - @tracer.capture_method - async def get_identity(): - ... + def lambda_handler(event: dict, context: Any) -> Dict: + gen = bookings_generator(booking_id=booking_id) + result = list(gen) + ``` - @tracer.capture_method - async def long_async_call(): - ... + **Tracing generator context managers** - @tracer.capture_method - async def async_tasks(): - await get_identity() - ret = await long_async_call() + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - return { "task": "done", **ret } + @tracer.capture_method + @contextlib.contextmanager + def booking_actions(booking_id): + resp = call_to_booking_service() + yield "example result" + cleanup_stuff() - **Safely tracing concurrent async calls with decorator** + def lambda_handler(event: dict, context: Any) -> Dict: + booking_id = event.get("booking_id") - This may not needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) + with booking_actions(booking_id=booking_id) as booking: + result = booking + ``` - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + **Tracing nested async calls** - async def get_identity(): - async with aioboto3.client("sts") as sts: - account = await sts.get_caller_identity() - return account + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - async def long_async_call(): - ... + @tracer.capture_method + async def get_identity(): + ... - @tracer.capture_method - async def async_tasks(): - _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) + @tracer.capture_method + async def long_async_call(): + ... - return { "task": "done", **ret } + @tracer.capture_method + async def async_tasks(): + await get_identity() + ret = await long_async_call() - **Safely tracing each concurrent async calls with escape hatch** + return { "task": "done", **ret } + ``` - This may not needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) + **Safely tracing concurrent async calls with decorator** - from aws_lambda_powertools import Tracer - tracer = Tracer(service="booking") + > This may not be needed once [this bug is closed](https://github.com/aws/aws-xray-sdk-python/issues/164) - async def get_identity(): - async tracer.provider.in_subsegment_async("## get_identity"): - ... + ```python + from aws_lambda_powertools import Tracer + tracer = Tracer(service="booking") - async def long_async_call(): - async tracer.provider.in_subsegment_async("## long_async_call"): - ... + async def get_identity(): + async with aioboto3.client("sts") as sts: + account = await sts.get_caller_identity() + return account - @tracer.capture_method - async def async_tasks(): - _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) + async def long_async_call(): + ... - return { "task": "done", **ret } + @tracer.capture_method + async def async_tasks(): + _, ret = await asyncio.gather(get_identity(), long_async_call(), return_exceptions=True) - Raises - ------ - err - Exception raised by method + return { "task": "done", **ret } + ``` """ # If method is None we've been called with parameters # Return a partial function with args filled @@ -658,13 +626,13 @@ def _add_response_as_metadata( Parameters ---------- - method_name : str, optional - method name to add as metadata key, by default None - data : Any, optional - data to add as subsegment metadata, by default None - subsegment : BaseSegment, optional - existing subsegment to add metadata on, by default None - capture_response : bool, optional + method_name : str + method name to add as metadata key, by default `None` + data : Any + data to add as subsegment metadata, by default `None` + subsegment : BaseSegment + existing subsegment to add metadata on, by default `None` + capture_response : Optional[bool] Do not include response as metadata """ if data is None or not capture_response or subsegment is None: @@ -684,12 +652,12 @@ def _add_full_exception_as_metadata( Parameters ---------- method_name : str - method name to add as metadata key, by default None + method name to add as metadata key, by default `None` error : Exception - error to add as subsegment metadata, by default None + error to add as subsegment metadata, by default `None` subsegment : BaseSegment - existing subsegment to add metadata on, by default None - capture_error : bool, optional + existing subsegment to add metadata on, by default `None` + capture_error : Optional[bool] Do not include error as metadata, by default True """ if not capture_error: From 23d33bc9fdd39731e9beac41ea49f1c01cabf421 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 17 Jul 2021 20:41:19 +0200 Subject: [PATCH 6/7] fix(mypy): re-add lazy mod and capture flags --- aws_lambda_powertools/tracing/tracer.py | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index d8c52b06319..0eced4c04c9 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) aws_xray_sdk = LazyLoader(constants.XRAY_SDK_MODULE, globals(), constants.XRAY_SDK_MODULE) -aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) +aws_xray_sdk.core = LazyLoader(constants.XRAY_SDK_CORE_MODULE, globals(), constants.XRAY_SDK_CORE_MODULE) # type: ignore # noqa: E501 AnyCallableT = TypeVar("AnyCallableT", bound=Callable[..., Any]) # noqa: VNE001 @@ -506,9 +506,9 @@ async def async_tasks(): def _decorate_async_function( self, method: Callable, - capture_response: Optional[Union[bool, str]] = None, - capture_error: Optional[Union[bool, str]] = None, - method_name: Optional[str] = None, + capture_response: bool, + capture_error: bool, + method_name: str, ): @functools.wraps(method) async def decorate(*args, **kwargs): @@ -533,9 +533,9 @@ async def decorate(*args, **kwargs): def _decorate_generator_function( self, method: Callable, - capture_response: Optional[Union[bool, str]] = None, - capture_error: Optional[Union[bool, str]] = None, - method_name: Optional[str] = None, + capture_response: bool, + capture_error: bool, + method_name: str, ): @functools.wraps(method) def decorate(*args, **kwargs): @@ -560,9 +560,9 @@ def decorate(*args, **kwargs): def _decorate_generator_function_with_context_manager( self, method: Callable, - capture_response: Optional[Union[bool, str]] = None, - capture_error: Optional[Union[bool, str]] = None, - method_name: Optional[str] = None, + capture_response: bool, + capture_error: bool, + method_name: str, ): @functools.wraps(method) @contextlib.contextmanager @@ -588,9 +588,9 @@ def decorate(*args, **kwargs): def _decorate_sync_function( self, method: AnyCallableT, - capture_response: Optional[Union[bool, str]] = None, - capture_error: Optional[Union[bool, str]] = None, - method_name: Optional[str] = None, + capture_response: bool, + capture_error: bool, + method_name: str, ) -> AnyCallableT: @functools.wraps(method) def decorate(*args, **kwargs): From 1acd7d0058764ecfbb2a7b933403c7cdc7eb61ed Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Sat, 17 Jul 2021 20:52:22 +0200 Subject: [PATCH 7/7] docs(tracer): use top-level namespace to ease deep linking --- aws_lambda_powertools/tracing/extensions.py | 19 +++++++++++++++++++ docs/api.md | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/tracing/extensions.py b/aws_lambda_powertools/tracing/extensions.py index 6c641238c98..3eafae6a651 100644 --- a/aws_lambda_powertools/tracing/extensions.py +++ b/aws_lambda_powertools/tracing/extensions.py @@ -3,6 +3,25 @@ def aiohttp_trace_config(): It expects you to have aiohttp as a dependency. + Example + ------- + + ```python + import asyncio + import aiohttp + + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.tracing import aiohttp_trace_config + + tracer = Tracer() + + async def aiohttp_task(): + async with aiohttp.ClientSession(trace_configs=[aiohttp_trace_config()]) as session: + async with session.get("https://httpbin.org/json") as resp: + resp = await resp.json() + return resp + ``` + Returns ------- TraceConfig diff --git a/docs/api.md b/docs/api.md index e853e77a5a3..a58e2f4e4bd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1 +1,6 @@ -::: aws_lambda_powertools.tracing.Tracer +::: aws_lambda_powertools.tracing + selection: + filters: + - "!^_" + - "^__init__$" + - "!^base$"