Skip to content

Commit 681d14e

Browse files
authored
[aws]Flatten span tag names (#768)
* Add blacklist for params argument of function * Remove unnecessary import * Add blacklist to aiobotocore * Check params * Fix tests * Revert blacklisting of params * Standardize tags for botocore, boto, aiobotocore * Fix flake8 * Remove resource from tag name * Change bucket name for error * Add botocore tests to CI * Pull out function for adding span tags * Fix typo * Add flatten_dict * Use flatten_dict to cleanup code * Add note on source * Fix quotes * Fix deactivated test * Add comment * Double quotes * Fix flake, remove nose
1 parent 9ad9540 commit 681d14e

File tree

11 files changed

+287
-228
lines changed

11 files changed

+287
-228
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ jobs:
134134
- checkout
135135
- *restore_cache_step
136136
- run: tox -e 'boto_contrib-{py27,py34}-boto' --result-json /tmp/boto.1.results
137-
- run: tox -e 'botocore_contrib-{py27,py34}-botocore' --result-json /tmp/boto.2.results
137+
- run: tox -e 'botocore_contrib-{py27,py34,py35,py36}-botocore' --result-json /tmp/boto.2.results
138138
- persist_to_workspace:
139139
root: /tmp
140140
paths:

ddtrace/contrib/aiobotocore/patch.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,6 @@ def __aexit__(self, *args, **kwargs):
6666
return response
6767

6868

69-
def truncate_arg_value(value, max_len=1024):
70-
"""Truncate values which are bytes and greater than `max_len`.
71-
Useful for parameters like 'Body' in `put_object` operations.
72-
"""
73-
if isinstance(value, bytes) and len(value) > max_len:
74-
return b'...'
75-
76-
return value
77-
78-
7969
@asyncio.coroutine
8070
def _wrapped_api_call(original_func, instance, args, kwargs):
8171
pin = Pin.get_from(instance)
@@ -96,12 +86,7 @@ def _wrapped_api_call(original_func, instance, args, kwargs):
9686
operation = None
9787
span.resource = endpoint_name
9888

99-
# add args in TRACED_ARGS if exist to the span
100-
if not aws.is_blacklist(endpoint_name):
101-
for name, value in aws.unpacking_args(args, ARGS_NAME, TRACED_ARGS):
102-
if name == 'params':
103-
value = {k: truncate_arg_value(v) for k, v in value.items()}
104-
span.set_tag(name, (value))
89+
aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS)
10590

10691
region_name = deep_getattr(instance, 'meta.region_name')
10792

ddtrace/contrib/boto/patch.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,7 @@ def patched_query_request(original_func, instance, args, kwargs):
7878
else:
7979
span.resource = endpoint_name
8080

81-
# Adding the args in AWS_QUERY_TRACED_ARGS if exist to the span
82-
if not aws.is_blacklist(endpoint_name):
83-
for arg in aws.unpacking_args(
84-
args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS
85-
):
86-
span.set_tag(arg[0], arg[1])
81+
aws.add_span_arg_tags(span, endpoint_name, args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS)
8782

8883
# Obtaining region name
8984
region_name = _get_instance_region_name(instance)
@@ -127,19 +122,14 @@ def patched_auth_request(original_func, instance, args, kwargs):
127122
span_type=SPAN_TYPE,
128123
) as span:
129124

130-
# Adding the args in AWS_AUTH_TRACED_ARGS if exist to the span
131-
if not aws.is_blacklist(endpoint_name):
132-
for arg in aws.unpacking_args(
133-
args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS
134-
):
135-
span.set_tag(arg[0], arg[1])
136-
137125
if args:
138126
http_method = args[0]
139127
span.resource = "%s.%s" % (endpoint_name, http_method.lower())
140128
else:
141129
span.resource = endpoint_name
142130

131+
aws.add_span_arg_tags(span, endpoint_name, args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS)
132+
143133
# Obtaining region name
144134
region_name = _get_instance_region_name(instance)
145135

ddtrace/contrib/botocore/patch.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
# Original botocore client class
1616
_Botocore_client = botocore.client.BaseClient
1717

18-
SPAN_TYPE = "http"
19-
ARGS_NAME = ("action", "params", "path", "verb")
20-
TRACED_ARGS = ["params", "path", "verb"]
18+
SPAN_TYPE = 'http'
19+
ARGS_NAME = ('action', 'params', 'path', 'verb')
20+
TRACED_ARGS = ['params', 'path', 'verb']
2121

2222

2323
def patch():
@@ -55,10 +55,7 @@ def patched_api_call(original_func, instance, args, kwargs):
5555
else:
5656
span.resource = endpoint_name
5757

58-
# Adding the args in TRACED_ARGS if exist to the span
59-
if not aws.is_blacklist(endpoint_name):
60-
for arg in aws.unpacking_args(args, ARGS_NAME, TRACED_ARGS):
61-
span.set_tag(arg[0], arg[1])
58+
aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS)
6259

6360
region_name = deep_getattr(instance, "meta.region_name")
6461

ddtrace/ext/aws.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
1+
from ..utils.formats import flatten_dict
2+
3+
14
BLACKLIST_ENDPOINT = ["kms", "sts"]
25

36

4-
def is_blacklist(endpoint_name):
5-
"""Protecting the args sent to kms, sts to avoid security leaks
6-
if kms disabled test_kms_client in test/contrib/botocore will fail
7-
if sts disabled test_sts_client in test/contrib/boto contrib will fail
7+
def truncate_arg_value(value, max_len=1024):
8+
"""Truncate values which are bytes and greater than `max_len`.
9+
Useful for parameters like 'Body' in `put_object` operations.
810
"""
9-
return endpoint_name in BLACKLIST_ENDPOINT
11+
if isinstance(value, bytes) and len(value) > max_len:
12+
return b'...'
1013

14+
return value
1115

12-
def unpacking_args(args, args_name, traced_args_list):
13-
"""
14-
@params:
15-
args: tuple of args sent to a patched function
16-
args_name: tuple containing the names of all the args that can be sent
17-
traced_args_list: list of names of the args we want to trace
18-
Returns a list of (arg name, arg) of the args we want to trace
19-
The number of args being variable from one call to another, this function
20-
will parse t"""
21-
index = 0
22-
response = []
23-
for arg in args:
24-
if arg and args_name[index] in traced_args_list:
25-
response += [(args_name[index], arg)]
26-
index += 1
27-
return response
16+
17+
def add_span_arg_tags(span, endpoint_name, args, args_names, args_traced):
18+
if endpoint_name not in BLACKLIST_ENDPOINT:
19+
tags = dict(
20+
(name, value)
21+
for (name, value) in zip(args_names, args)
22+
if name in args_traced
23+
)
24+
tags = flatten_dict(tags)
25+
tags = {k: truncate_arg_value(v) for k, v in tags.items()}
26+
span.set_tags(tags)
2827

2928

3029
REGION = "aws.region"

ddtrace/utils/formats.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,16 @@ def asbool(value):
6565
return value
6666

6767
return value.lower() in ("true", "1")
68+
69+
70+
def flatten_dict(d, sep='.', prefix=''):
71+
"""
72+
Returns a normalized dict of depth 1 with keys in order of embedding
73+
74+
"""
75+
# adapted from https://stackoverflow.com/a/19647596
76+
return {
77+
prefix + sep + k if prefix else k: v
78+
for kk, vv in d.items()
79+
for k, v in flatten_dict(vv, sep, kk).items()
80+
} if isinstance(d, dict) else {prefix: d}

tests/contrib/aiobotocore/test.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from botocore.errorfactory import ClientError
55

66
from ddtrace.contrib.aiobotocore.patch import patch, unpatch
7+
from ddtrace.ext import http
8+
from ddtrace.compat import stringify
79

810
from .utils import aiobotocore_client
911
from ..asyncio.utils import AsyncioTestCase, mark_asyncio
@@ -59,11 +61,33 @@ def test_s3_client(self):
5961
eq_(span.resource, 's3.listbuckets')
6062
eq_(span.name, 's3.command')
6163

64+
@mark_asyncio
65+
def test_s3_put(self):
66+
params = dict(Key='foo', Bucket='mybucket', Body=b'bar')
67+
68+
with aiobotocore_client('s3', self.tracer) as s3:
69+
yield from s3.create_bucket(Bucket='mybucket')
70+
yield from s3.put_object(**params)
71+
72+
spans = [trace[0] for trace in self.tracer.writer.pop_traces()]
73+
assert spans
74+
self.assertEqual(len(spans), 2)
75+
self.assertEqual(spans[0].get_tag('aws.operation'), 'CreateBucket')
76+
self.assertEqual(spans[0].get_tag(http.STATUS_CODE), '200')
77+
self.assertEqual(spans[0].service, 'aws.s3')
78+
self.assertEqual(spans[0].resource, 's3.createbucket')
79+
self.assertEqual(spans[1].get_tag('aws.operation'), 'PutObject')
80+
self.assertEqual(spans[1].resource, 's3.putobject')
81+
self.assertEqual(spans[1].get_tag('params.Key'), stringify(params['Key']))
82+
self.assertEqual(spans[1].get_tag('params.Bucket'), stringify(params['Bucket']))
83+
self.assertEqual(spans[1].get_tag('params.Body'), stringify(params['Body']))
84+
6285
@mark_asyncio
6386
def test_s3_client_error(self):
6487
with aiobotocore_client('s3', self.tracer) as s3:
6588
with assert_raises(ClientError):
66-
yield from s3.list_objects(Bucket='mybucket')
89+
# FIXME: add proper clean-up to tearDown
90+
yield from s3.list_objects(Bucket='doesnotexist')
6791

6892
traces = self.tracer.writer.pop_traces()
6993
eq_(len(traces), 1)

0 commit comments

Comments
 (0)