Skip to content

Commit 6de2714

Browse files
committed
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".
1 parent 9e9063f commit 6de2714

File tree

11 files changed

+272
-6
lines changed

11 files changed

+272
-6
lines changed

aws_xray_sdk/core/lambda_launcher.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import logging
33
import threading
44

5+
from aws_xray_sdk.sdk_config import SDKConfig
56
from .models.facade_segment import FacadeSegment
67
from .models.trace_header import TraceHeader
8+
from .models.dummy_entities import DummySubsegment
79
from .context import Context
810

9-
1011
log = logging.getLogger(__name__)
1112

1213

@@ -74,6 +75,11 @@ def put_subsegment(self, subsegment):
7475
log.warning("Subsegment %s discarded due to Lambda worker still initializing" % subsegment.name)
7576
return
7677

78+
enabled = SDKConfig.sdk_enabled()
79+
if not enabled:
80+
# For lambda, if the SDK is not enabled, we force the subsegment to be a dummy segment.
81+
subsegment = DummySubsegment(current_entity)
82+
7783
current_entity.add_subsegment(subsegment)
7884
self._local.entities.append(subsegment)
7985

aws_xray_sdk/core/patcher.py

Lines changed: 4 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.sdk_config import SDKConfig
1011
from .utils.compat import PY2, is_classmethod, is_instance_method
1112

1213
log = logging.getLogger(__name__)
@@ -60,6 +61,9 @@ def _is_valid_import(module):
6061

6162

6263
def patch(modules_to_patch, raise_errors=True, ignore_module_patterns=None):
64+
enabled = SDKConfig.sdk_enabled()
65+
if not enabled:
66+
return # Disable module patching if the SDK is disabled.
6367
modules = set()
6468
for module_to_patch in modules_to_patch:
6569
# boto3 depends on botocore and patching botocore is sufficient

aws_xray_sdk/core/recorder.py

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

88
from aws_xray_sdk.version import VERSION
9+
from aws_xray_sdk.sdk_config import SDKConfig
910
from .models.segment import Segment, SegmentContextManager
1011
from .models.subsegment import Subsegment, SubsegmentContextManager
1112
from .models.default_dynamic_naming import DefaultDynamicNaming
@@ -18,12 +19,13 @@
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

2526
log = logging.getLogger(__name__)
2627

28+
XRAY_ENABLED_KEY = 'AWS_XRAY_ENABLED'
2729
TRACING_NAME_KEY = 'AWS_XRAY_TRACING_NAME'
2830
DAEMON_ADDR_KEY = 'AWS_XRAY_DAEMON_ADDRESS'
2931
CONTEXT_MISSING_KEY = 'AWS_XRAY_CONTEXT_MISSING'
@@ -64,6 +66,7 @@ def __init__(self):
6466
self._context = Context()
6567
self._sampler = DefaultSampler()
6668

69+
self._enabled = True
6770
self._emitter = UDPEmitter()
6871
self._sampling = True
6972
self._max_trace_back = 10
@@ -77,7 +80,7 @@ def __init__(self):
7780
if type(self.sampler).__name__ == 'DefaultSampler':
7881
self.sampler.load_settings(DaemonConfig(), self.context)
7982

80-
def configure(self, sampling=None, plugins=None,
83+
def configure(self, enabled=None, sampling=None, plugins=None,
8184
context_missing=None, sampling_rules=None,
8285
daemon_address=None, service=None,
8386
context=None, emitter=None, streaming=None,
@@ -88,7 +91,16 @@ def configure(self, sampling=None, plugins=None,
8891
8992
Configure needs to run before patching thrid party libraries
9093
to avoid creating dangling subsegment.
91-
94+
:param bool enabled: If not enabled, the recorder automatically
95+
generates DummySegments for every segment creation, whether
96+
through patched extensions nor middlewares, and thus
97+
not send any Segments out to the Daemon. May be set through an
98+
environmental variable, where the environmental variable will
99+
always take precedence over hardcoded configurations. The environment
100+
variable is set as a case-insensitive string boolean. If the environment
101+
variable exists but is an invalid string boolean, this enabled flag
102+
will automatically be set to true. If no enabled flag is given and the
103+
env variable is not set, then it will also default to being enabled.
92104
:param bool sampling: If sampling is enabled, every time the recorder
93105
creates a segment it decides whether to send this segment to
94106
the X-Ray daemon. This setting is not used if the recorder
@@ -134,10 +146,11 @@ class to have your own implementation of the streaming process.
134146
by auto-capture. Lower this if a single document becomes too large.
135147
:param bool stream_sql: Whether SQL query texts should be streamed.
136148
137-
Environment variables AWS_XRAY_DAEMON_ADDRESS, AWS_XRAY_CONTEXT_MISSING
149+
Environment variables AWS_XRAY_ENABLED, AWS_XRAY_DAEMON_ADDRESS, AWS_XRAY_CONTEXT_MISSING
138150
and AWS_XRAY_TRACING_NAME respectively overrides arguments
139-
daemon_address, context_missing and service.
151+
enabled, daemon_address, context_missing and service.
140152
"""
153+
141154
if sampling is not None:
142155
self.sampling = sampling
143156
if sampler:
@@ -164,6 +177,12 @@ class to have your own implementation of the streaming process.
164177
self.max_trace_back = max_trace_back
165178
if stream_sql is not None:
166179
self.stream_sql = stream_sql
180+
if enabled is not None:
181+
SDKConfig.set_sdk_enabled(enabled)
182+
else:
183+
# By default we enable if no enable parameter is given. Prevents unit tests from breaking
184+
# if setup doesn't explicitly set enabled while other tests set enabled to false.
185+
SDKConfig.set_sdk_enabled(True)
167186

168187
if plugins:
169188
plugin_modules = get_plugin_modules(plugins)
@@ -219,6 +238,12 @@ def begin_segment(self, name=None, traceid=None,
219238
# depending on if centralized or local sampling rule takes effect.
220239
decision = True
221240

241+
# To disable the recorder, we set the sampling decision to always be false.
242+
# This way, when segments are generated, they become dummy segments and are ultimately never sent.
243+
# The call to self._sampler.should_trace() is never called either so the poller threads are never started.
244+
if not SDKConfig.sdk_enabled():
245+
sampling = 0
246+
222247
# we respect the input sampling decision
223248
# regardless of recorder configuration.
224249
if sampling == 0:
@@ -273,6 +298,7 @@ def begin_subsegment(self, name, namespace='local'):
273298
:param str name: the name of the subsegment.
274299
:param str namespace: currently can only be 'local', 'remote', 'aws'.
275300
"""
301+
276302
segment = self.current_segment()
277303
if not segment:
278304
log.warning("No segment found, cannot begin subsegment %s." % name)
@@ -396,6 +422,16 @@ def capture(self, name=None):
396422
def record_subsegment(self, wrapped, instance, args, kwargs, name,
397423
namespace, meta_processor):
398424

425+
# In the case when the SDK is disabled, we ensure that a parent segment exists, because this is usually
426+
# handled by the middleware. We generate a dummy segment as the parent segment if one doesn't exist.
427+
# This is to allow potential segment method calls to not throw exceptions in the captured method.
428+
if not SDKConfig.sdk_enabled():
429+
try:
430+
self.current_segment()
431+
except SegmentNotFoundException:
432+
segment = DummySegment(name)
433+
self.context.put_segment(segment)
434+
399435
subsegment = self.begin_subsegment(name, namespace)
400436

401437
exception = None
@@ -473,6 +509,14 @@ def _is_subsegment(self, entity):
473509

474510
return (hasattr(entity, 'type') and entity.type == 'subsegment')
475511

512+
@property
513+
def enabled(self):
514+
return self._enabled
515+
516+
@enabled.setter
517+
def enabled(self, value):
518+
self._enabled = value
519+
476520
@property
477521
def sampling(self):
478522
return self._sampling

aws_xray_sdk/sdk_config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
3+
4+
class SDKConfig(object):
5+
"""
6+
Global Configuration Class that defines SDK-level configuration properties.
7+
It is recommended to only use the recorder to set this configuration's enabled
8+
flag to maintain thread safety.
9+
"""
10+
XRAY_ENABLED_KEY = 'AWS_XRAY_ENABLED'
11+
__SDK_ENABLED = str(os.getenv(XRAY_ENABLED_KEY, 'true')).lower() != 'false'
12+
13+
@staticmethod
14+
def sdk_enabled():
15+
"""
16+
Returns whether the SDK is enabled or not.
17+
"""
18+
return SDKConfig.__SDK_ENABLED
19+
20+
@staticmethod
21+
def set_sdk_enabled(value):
22+
"""
23+
Modifies the enabled flag if the "AWS_XRAY_ENABLED" environment variable is not set,
24+
otherwise, set the enabled flag to be equal to the environment variable. If the
25+
env variable is an invalid string boolean, it will default to true.
26+
27+
:param bool value: Flag to set whether the SDK is enabled or disabled.
28+
29+
Environment variables AWS_XRAY_ENABLED overrides argument value.
30+
"""
31+
# Environment Variables take precedence over hardcoded configurations.
32+
if SDKConfig.XRAY_ENABLED_KEY in os.environ:
33+
SDKConfig.__SDK_ENABLED = str(os.getenv(SDKConfig.XRAY_ENABLED_KEY, 'true')).lower() != 'false'
34+
else:
35+
SDKConfig.__SDK_ENABLED = value

tests/ext/aiohttp/test_middleware.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,21 @@ async def get_delay():
283283
# Ensure all ID's are different
284284
ids = [item.id for item in recorder.emitter.local]
285285
assert len(ids) == len(set(ids))
286+
287+
288+
async def test_disabled_sdk(test_client, loop, recorder):
289+
"""
290+
Test a normal response when the SDK is disabled.
291+
292+
:param test_client: AioHttp test client fixture
293+
:param loop: Eventloop fixture
294+
:param recorder: X-Ray recorder fixture
295+
"""
296+
recorder.configure(enabled=False)
297+
client = await test_client(ServerTest.app(loop=loop))
298+
299+
resp = await client.get('/')
300+
assert resp.status == 200
301+
302+
segment = recorder.emitter.pop()
303+
assert not segment

tests/ext/django/test_middleware.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,10 @@ def test_response_header(self):
102102

103103
assert 'Sampled=1' in trace_header
104104
assert segment.trace_id in trace_header
105+
106+
def test_disabled_sdk(self):
107+
xray_recorder.configure(enabled=False)
108+
url = reverse('200ok')
109+
self.client.get(url)
110+
segment = xray_recorder.emitter.pop()
111+
assert not segment

tests/ext/flask/test_flask.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,11 @@ def test_sampled_response_header():
143143
resp_header = resp.headers[http.XRAY_HEADER]
144144
assert segment.trace_id in resp_header
145145
assert 'Sampled=1' in resp_header
146+
147+
148+
def test_disabled_sdk():
149+
recorder.configure(enabled=False)
150+
path = '/ok'
151+
app.get(path)
152+
segment = recorder.emitter.pop()
153+
assert not segment

tests/test_lambda_context.py

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

3+
from aws_xray_sdk.sdk_config import SDKConfig
34
from aws_xray_sdk.core import lambda_launcher
45
from aws_xray_sdk.core.models.subsegment import Subsegment
6+
from aws_xray_sdk.core.models.dummy_entities import DummySubsegment
57

68

79
TRACE_ID = '1-5759e988-bd862e3fe1be46a994272793'
@@ -41,3 +43,11 @@ def test_put_subsegment():
4143

4244
context.end_subsegment()
4345
assert context.get_trace_entity().id == segment.id
46+
47+
48+
def test_disable():
49+
SDKConfig.set_sdk_enabled(False)
50+
segment = context.get_trace_entity()
51+
subsegment = Subsegment('name', 'local', segment)
52+
context.put_subsegment(subsegment)
53+
assert type(context.get_trace_entity()) is DummySubsegment

tests/test_patcher.py

Lines changed: 9 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.sdk_config import SDKConfig
1617
from aws_xray_sdk.core import patcher, xray_recorder
1718
from aws_xray_sdk.core.context import Context
1819

@@ -172,3 +173,11 @@ def test_external_submodules_ignores_module():
172173
assert xray_recorder.current_segment().subsegments[0].name == 'mock_init'
173174
assert xray_recorder.current_segment().subsegments[1].name == 'mock_func'
174175
assert xray_recorder.current_segment().subsegments[2].name == 'mock_no_doublepatch' # It is patched with decorator
176+
177+
178+
def test_disable_sdk_disables_patching():
179+
SDKConfig.set_sdk_enabled(False)
180+
patcher.patch(['tests.mock_module'])
181+
imported_modules = [module for module in TEST_MODULES if module in sys.modules]
182+
assert not imported_modules
183+
assert len(xray_recorder.current_segment().subsegments) == 0

tests/test_recorder.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
from aws_xray_sdk.version import VERSION
66
from .util import get_new_stubbed_recorder
77

8+
import os
9+
from aws_xray_sdk.sdk_config import SDKConfig
10+
from aws_xray_sdk.core.models.segment import Segment
11+
from aws_xray_sdk.core.models.subsegment import Subsegment
12+
from aws_xray_sdk.core.models.dummy_entities import DummySegment, DummySubsegment
13+
814
xray_recorder = get_new_stubbed_recorder()
15+
XRAY_ENABLED_KEY = SDKConfig.XRAY_ENABLED_KEY
916

1017

1118
@pytest.fixture(autouse=True)
@@ -166,3 +173,45 @@ def test_in_segment_exception():
166173
raise Exception('test exception')
167174

168175
assert len(subsegment.cause['exceptions']) == 1
176+
177+
178+
def test_default_enabled():
179+
segment = xray_recorder.begin_segment('name')
180+
subsegment = xray_recorder.begin_subsegment('name')
181+
assert type(xray_recorder.current_segment()) is Segment
182+
assert type(xray_recorder.current_subsegment()) is Subsegment
183+
184+
185+
def test_disable_is_dummy():
186+
xray_recorder.configure(enabled=False)
187+
segment = xray_recorder.begin_segment('name')
188+
subsegment = xray_recorder.begin_subsegment('name')
189+
assert type(xray_recorder.current_segment()) is DummySegment
190+
assert type(xray_recorder.current_subsegment()) is DummySubsegment
191+
192+
193+
def test_disable_env_precedence():
194+
os.environ[XRAY_ENABLED_KEY] = "False"
195+
xray_recorder.configure(enabled=True)
196+
segment = xray_recorder.begin_segment('name')
197+
subsegment = xray_recorder.begin_subsegment('name')
198+
assert type(xray_recorder.current_segment()) is DummySegment
199+
assert type(xray_recorder.current_subsegment()) is DummySubsegment
200+
201+
202+
def test_disable_env():
203+
os.environ[XRAY_ENABLED_KEY] = "False"
204+
xray_recorder.configure(enabled=False)
205+
segment = xray_recorder.begin_segment('name')
206+
subsegment = xray_recorder.begin_subsegment('name')
207+
assert type(xray_recorder.current_segment()) is DummySegment
208+
assert type(xray_recorder.current_subsegment()) is DummySubsegment
209+
210+
211+
def test_enable_env():
212+
os.environ[XRAY_ENABLED_KEY] = "True"
213+
xray_recorder.configure(enabled=True)
214+
segment = xray_recorder.begin_segment('name')
215+
subsegment = xray_recorder.begin_subsegment('name')
216+
assert type(xray_recorder.current_segment()) is Segment
217+
assert type(xray_recorder.current_subsegment()) is Subsegment

0 commit comments

Comments
 (0)