Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: aws-powertools/powertools-lambda-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.1.3
Choose a base ref
...
head repository: aws-powertools/powertools-lambda-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.2.0
Choose a head ref
  • 2 commits
  • 7 files changed
  • 1 contributor

Commits on Aug 20, 2020

  1. feat: add support for tracing of generators using capture_method deco…

    …rator (#113)
    
    * feat: add support for tracing of generators using capture_method decorator
    
    * fix: ensure tracer config is reset before example test runs
    
    * chore: refactor to lower complexity of capture_method, update docstring
    
    * chore: update docs
    Tom McCarthy authored Aug 20, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    dbfbf55 View commit details
  2. chore: bump version to 1.2.0 (#119)

    Tom McCarthy authored Aug 20, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8621d4e View commit details
Showing with 252 additions and 36 deletions.
  1. +4 −0 CHANGELOG.md
  2. +116 −32 aws_lambda_powertools/tracing/tracer.py
  3. +25 −3 docs/content/core/tracer.mdx
  4. +8 −0 example/tests/test_handler.py
  5. +1 −1 pyproject.toml
  6. +35 −0 tests/functional/test_tracing.py
  7. +63 −0 tests/unit/test_tracing.py
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.0] - 2020-08-20
### Added
- **Tracer**: capture_method decorator now supports generator functions (including context managers)

## [1.1.3] - 2020-08-18
### Fixed
- **Logger**: Logs emitted twice, structured and unstructured, due to Lambda configuring the root handler
148 changes: 116 additions & 32 deletions aws_lambda_powertools/tracing/tracer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import copy
import functools
import inspect
@@ -320,6 +321,39 @@ def lambda_handler(event: dict, context: Any) -> Dict:
booking_id = event.get("booking_id")
asyncio.run(confirm_booking(booking_id=booking_id))
**Custom generator function using capture_method decorator**
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]
def lambda_handler(event: dict, context: Any) -> Dict:
gen = bookings_generator(booking_id=booking_id)
result = list(gen)
**Custom generator context manager using capture_method decorator**
from aws_lambda_powertools import Tracer
tracer = Tracer(service="booking")
@tracer.capture_method
@contextlib.contextmanager
def booking_actions(booking_id):
resp = call_to_booking_service()
yield "example result"
cleanup_stuff()
def lambda_handler(event: dict, context: Any) -> Dict:
booking_id = event.get("booking_id")
with booking_actions(booking_id=booking_id) as booking:
result = booking
**Tracing nested async calls**
from aws_lambda_powertools import Tracer
@@ -392,43 +426,93 @@ async def async_tasks():
err
Exception raised by method
"""
method_name = f"{method.__name__}"

if inspect.iscoroutinefunction(method):
decorate = self._decorate_async_function(method=method)
elif inspect.isgeneratorfunction(method):
decorate = self._decorate_generator_function(method=method)
elif hasattr(method, "__wrapped__") and inspect.isgeneratorfunction(method.__wrapped__):
decorate = self._decorate_generator_function_with_context_manager(method=method)
else:
decorate = self._decorate_sync_function(method=method)

@functools.wraps(method)
async def decorate(*args, **kwargs):
async with self.provider.in_subsegment_async(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
response = await method(*args, **kwargs)
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(
function_name=method_name, error=err, subsegment=subsegment
)
raise

return response
return decorate

else:
def _decorate_async_function(self, method: Callable = None):
method_name = f"{method.__name__}"

@functools.wraps(method)
async def decorate(*args, **kwargs):
async with self.provider.in_subsegment_async(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
response = await method(*args, **kwargs)
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
raise

@functools.wraps(method)
def decorate(*args, **kwargs):
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
response = method(*args, **kwargs)
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(
function_name=method_name, error=err, subsegment=subsegment
)
raise

return response
return response

return decorate

def _decorate_generator_function(self, method: Callable = None):
method_name = f"{method.__name__}"

@functools.wraps(method)
def decorate(*args, **kwargs):
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
result = yield from method(*args, **kwargs)
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
raise

return result

return decorate

def _decorate_generator_function_with_context_manager(self, method: Callable = None):
method_name = f"{method.__name__}"

@functools.wraps(method)
@contextlib.contextmanager
def decorate(*args, **kwargs):
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
with method(*args, **kwargs) as return_val:
result = return_val
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
raise

yield result

return decorate

def _decorate_sync_function(self, method: Callable = None):
method_name = f"{method.__name__}"

@functools.wraps(method)
def decorate(*args, **kwargs):
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
try:
logger.debug(f"Calling method: {method_name}")
response = method(*args, **kwargs)
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
except Exception as err:
logger.exception(f"Exception received from '{method_name}' method")
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
raise

return response

return decorate

28 changes: 25 additions & 3 deletions docs/content/core/tracer.mdx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github.
* Capture cold start as annotation, and responses as well as full exceptions as metadata
* Run functions locally with SAM CLI without code change to disable tracing
* Explicitly disable tracing via env var `POWERTOOLS_TRACE_DISABLED="true"`
* Support tracing async methods
* Support tracing async methods, generators, and context managers
* Auto patch supported modules, or a tuple of explicit modules supported by AWS X-Ray

## Initialization
@@ -111,16 +111,18 @@ def collect_payment(charge_id):
...
```

## Asynchronous functions
## Asynchronous and generator functions

<Note type="warning">
<strong>We do not support async Lambda handler</strong> - Lambda handler itself must be synchronous
</Note><br/>

You can trace an asynchronous function using the `capture_method`. The decorator will detect whether your function is asynchronous, and adapt its behaviour accordingly.
You can trace asynchronous functions and generator functions (including context managers) using `capture_method`.
The decorator will detect whether your function is asynchronous, a generator, or a context manager and adapt its behaviour accordingly.

```python:title=lambda_handler_with_async_code.py
import asyncio
import contextlib
from aws_lambda_powertools import Tracer
tracer = Tracer()

@@ -130,9 +132,29 @@ async def collect_payment():
...
# highlight-end

# highlight-start
@contextlib.contextmanager
@tracer.capture_method
def collect_payment_ctxman():
yield result
...
# highlight-end

# highlight-start
@tracer.capture_method
def collect_payment_gen():
yield result
...
# highlight-end

@tracer.capture_lambda_handler
def handler(evt, ctx): # highlight-line
asyncio.run(collect_payment())

with collect_payment_ctxman as result:
do_something_with(result)

another_result = list(collect_payment_gen())
```

## Tracing aiohttp requests
8 changes: 8 additions & 0 deletions example/tests/test_handler.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,14 @@

import pytest

from aws_lambda_powertools import Tracer


@pytest.fixture(scope="function", autouse=True)
def reset_tracing_config():
Tracer._reset_config()
yield


@pytest.fixture()
def env_vars(monkeypatch):
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "aws_lambda_powertools"
version = "1.1.3"
version = "1.2.0"
description = "Python utilities for AWS Lambda functions including but not limited to tracing, logging and custom metric"
authors = ["Amazon Web Services"]
classifiers=[
35 changes: 35 additions & 0 deletions tests/functional/test_tracing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import contextlib

import pytest

from aws_lambda_powertools import Tracer
@@ -150,3 +152,36 @@ def sums_values():
return func_1() + func_2()

sums_values()


def test_tracer_yield_with_capture():
# GIVEN tracer method decorator is used
tracer = Tracer(disabled=True)

# WHEN capture_method decorator is applied to a context manager
@tracer.capture_method
@contextlib.contextmanager
def yield_with_capture():
yield "testresult"

# Or WHEN capture_method decorator is applied to a generator function
@tracer.capture_method
def generator_func():
yield "testresult2"

@tracer.capture_lambda_handler
def handler(event, context):
result = []
with yield_with_capture() as yielded_value:
result.append(yielded_value)

gen = generator_func()

result.append(next(gen))

return result

# THEN no exception is thrown, and the functions properly return values
result = handler({}, {})
assert "testresult" in result
assert "testresult2" in result
63 changes: 63 additions & 0 deletions tests/unit/test_tracing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import sys
from typing import NamedTuple
from unittest import mock
@@ -348,3 +349,65 @@ async def greeting(name, message):
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
assert put_metadata_mock_args["key"] == "greeting error"
assert put_metadata_mock_args["namespace"] == "booking"


def test_tracer_yield_from_context_manager(mocker, provider_stub, in_subsegment_mock):
# GIVEN tracer is initialized
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
tracer = Tracer(provider=provider, service="booking")

# WHEN capture_method decorator is used on a context manager
@tracer.capture_method
@contextlib.contextmanager
def yield_with_capture():
yield "test result"

@tracer.capture_lambda_handler
def handler(event, context):
response = []
with yield_with_capture() as yielded_value:
response.append(yielded_value)

return response

result = handler({}, {})

# THEN we should have a subsegment named after the method name
# and add its response as trace metadata
handler_trace, yield_function_trace = in_subsegment_mock.in_subsegment.call_args_list

assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
assert in_subsegment_mock.in_subsegment.call_count == 2
assert handler_trace == mocker.call(name="## handler")
assert yield_function_trace == mocker.call(name="## yield_with_capture")
assert "test result" in result


def test_tracer_yield_from_generator(mocker, provider_stub, in_subsegment_mock):
# GIVEN tracer is initialized
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
tracer = Tracer(provider=provider, service="booking")

# WHEN capture_method decorator is used on a generator function
@tracer.capture_method
def generator_fn():
yield "test result"

@tracer.capture_lambda_handler
def handler(event, context):
gen = generator_fn()
response = list(gen)

return response

result = handler({}, {})

# THEN we should have a subsegment named after the method name
# and add its response as trace metadata
handler_trace, generator_fn_trace = in_subsegment_mock.in_subsegment.call_args_list

assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
assert in_subsegment_mock.in_subsegment.call_count == 2
assert handler_trace == mocker.call(name="## handler")
assert generator_fn_trace == mocker.call(name="## generator_fn")
assert "test result" in result