Skip to content

Commit d081441

Browse files
chanchiemhaotianw465
authored andcommitted
Add ability to enable/disable the SDK (#26) (#119)
* Add ability to enable/disable the SDK (#26) * Disabling the SDK does the following: + Ext module patch methods no longer patch. + Any call to BeginSegment() automatically generates a DummySegment instead and will not be sent to the Daemon, and consequently, since DummySegments are generated, any generated subsegments automatically become DummySubsegments. + For Lambda, when Subsegments are created, they automatically are converted to DummySubsegments * Disabling/Enabling is done through the configure() method of the recorder. It may also be set through an environment variable "AWS_XRAY_ENABLED". * Add ability to enable/disable the SDK (#26) Revision 2 Changes: - SDKConfig now built on top of recorder. SDK-level configuration should all be done through SDKConfig. - Renamed the SDK Enable environment variable to AWS_XRAY_SDK_ENABLED - Disabling the SDK now causes LambdaContext to set sampling decision of facade segment to False. Previously, it would force all subsegments to be generated as dummy subsegments. - SDKConfig.set_sdk_enabled(value) method defaults to True and throws an exception if value is not of type boolean. * Add ability to enable/disable the SDK (#26) Revision 3 Changes: - Removed enable flag from recorder. Now it checks the global SDK configuration to determine if the SDK is disabled. Global SDK configuration no longer modifies the recorder. - Tests modified to reflect change. * Add ability to enable/disable the SDK (#26) Revision 4 Changes: - Refactored multiple modules; instead of import aws_xray_sdk to get the global_sdk_config, direct import using from... import. - Removed unused XRAY_ENABLED_KEY variable from recorder. It wasn't being used. - Added debug log entry to the patcher to inform the customer that patching is disabled when the SDK is disabled. * Add ability to enable/disable the SDK (#26) Revision 5 Changes: - Refactored sampler to use "from ... import ..." for the global sdk config module. - global sdk config logs and defaults to true instead of throwing an exception when an invalid parameter is passed into the method.
1 parent 2fb1d88 commit d081441

File tree

13 files changed

+281
-5
lines changed

13 files changed

+281
-5
lines changed

aws_xray_sdk/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .sdk_config import SDKConfig
2+
3+
global_sdk_config = SDKConfig()

aws_xray_sdk/core/lambda_launcher.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import logging
33
import threading
44

5+
from aws_xray_sdk import global_sdk_config
56
from .models.facade_segment import FacadeSegment
67
from .models.trace_header import TraceHeader
78
from .context import Context
89

9-
1010
log = logging.getLogger(__name__)
1111

1212

@@ -71,7 +71,8 @@ def put_subsegment(self, subsegment):
7171
current_entity = self.get_trace_entity()
7272

7373
if not self._is_subsegment(current_entity) and current_entity.initializing:
74-
log.warning("Subsegment %s discarded due to Lambda worker still initializing" % subsegment.name)
74+
if sdk_config_module.sdk_enabled():
75+
log.warning("Subsegment %s discarded due to Lambda worker still initializing" % subsegment.name)
7576
return
7677

7778
current_entity.add_subsegment(subsegment)
@@ -93,6 +94,9 @@ def _refresh_context(self):
9394
"""
9495
header_str = os.getenv(LAMBDA_TRACE_HEADER_KEY)
9596
trace_header = TraceHeader.from_header_str(header_str)
97+
if not global_sdk_config.sdk_enabled():
98+
trace_header._sampled = False
99+
96100
segment = getattr(self._local, 'segment', None)
97101

98102
if segment:
@@ -124,7 +128,10 @@ def _initialize_context(self, trace_header):
124128
set by AWS Lambda and initialize storage for subsegments.
125129
"""
126130
sampled = None
127-
if trace_header.sampled == 0:
131+
if not global_sdk_config.sdk_enabled():
132+
# Force subsequent subsegments to be disabled and turned into DummySegments.
133+
sampled = False
134+
elif trace_header.sampled == 0:
128135
sampled = False
129136
elif trace_header.sampled == 1:
130137
sampled = True

aws_xray_sdk/core/patcher.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import wrapt
99

10+
from aws_xray_sdk import global_sdk_config
1011
from .utils.compat import PY2, is_classmethod, is_instance_method
1112

1213
log = logging.getLogger(__name__)
@@ -62,6 +63,10 @@ def _is_valid_import(module):
6263

6364

6465
def patch(modules_to_patch, raise_errors=True, ignore_module_patterns=None):
66+
enabled = global_sdk_config.sdk_enabled()
67+
if not enabled:
68+
log.debug("Skipped patching modules %s because the SDK is currently disabled." % ', '.join(modules_to_patch))
69+
return # Disable module patching if the SDK is disabled.
6570
modules = set()
6671
for module_to_patch in modules_to_patch:
6772
# boto3 depends on botocore and patching botocore is sufficient

aws_xray_sdk/core/recorder.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import platform
66
import time
77

8+
from aws_xray_sdk import global_sdk_config
89
from aws_xray_sdk.version import VERSION
910
from .models.segment import Segment, SegmentContextManager
1011
from .models.subsegment import Subsegment, SubsegmentContextManager
@@ -18,7 +19,7 @@
1819
from .daemon_config import DaemonConfig
1920
from .plugins.utils import get_plugin_modules
2021
from .lambda_launcher import check_in_lambda
21-
from .exceptions.exceptions import SegmentNameMissingException
22+
from .exceptions.exceptions import SegmentNameMissingException, SegmentNotFoundException
2223
from .utils.compat import string_types
2324
from .utils import stacktrace
2425

@@ -88,7 +89,6 @@ def configure(self, sampling=None, plugins=None,
8889
8990
Configure needs to run before patching thrid party libraries
9091
to avoid creating dangling subsegment.
91-
9292
:param bool sampling: If sampling is enabled, every time the recorder
9393
creates a segment it decides whether to send this segment to
9494
the X-Ray daemon. This setting is not used if the recorder
@@ -138,6 +138,7 @@ class to have your own implementation of the streaming process.
138138
and AWS_XRAY_TRACING_NAME respectively overrides arguments
139139
daemon_address, context_missing and service.
140140
"""
141+
141142
if sampling is not None:
142143
self.sampling = sampling
143144
if sampler:
@@ -219,6 +220,12 @@ def begin_segment(self, name=None, traceid=None,
219220
# depending on if centralized or local sampling rule takes effect.
220221
decision = True
221222

223+
# To disable the recorder, we set the sampling decision to always be false.
224+
# This way, when segments are generated, they become dummy segments and are ultimately never sent.
225+
# The call to self._sampler.should_trace() is never called either so the poller threads are never started.
226+
if not global_sdk_config.sdk_enabled():
227+
sampling = 0
228+
222229
# we respect the input sampling decision
223230
# regardless of recorder configuration.
224231
if sampling == 0:
@@ -273,6 +280,7 @@ def begin_subsegment(self, name, namespace='local'):
273280
:param str name: the name of the subsegment.
274281
:param str namespace: currently can only be 'local', 'remote', 'aws'.
275282
"""
283+
276284
segment = self.current_segment()
277285
if not segment:
278286
log.warning("No segment found, cannot begin subsegment %s." % name)
@@ -396,6 +404,16 @@ def capture(self, name=None):
396404
def record_subsegment(self, wrapped, instance, args, kwargs, name,
397405
namespace, meta_processor):
398406

407+
# In the case when the SDK is disabled, we ensure that a parent segment exists, because this is usually
408+
# handled by the middleware. We generate a dummy segment as the parent segment if one doesn't exist.
409+
# This is to allow potential segment method calls to not throw exceptions in the captured method.
410+
if not global_sdk_config.sdk_enabled():
411+
try:
412+
self.current_segment()
413+
except SegmentNotFoundException:
414+
segment = DummySegment(name)
415+
self.context.put_segment(segment)
416+
399417
subsegment = self.begin_subsegment(name, namespace)
400418

401419
exception = None
@@ -473,6 +491,14 @@ def _is_subsegment(self, entity):
473491

474492
return (hasattr(entity, 'type') and entity.type == 'subsegment')
475493

494+
@property
495+
def enabled(self):
496+
return self._enabled
497+
498+
@enabled.setter
499+
def enabled(self, value):
500+
self._enabled = value
501+
476502
@property
477503
def sampling(self):
478504
return self._sampling

aws_xray_sdk/core/sampling/sampler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .target_poller import TargetPoller
1010
from .connector import ServiceConnector
1111
from .reservoir import ReservoirDecision
12+
from aws_xray_sdk import global_sdk_config
1213

1314
log = logging.getLogger(__name__)
1415

@@ -37,6 +38,9 @@ def start(self):
3738
Start rule poller and target poller once X-Ray daemon address
3839
and context manager is in place.
3940
"""
41+
if not global_sdk_config.sdk_enabled():
42+
return
43+
4044
with self._lock:
4145
if not self._started:
4246
self._rule_poller.start()
@@ -51,6 +55,9 @@ def should_trace(self, sampling_req=None):
5155
All optional arguments are extracted from incoming requests by
5256
X-Ray middleware to perform path based sampling.
5357
"""
58+
if not global_sdk_config.sdk_enabled():
59+
return False
60+
5461
if not self._started:
5562
self.start() # only front-end that actually uses the sampler spawns poller threads
5663

aws_xray_sdk/sdk_config.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
import logging
3+
4+
log = logging.getLogger(__name__)
5+
6+
7+
class SDKConfig(object):
8+
"""
9+
Global Configuration Class that defines SDK-level configuration properties.
10+
11+
Enabling/Disabling the SDK:
12+
By default, the SDK is enabled unless if an environment variable AWS_XRAY_SDK_ENABLED
13+
is set. If it is set, it needs to be a valid string boolean, otherwise, it will default
14+
to true. If the environment variable is set, all calls to set_sdk_enabled() will
15+
prioritize the value of the environment variable.
16+
Disabling the SDK affects the recorder, patcher, and middlewares in the following ways:
17+
For the recorder, disabling automatically generates DummySegments for subsequent segments
18+
and DummySubsegments for subsegments created and thus not send any traces to the daemon.
19+
For the patcher, module patching will automatically be disabled. The SDK must be disabled
20+
before calling patcher.patch() method in order for this to function properly.
21+
For the middleware, no modification is made on them, but since the recorder automatically
22+
generates DummySegments for all subsequent calls, they will not generate segments/subsegments
23+
to be sent.
24+
25+
Environment variables:
26+
"AWS_XRAY_SDK_ENABLED" - If set to 'false' disables the SDK and causes the explained above
27+
to occur.
28+
"""
29+
XRAY_ENABLED_KEY = 'AWS_XRAY_SDK_ENABLED'
30+
__SDK_ENABLED = str(os.getenv(XRAY_ENABLED_KEY, 'true')).lower() != 'false'
31+
32+
@classmethod
33+
def sdk_enabled(cls):
34+
"""
35+
Returns whether the SDK is enabled or not.
36+
"""
37+
return cls.__SDK_ENABLED
38+
39+
@classmethod
40+
def set_sdk_enabled(cls, value):
41+
"""
42+
Modifies the enabled flag if the "AWS_XRAY_SDK_ENABLED" environment variable is not set,
43+
otherwise, set the enabled flag to be equal to the environment variable. If the
44+
env variable is an invalid string boolean, it will default to true.
45+
46+
:param bool value: Flag to set whether the SDK is enabled or disabled.
47+
48+
Environment variables AWS_XRAY_SDK_ENABLED overrides argument value.
49+
"""
50+
# Environment Variables take precedence over hardcoded configurations.
51+
if cls.XRAY_ENABLED_KEY in os.environ:
52+
cls.__SDK_ENABLED = str(os.getenv(cls.XRAY_ENABLED_KEY, 'true')).lower() != 'false'
53+
else:
54+
if type(value) == bool:
55+
cls.__SDK_ENABLED = value
56+
else:
57+
cls.__SDK_ENABLED = True
58+
log.warning("Invalid parameter type passed into set_sdk_enabled(). Defaulting to True...")

tests/ext/aiohttp/test_middleware.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Expects pytest-aiohttp
55
"""
66
import asyncio
7+
from aws_xray_sdk import global_sdk_config
78
from unittest.mock import patch
89

910
from aiohttp import web
@@ -109,6 +110,7 @@ def recorder(loop):
109110

110111
xray_recorder.clear_trace_entities()
111112
yield xray_recorder
113+
global_sdk_config.set_sdk_enabled(True)
112114
xray_recorder.clear_trace_entities()
113115
patcher.stop()
114116

@@ -283,3 +285,21 @@ async def get_delay():
283285
# Ensure all ID's are different
284286
ids = [item.id for item in recorder.emitter.local]
285287
assert len(ids) == len(set(ids))
288+
289+
290+
async def test_disabled_sdk(test_client, loop, recorder):
291+
"""
292+
Test a normal response when the SDK is disabled.
293+
294+
:param test_client: AioHttp test client fixture
295+
:param loop: Eventloop fixture
296+
:param recorder: X-Ray recorder fixture
297+
"""
298+
global_sdk_config.set_sdk_enabled(False)
299+
client = await test_client(ServerTest.app(loop=loop))
300+
301+
resp = await client.get('/')
302+
assert resp.status == 200
303+
304+
segment = recorder.emitter.pop()
305+
assert not segment

tests/ext/django/test_middleware.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import django
2+
from aws_xray_sdk import global_sdk_config
23
from django.core.urlresolvers import reverse
34
from django.test import TestCase
45

@@ -14,6 +15,7 @@ def setUp(self):
1415
xray_recorder.configure(context=Context(),
1516
context_missing='LOG_ERROR')
1617
xray_recorder.clear_trace_entities()
18+
global_sdk_config.set_sdk_enabled(True)
1719

1820
def tearDown(self):
1921
xray_recorder.clear_trace_entities()
@@ -102,3 +104,10 @@ def test_response_header(self):
102104

103105
assert 'Sampled=1' in trace_header
104106
assert segment.trace_id in trace_header
107+
108+
def test_disabled_sdk(self):
109+
global_sdk_config.set_sdk_enabled(False)
110+
url = reverse('200ok')
111+
self.client.get(url)
112+
segment = xray_recorder.emitter.pop()
113+
assert not segment

tests/ext/flask/test_flask.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22
from flask import Flask, render_template_string
33

4+
from aws_xray_sdk import global_sdk_config
45
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware
56
from aws_xray_sdk.core.context import Context
67
from aws_xray_sdk.core.models import http
@@ -51,6 +52,7 @@ def cleanup():
5152
recorder.clear_trace_entities()
5253
yield
5354
recorder.clear_trace_entities()
55+
global_sdk_config.set_sdk_enabled(True)
5456

5557

5658
def test_ok():
@@ -143,3 +145,11 @@ def test_sampled_response_header():
143145
resp_header = resp.headers[http.XRAY_HEADER]
144146
assert segment.trace_id in resp_header
145147
assert 'Sampled=1' in resp_header
148+
149+
150+
def test_disabled_sdk():
151+
global_sdk_config.set_sdk_enabled(False)
152+
path = '/ok'
153+
app.get(path)
154+
segment = recorder.emitter.pop()
155+
assert not segment

tests/test_lambda_context.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22

3+
from aws_xray_sdk import global_sdk_config
4+
import pytest
35
from aws_xray_sdk.core import lambda_launcher
46
from aws_xray_sdk.core.models.subsegment import Subsegment
57

@@ -12,6 +14,12 @@
1214
context = lambda_launcher.LambdaContext()
1315

1416

17+
@pytest.fixture(autouse=True)
18+
def setup():
19+
yield
20+
global_sdk_config.set_sdk_enabled(True)
21+
22+
1523
def test_facade_segment_generation():
1624

1725
segment = context.get_trace_entity()
@@ -41,3 +49,14 @@ def test_put_subsegment():
4149

4250
context.end_subsegment()
4351
assert context.get_trace_entity().id == segment.id
52+
53+
54+
def test_disable():
55+
context.clear_trace_entities()
56+
segment = context.get_trace_entity()
57+
assert segment.sampled
58+
59+
context.clear_trace_entities()
60+
global_sdk_config.set_sdk_enabled(False)
61+
segment = context.get_trace_entity()
62+
assert not segment.sampled

tests/test_patcher.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# Python versions < 3 have reload built-in
1414
pass
1515

16+
from aws_xray_sdk import global_sdk_config
1617
from aws_xray_sdk.core import patcher, xray_recorder
1718
from aws_xray_sdk.core.context import Context
1819

@@ -40,6 +41,7 @@ def construct_ctx():
4041
yield
4142
xray_recorder.end_segment()
4243
xray_recorder.clear_trace_entities()
44+
global_sdk_config.set_sdk_enabled(True)
4345

4446
# Reload wrapt.importer references to modules to start off clean
4547
reload(wrapt)
@@ -172,3 +174,11 @@ def test_external_submodules_ignores_module():
172174
assert xray_recorder.current_segment().subsegments[0].name == 'mock_init'
173175
assert xray_recorder.current_segment().subsegments[1].name == 'mock_func'
174176
assert xray_recorder.current_segment().subsegments[2].name == 'mock_no_doublepatch' # It is patched with decorator
177+
178+
179+
def test_disable_sdk_disables_patching():
180+
global_sdk_config.set_sdk_enabled(False)
181+
patcher.patch(['tests.mock_module'])
182+
imported_modules = [module for module in TEST_MODULES if module in sys.modules]
183+
assert not imported_modules
184+
assert len(xray_recorder.current_segment().subsegments) == 0

0 commit comments

Comments
 (0)