Skip to content

Commit 6b64982

Browse files
authored
Oversampling Mitigation (#366)
* implemented oversampling mitigation * updated begin_subsegment docstring to include sampling parameter * created separate API for adding subsegments without sampling * Modified add_subsegment method to log warning for orphaned subsegments * updated unit tests * addressing feedback * final design changes * remove default namespace value * minor fix
1 parent 6e30483 commit 6b64982

10 files changed

+230
-17
lines changed

aws_xray_sdk/core/models/dummy_entities.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class DummySegment(Segment):
1111
the segment based on sampling rules.
1212
Adding data to a dummy segment becomes a no-op except for
1313
subsegments. This is to reduce the memory footprint of the SDK.
14-
A dummy segment will not be sent to the X-Ray daemon. Manually create
14+
A dummy segment will not be sent to the X-Ray daemon. Manually creating
1515
dummy segments is not recommended.
1616
"""
1717

aws_xray_sdk/core/models/entity.py

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def add_subsegment(self, subsegment):
8181
"""
8282
self._check_ended()
8383
subsegment.parent_id = self.id
84+
85+
if not self.sampled and subsegment.sampled:
86+
log.warning("This sampled subsegment is being added to an unsampled parent segment/subsegment and will be orphaned.")
87+
8488
self.subsegments.append(subsegment)
8589

8690
def remove_subsegment(self, subsegment):

aws_xray_sdk/core/recorder.py

+35-14
Original file line numberDiff line numberDiff line change
@@ -275,16 +275,10 @@ def current_segment(self):
275275
else:
276276
return entity
277277

278-
def begin_subsegment(self, name, namespace='local'):
279-
"""
280-
Begin a new subsegment.
281-
If there is open subsegment, the newly created subsegment will be the
282-
child of latest opened subsegment.
283-
If not, it will be the child of the current open segment.
284-
285-
:param str name: the name of the subsegment.
286-
:param str namespace: currently can only be 'local', 'remote', 'aws'.
287-
"""
278+
def _begin_subsegment_helper(self, name, namespace='local', beginWithoutSampling=False):
279+
'''
280+
Helper method to begin_subsegment and begin_subsegment_without_sampling
281+
'''
288282
# Generating the parent dummy segment is necessary.
289283
# We don't need to store anything in context. Assumption here
290284
# is that we only work with recorder-level APIs.
@@ -295,16 +289,42 @@ def begin_subsegment(self, name, namespace='local'):
295289
if not segment:
296290
log.warning("No segment found, cannot begin subsegment %s." % name)
297291
return None
298-
299-
if not segment.sampled:
292+
293+
current_entity = self.get_trace_entity()
294+
if not current_entity.sampled or beginWithoutSampling:
300295
subsegment = DummySubsegment(segment, name)
301296
else:
302297
subsegment = Subsegment(name, namespace, segment)
303298

304299
self.context.put_subsegment(subsegment)
305-
306300
return subsegment
307301

302+
303+
304+
def begin_subsegment(self, name, namespace='local'):
305+
"""
306+
Begin a new subsegment.
307+
If there is open subsegment, the newly created subsegment will be the
308+
child of latest opened subsegment.
309+
If not, it will be the child of the current open segment.
310+
311+
:param str name: the name of the subsegment.
312+
:param str namespace: currently can only be 'local', 'remote', 'aws'.
313+
"""
314+
return self._begin_subsegment_helper(name, namespace)
315+
316+
317+
def begin_subsegment_without_sampling(self, name):
318+
"""
319+
Begin a new unsampled subsegment.
320+
If there is open subsegment, the newly created subsegment will be the
321+
child of latest opened subsegment.
322+
If not, it will be the child of the current open segment.
323+
324+
:param str name: the name of the subsegment.
325+
"""
326+
return self._begin_subsegment_helper(name, beginWithoutSampling=True)
327+
308328
def current_subsegment(self):
309329
"""
310330
Return the latest opened subsegment. In a multithreading environment,
@@ -487,7 +507,8 @@ def _send_segment(self):
487507

488508
def _stream_subsegment_out(self, subsegment):
489509
log.debug("streaming subsegments...")
490-
self.emitter.send_entity(subsegment)
510+
if subsegment.sampled:
511+
self.emitter.send_entity(subsegment)
491512

492513
def _load_sampling_rules(self, sampling_rules):
493514

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
SQS_XRAY_HEADER = "AWSTraceHeader"
2+
class SqsMessageHelper:
3+
4+
@staticmethod
5+
def isSampled(sqs_message):
6+
attributes = sqs_message['attributes']
7+
8+
if SQS_XRAY_HEADER not in attributes:
9+
return False
10+
11+
return 'Sampled=1' in attributes[SQS_XRAY_HEADER]

aws_xray_sdk/ext/util.py

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def inject_trace_header(headers, entity):
3535
else:
3636
header = entity.get_origin_trace_header()
3737
data = header.data if header else None
38-
3938
to_insert = TraceHeader(
4039
root=entity.trace_id,
4140
parent=entity.id,

tests/test_facade_segment.py

+15
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,18 @@ def test_structure_intact():
5555

5656
assert segment.subsegments[0] is subsegment
5757
assert subsegment.subsegments[0] is subsegment2
58+
59+
def test_adding_unsampled_subsegment():
60+
61+
segment = FacadeSegment('name', 'id', 'id', True)
62+
subsegment = Subsegment('sampled', 'local', segment)
63+
subsegment2 = Subsegment('unsampled', 'local', segment)
64+
subsegment2.sampled = False
65+
66+
segment.add_subsegment(subsegment)
67+
subsegment.add_subsegment(subsegment2)
68+
69+
70+
assert segment.subsegments[0] is subsegment
71+
assert subsegment.subsegments[0] is subsegment2
72+
assert subsegment2.sampled == False

tests/test_recorder.py

+35
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,24 @@ def test_first_begin_segment_sampled():
141141

142142
assert segment.sampled
143143

144+
def test_unsampled_subsegment_of_sampled_parent():
145+
xray_recorder = get_new_stubbed_recorder()
146+
xray_recorder.configure(sampling=True)
147+
segment = xray_recorder.begin_segment('name', sampling=True)
148+
subsegment = xray_recorder.begin_subsegment_without_sampling('unsampled')
149+
150+
assert segment.sampled == True
151+
assert subsegment.sampled == False
152+
153+
def test_begin_subsegment_unsampled():
154+
xray_recorder = get_new_stubbed_recorder()
155+
xray_recorder.configure(sampling=False)
156+
segment = xray_recorder.begin_segment('name', sampling=False)
157+
subsegment = xray_recorder.begin_subsegment_without_sampling('unsampled')
158+
159+
assert segment.sampled == False
160+
assert subsegment.sampled == False
161+
144162

145163
def test_in_segment_closing():
146164
xray_recorder = get_new_stubbed_recorder()
@@ -201,6 +219,23 @@ def test_disable_is_dummy():
201219
assert type(xray_recorder.current_segment()) is DummySegment
202220
assert type(xray_recorder.current_subsegment()) is DummySubsegment
203221

222+
def test_unsampled_subsegment_is_dummy():
223+
assert global_sdk_config.sdk_enabled()
224+
segment = xray_recorder.begin_segment('name')
225+
subsegment = xray_recorder.begin_subsegment_without_sampling('name')
226+
227+
assert type(xray_recorder.current_subsegment()) is DummySubsegment
228+
229+
def test_subsegment_respects_parent_sampling_decision():
230+
assert global_sdk_config.sdk_enabled()
231+
segment = xray_recorder.begin_segment('name')
232+
subsegment = xray_recorder.begin_subsegment_without_sampling('name2')
233+
subsegment2 = xray_recorder.begin_subsegment('unsampled-subsegment')
234+
235+
assert type(xray_recorder.current_subsegment()) is DummySubsegment
236+
assert subsegment.sampled == False
237+
assert subsegment2.sampled == False
238+
204239

205240
def test_disabled_empty_context_current_calls():
206241
global_sdk_config.set_sdk_enabled(False)

tests/test_sqs_message_helper.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from aws_xray_sdk.core.utils.sqs_message_helper import SqsMessageHelper
2+
3+
import pytest
4+
5+
sampleSqsMessageEvent = {
6+
"Records": [
7+
{
8+
"messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
9+
"receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...",
10+
"body": "Test message.",
11+
"attributes": {
12+
"ApproximateReceiveCount": "1",
13+
"SentTimestamp": "1545082649183",
14+
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
15+
"ApproximateFirstReceiveTimestamp": "1545082649185",
16+
"AWSTraceHeader":"Root=1-632BB806-bd862e3fe1be46a994272793;Sampled=1"
17+
},
18+
"messageAttributes": {},
19+
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
20+
"eventSource": "aws:sqs",
21+
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
22+
"awsRegion": "us-east-2"
23+
},
24+
{
25+
"messageId": "2e1424d4-f796-459a-8184-9c92662be6da",
26+
"receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...",
27+
"body": "Test message.",
28+
"attributes": {
29+
"ApproximateReceiveCount": "1",
30+
"SentTimestamp": "1545082650636",
31+
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
32+
"ApproximateFirstReceiveTimestamp": "1545082650649",
33+
"AWSTraceHeader":"Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0"
34+
},
35+
"messageAttributes": {},
36+
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
37+
"eventSource": "aws:sqs",
38+
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
39+
"awsRegion": "us-east-2"
40+
},
41+
{
42+
"messageId": "2e1424d4-f796-459a-8184-9c92662be6da",
43+
"receiptHandle": "AQEBzWwaftRI0KuVm4tP+/7q1rGgNqicHq...",
44+
"body": "Test message.",
45+
"attributes": {
46+
"ApproximateReceiveCount": "1",
47+
"SentTimestamp": "1545082650636",
48+
"SenderId": "AIDAIENQZJOLO23YVJ4VO",
49+
"ApproximateFirstReceiveTimestamp": "1545082650649",
50+
"AWSTraceHeader":"Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8"
51+
},
52+
"messageAttributes": {},
53+
"md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
54+
"eventSource": "aws:sqs",
55+
"eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
56+
"awsRegion": "us-east-2"
57+
}
58+
]
59+
}
60+
61+
def test_return_true_when_sampling_1():
62+
assert SqsMessageHelper.isSampled(sampleSqsMessageEvent['Records'][0]) == True
63+
64+
def test_return_false_when_sampling_0():
65+
assert SqsMessageHelper.isSampled(sampleSqsMessageEvent['Records'][1]) == False
66+
67+
def test_return_false_with_no_sampling_flag():
68+
assert SqsMessageHelper.isSampled(sampleSqsMessageEvent['Records'][2]) == False

tests/test_trace_entities.py

+19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
from aws_xray_sdk.core.exceptions.exceptions import AlreadyEndedException
1212

1313
from .util import entity_to_dict
14+
from .util import get_new_stubbed_recorder
15+
16+
xray_recorder = get_new_stubbed_recorder()
1417

1518

1619
def test_unicode_entity_name():
@@ -263,3 +266,19 @@ def test_add_exception_appending_exceptions():
263266

264267
assert isinstance(segment.cause, dict)
265268
assert len(segment.cause['exceptions']) == 2
269+
270+
def test_adding_subsegments_with_recorder():
271+
xray_recorder.configure(sampling=False)
272+
xray_recorder.clear_trace_entities()
273+
274+
segment = xray_recorder.begin_segment('parent');
275+
subsegment = xray_recorder.begin_subsegment('sampled-child')
276+
unsampled_subsegment = xray_recorder.begin_subsegment_without_sampling('unsampled-child1')
277+
unsampled_child_subsegment = xray_recorder.begin_subsegment('unsampled-child2')
278+
279+
assert segment.sampled == True
280+
assert subsegment.sampled == True
281+
assert unsampled_subsegment.sampled == False
282+
assert unsampled_child_subsegment.sampled == False
283+
284+
xray_recorder.clear_trace_entities()

tests/test_utils.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from aws_xray_sdk.ext.util import to_snake_case, get_hostname, strip_url
1+
from aws_xray_sdk.ext.util import to_snake_case, get_hostname, strip_url, inject_trace_header
2+
from aws_xray_sdk.core.models.segment import Segment
3+
from aws_xray_sdk.core.models.subsegment import Subsegment
4+
from aws_xray_sdk.core.models.dummy_entities import DummySegment, DummySubsegment
5+
from .util import get_new_stubbed_recorder
6+
7+
xray_recorder = get_new_stubbed_recorder()
28

39
UNKNOWN_HOST = "UNKNOWN HOST"
410

@@ -52,3 +58,38 @@ def test_strip_url():
5258

5359
assert strip_url("") == ""
5460
assert not strip_url(None)
61+
62+
63+
def test_inject_trace_header_unsampled():
64+
headers = {'host': 'test', 'accept': '*/*', 'connection': 'keep-alive', 'X-Amzn-Trace-Id': 'Root=1-6369739a-7d8bb07e519b795eb24d382d;Parent=089e3de743fb9e79;Sampled=1'}
65+
xray_recorder = get_new_stubbed_recorder()
66+
xray_recorder.configure(sampling=True)
67+
segment = xray_recorder.begin_segment('name', sampling=True)
68+
subsegment = xray_recorder.begin_subsegment_without_sampling('unsampled')
69+
70+
inject_trace_header(headers, subsegment)
71+
72+
assert 'Sampled=0' in headers['X-Amzn-Trace-Id']
73+
74+
def test_inject_trace_header_respects_parent_subsegment():
75+
headers = {'host': 'test', 'accept': '*/*', 'connection': 'keep-alive', 'X-Amzn-Trace-Id': 'Root=1-6369739a-7d8bb07e519b795eb24d382d;Parent=089e3de743fb9e79;Sampled=1'}
76+
77+
xray_recorder = get_new_stubbed_recorder()
78+
xray_recorder.configure(sampling=True)
79+
segment = xray_recorder.begin_segment('name', sampling=True)
80+
subsegment = xray_recorder.begin_subsegment_without_sampling('unsampled')
81+
subsegment2 = xray_recorder.begin_subsegment('unsampled2')
82+
inject_trace_header(headers, subsegment2)
83+
84+
assert 'Sampled=0' in headers['X-Amzn-Trace-Id']
85+
86+
def test_inject_trace_header_sampled():
87+
headers = {'host': 'test', 'accept': '*/*', 'connection': 'keep-alive', 'X-Amzn-Trace-Id': 'Root=1-6369739a-7d8bb07e519b795eb24d382d;Parent=089e3de743fb9e79;Sampled=1'}
88+
xray_recorder = get_new_stubbed_recorder()
89+
xray_recorder.configure(sampling=True)
90+
segment = xray_recorder.begin_segment('name')
91+
subsegment = xray_recorder.begin_subsegment('unsampled')
92+
93+
inject_trace_header(headers, subsegment)
94+
95+
assert 'Sampled=1' in headers['X-Amzn-Trace-Id']

0 commit comments

Comments
 (0)