-
Notifications
You must be signed in to change notification settings - Fork 144
Support patching PynamoDB calls to DynamoDB #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .patch import patch | ||
|
||
__all__ = ['patch'] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import botocore.vendored.requests.sessions | ||
import json | ||
import wrapt | ||
|
||
from aws_xray_sdk.core import xray_recorder | ||
from aws_xray_sdk.core.models import http | ||
from aws_xray_sdk.ext.boto_utils import _extract_whitelisted_params | ||
|
||
|
||
def patch(): | ||
"""Patch PynamoDB so it generates subsegements when calling DynamoDB.""" | ||
if hasattr(botocore.vendored.requests.sessions, '_xray_enabled'): | ||
return | ||
setattr(botocore.vendored.requests.sessions, '_xray_enabled', True) | ||
|
||
wrapt.wrap_function_wrapper( | ||
'botocore.vendored.requests.sessions', | ||
'Session.send', | ||
_xray_traced_pynamodb, | ||
) | ||
|
||
|
||
def _xray_traced_pynamodb(wrapped, instance, args, kwargs): | ||
|
||
# Check if it's a request to DynamoDB and return otherwise. | ||
try: | ||
service = args[0].headers['X-Amz-Target'].decode('utf-8').split('_')[0] | ||
except KeyError: | ||
return wrapped(*args, **kwargs) | ||
if service.lower() != 'dynamodb': | ||
return wrapped(*args, **kwargs) | ||
|
||
return xray_recorder.record_subsegment( | ||
wrapped, instance, args, kwargs, | ||
name='dynamodb', | ||
namespace='aws', | ||
meta_processor=pynamodb_meta_processor, | ||
) | ||
|
||
|
||
def pynamodb_meta_processor(wrapped, instance, args, kwargs, return_value, | ||
exception, subsegment, stack): | ||
operation_name = args[0].headers['X-Amz-Target'].decode('utf-8').split('.')[1] | ||
region = args[0].url.split('.')[1] | ||
request_id = return_value.headers.get('x-amzn-RequestId') | ||
|
||
aws_meta = { | ||
'operation': operation_name, | ||
'request_id': request_id, | ||
'region': region | ||
} | ||
|
||
if exception: | ||
subsegment.add_error_flag() | ||
subsegment.add_exception(exception, stack, True) | ||
|
||
subsegment.put_http_meta(http.STATUS, return_value.status_code) | ||
|
||
_extract_whitelisted_params(subsegment.name, operation_name, | ||
aws_meta, [None, json.loads(args[0].body)], | ||
None, return_value.json()) | ||
|
||
subsegment.set_aws(aws_meta) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
aws\_xray\_sdk\.ext\.pynamodb package | ||
===================================== | ||
|
||
Submodules | ||
---------- | ||
|
||
aws\_xray\_sdk\.ext\.pynamodb\.patch module | ||
------------------------------------------- | ||
|
||
.. automodule:: aws_xray_sdk.ext.pynamodb.patch | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: | ||
|
||
|
||
Module contents | ||
--------------- | ||
|
||
.. automodule:: aws_xray_sdk.ext.pynamodb | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import botocore.session | ||
import pytest | ||
from botocore.exceptions import ClientError | ||
from pynamodb.attributes import UnicodeAttribute | ||
from pynamodb.exceptions import VerboseClientError | ||
from pynamodb.models import Model | ||
|
||
from aws_xray_sdk.core import patch | ||
from aws_xray_sdk.core import xray_recorder | ||
from aws_xray_sdk.core.context import Context | ||
|
||
patch(('pynamodb',)) | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def construct_ctx(): | ||
""" | ||
Clean up context storage on each test run and begin a segment | ||
so that later subsegment can be attached. After each test run | ||
it cleans up context storage again. | ||
""" | ||
xray_recorder.configure(service='test', sampling=False, context=Context()) | ||
xray_recorder.clear_trace_entities() | ||
xray_recorder.begin_segment('name') | ||
yield | ||
xray_recorder.clear_trace_entities() | ||
|
||
|
||
def test_exception(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a test to guard the behavior where vendored requests only emit subsegments when talking to DynamoDB? In other words after patching There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find it pretty difficult to write proper tests for the whole stuff, thanks to all the patching. I added a test now to check that only a single subsegment gets created when the pynamodb patch is enabled, but another AWS service is called. That single subsegment gets created, because the botocore patch is active in this case too. |
||
class SampleModel(Model): | ||
class Meta: | ||
region = 'us-west-2' | ||
table_name = 'mytable' | ||
|
||
sample_attribute = UnicodeAttribute(hash_key=True) | ||
|
||
try: | ||
SampleModel.describe_table() | ||
except VerboseClientError: | ||
pass | ||
|
||
subsegments = xray_recorder.current_segment().subsegments | ||
assert len(subsegments) == 1 | ||
subsegment = subsegments[0] | ||
assert subsegment.name == 'dynamodb' | ||
assert len(subsegment.subsegments) == 0 | ||
assert subsegment.error | ||
|
||
aws_meta = subsegment.aws | ||
assert aws_meta['region'] == 'us-west-2' | ||
assert aws_meta['operation'] == 'DescribeTable' | ||
assert aws_meta['table_name'] == 'mytable' | ||
|
||
|
||
def test_only_dynamodb_calls_are_traced(): | ||
"""Test only a single subsegment is created for other AWS services. | ||
|
||
As the pynamodb patch applies the botocore patch as well, we need | ||
to ensure that only one subsegment is created for all calls not | ||
made by PynamoDB. As PynamoDB calls botocore differently than the | ||
botocore patch expects we also just get a single subsegment per | ||
PynamoDB call. | ||
""" | ||
session = botocore.session.get_session() | ||
s3 = session.create_client('s3', region_name='us-west-2') | ||
try: | ||
s3.get_bucket_location(Bucket='mybucket') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're making external http calls anyway on the dynamo test case so I would suggest to not cause a Client error here. Exception raised from remote endpoint makes sure it reach further down to the botocore code path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid I don't get your point. I am doing an external call in this test as well and as the remote end returns an error (because I'm not authorized to access that bucket) I get an error back. I have to catch this error to be able to properly do the assertions afterwards. Am I missing something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad. I got confused by |
||
except ClientError: | ||
pass | ||
|
||
subsegments = xray_recorder.current_segment().subsegments | ||
assert len(subsegments) == 1 | ||
assert subsegments[0].name == 's3' | ||
assert len(subsegments[0].subsegments) == 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to capture
request_id
here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing out. Turned out I searched for it in the request instead of the response. Added it now.