Skip to content

Commit 95bd2c8

Browse files
committed
Add Serverless Framework Support (#2)
* Serverless architecture in this case includes one that utilizes Lambda and API Gateway. * A new "Serverless" context is created to give the abstraction of Segments being the toplevel entities but is then converted to a subsegment upon transmission to the data plane. These segments are called MimicSegments. All generated segments have a parent segment that is the FacadeSegment. * Currently supports Flask and Django as middlewares; this has been confirmed to be natively working with Zappa if the application is running under Flask/Django.
1 parent ae06e28 commit 95bd2c8

File tree

8 files changed

+476
-1
lines changed

8 files changed

+476
-1
lines changed

aws_xray_sdk/core/exceptions/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class FacadeSegmentMutationException(Exception):
2222
pass
2323

2424

25+
class MimicSegmentInvalidException(Exception):
26+
pass
27+
28+
2529
class MissingPluginNames(Exception):
2630
pass
2731

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from .segment import Segment
2+
from ..exceptions.exceptions import MimicSegmentInvalidException
3+
4+
5+
class MimicSegment(Segment):
6+
"""
7+
The MimicSegment is an entity that mimics a segment for the use of the serverless context.
8+
When the MimicSegment is generated, its parent segment is assigned to be the FacadeSegment
9+
generated by the Lambda Environment. Upon serialization and transmission of the MimicSegment,
10+
it is converted to a locally-namespaced, subsegment. This is only done during serialization.
11+
All Segment-related method calls done on this object are valid.
12+
13+
Subsegments are automatically created with the namespace "local" to prevent it from appearing
14+
as a node on the service graph. For all purposes, the MimicSegment can be interacted as if it's
15+
a real segment, meaning that all methods that exist only in a Segment but not a subsegment
16+
is available to be used.
17+
"""
18+
19+
def __init__(self, facade_segment=None, original_segment=None):
20+
if not original_segment or not facade_segment:
21+
raise MimicSegmentInvalidException("Invalid MimicSegment construction. "
22+
"Please put in the original segment and the facade segment.")
23+
super(MimicSegment, self).__init__(name=original_segment.name, entityid=original_segment.id,
24+
traceid=facade_segment.trace_id, parent_id=facade_segment.id,
25+
sampled=facade_segment.sampled)
26+
27+
def __getstate__(self):
28+
"""
29+
Used during serialization. We mark the subsegment properties to let the dataplane know
30+
that we want the mimic segment to be represented as a subsegment.
31+
"""
32+
properties = super(MimicSegment, self).__getstate__()
33+
properties['type'] = 'subsegment'
34+
properties['namespace'] = 'local'
35+
return properties

aws_xray_sdk/core/recorder.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ def begin_segment(self, name=None, traceid=None,
243243
self._populate_runtime_context(segment, decision)
244244

245245
self.context.put_segment(segment)
246-
return segment
246+
current_segment = self.get_trace_entity()
247+
return current_segment
247248

248249
def end_segment(self, end_time=None):
249250
"""
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import os
2+
import logging
3+
4+
from .models.facade_segment import FacadeSegment
5+
from .models.segment import Segment
6+
from .models.mimic_segment import MimicSegment
7+
from .context import CXT_MISSING_STRATEGY_KEY
8+
from .lambda_launcher import LambdaContext
9+
from .context import Context
10+
11+
12+
log = logging.getLogger(__name__)
13+
14+
15+
class ServerlessContext(LambdaContext):
16+
"""
17+
Context used specifically for running middlewares on Lambda through the
18+
Serverless design. This context is built on top of the LambdaContext, but
19+
creates a Segment masked as a Subsegment known as a MimicSegment underneath
20+
the Lambda-generated Facade Segment. This ensures that middleware->recorder's
21+
consequent calls to "put_segment()" will not throw exceptions but instead create
22+
subsegments underneath the lambda-generated segment. This context also
23+
ensures that FacadeSegments exist through underlying calls to _refresh_context().
24+
"""
25+
def __init__(self, context_missing='RUNTIME_ERROR'):
26+
super(ServerlessContext, self).__init__()
27+
28+
strategy = os.getenv(CXT_MISSING_STRATEGY_KEY, context_missing)
29+
self._context_missing = strategy
30+
31+
def put_segment(self, segment):
32+
"""
33+
Convert the segment into a mimic segment and append it to FacadeSegment's subsegment list.
34+
:param Segment segment:
35+
:return:
36+
"""
37+
# When putting a segment, convert it to a mimic segment and make it a child of the Facade Segment.
38+
parent_facade_segment = self.__get_facade_entity() # type: FacadeSegment
39+
mimic_segment = MimicSegment(parent_facade_segment, segment)
40+
parent_facade_segment.add_subsegment(mimic_segment)
41+
Context.put_segment(self, mimic_segment)
42+
43+
def end_segment(self, end_time=None):
44+
"""
45+
Close the MimicSegment
46+
"""
47+
# Close the last mimic segment opened then remove it from our facade segment.
48+
mimic_segment = self.get_trace_entity()
49+
Context.end_segment(self, end_time)
50+
if type(mimic_segment) == MimicSegment:
51+
# The facade segment can only hold mimic segments.
52+
facade_segment = self.__get_facade_entity()
53+
facade_segment.remove_subsegment(mimic_segment)
54+
55+
def put_subsegment(self, subsegment):
56+
"""
57+
Appends the subsegment as a subsegment of either the mimic segment or
58+
another subsegment if they are the last opened entity.
59+
:param subsegment: The subsegment to to be added as a subsegment.
60+
"""
61+
Context.put_subsegment(self, subsegment)
62+
63+
def end_subsegment(self, end_time=None):
64+
"""
65+
End the current subsegment. In our case, subsegments
66+
will either be a subsegment of a mimic segment or another
67+
subsegment.
68+
:param int end_time: epoch in seconds. If not specified the current
69+
system time will be used.
70+
:return: True on success, false if no parent mimic segment/subsegment is found.
71+
"""
72+
return Context.end_subsegment(self, end_time)
73+
74+
def __get_facade_entity(self):
75+
"""
76+
Retrieves the Facade segment from thread local. This facade segment should always be present
77+
because it was generated by the Lambda Container.
78+
:return: FacadeSegment
79+
"""
80+
self._refresh_context()
81+
facade_segment = self._local.segment # type: FacadeSegment
82+
return facade_segment
83+
84+
def get_trace_entity(self):
85+
"""
86+
Return the latest entity added. In this case, it'll either be a Mimic Segment or
87+
a subsegment. Facade Segments are never returned.
88+
If no mimic segments or subsegments were ever passed in, throw the default
89+
context missing error.
90+
:return: Entity
91+
"""
92+
# Call to Context.get_trace_entity() returns the latest mimic segment/subsegment if they exist.
93+
# Otherwise, returns None through the following way:
94+
# No mimic segment/subsegment exists so Context calls LambdaContext's handle_context_missing().
95+
# By default, Lambda's method returns no-op, so it will return None to ServerlessContext.
96+
# Take that None as an indication to return the rightful handle_context_missing(), otherwise
97+
# return the entity.
98+
entity = Context.get_trace_entity(self)
99+
if entity is None:
100+
return Context.handle_context_missing(self)
101+
else:
102+
return entity
103+
104+
def set_trace_entity(self, trace_entity):
105+
"""
106+
Store the input trace_entity to local context. It will overwrite all
107+
existing ones if there is any.
108+
"""
109+
if type(trace_entity) == Segment:
110+
# Convert to a mimic segment.
111+
parent_facade_segment = self.__get_facade_entity() # type: FacadeSegment
112+
converted_segment = MimicSegment(parent_facade_segment, trace_entity)
113+
mimic_segment = converted_segment
114+
else:
115+
# Should be a Mimic Segment. If it's a subsegment, grandparent Context's
116+
# behavior would be invoked.
117+
mimic_segment = trace_entity
118+
119+
Context.set_trace_entity(self, mimic_segment)
120+
self.__get_facade_entity().subsegments = [mimic_segment]
121+
122+
def _is_subsegment(self, entity):
123+
return super(ServerlessContext, self)._is_subsegment(entity) and type(entity) != MimicSegment
124+
125+
@property
126+
def context_missing(self):
127+
return self._context_missing
128+
129+
@context_missing.setter
130+
def context_missing(self, value):
131+
self._context_missing = value

aws_xray_sdk/ext/django/middleware.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22

33
from aws_xray_sdk.core import xray_recorder
4+
from aws_xray_sdk.core.lambda_launcher import check_in_lambda
45
from aws_xray_sdk.core.models import http
6+
from aws_xray_sdk.core.serverless_context import ServerlessContext
57
from aws_xray_sdk.core.utils import stacktrace
68
from aws_xray_sdk.ext.util import calculate_sampling_decision, \
79
calculate_segment_name, construct_xray_header, prepare_response_header
@@ -25,6 +27,11 @@ def __init__(self, get_response):
2527

2628
self.get_response = get_response
2729

30+
# The case when the middleware is initialized in a Lambda Context, we make sure
31+
# to use the ServerlessContext so that the middleware properly functions.
32+
if check_in_lambda() is not None:
33+
xray_recorder.context = ServerlessContext()
34+
2835
# hooks for django version >= 1.10
2936
def __call__(self, request):
3037

aws_xray_sdk/ext/flask/middleware.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import flask.templating
22
from flask import request
33

4+
from aws_xray_sdk.core.lambda_launcher import check_in_lambda
45
from aws_xray_sdk.core.models import http
6+
from aws_xray_sdk.core.serverless_context import ServerlessContext
57
from aws_xray_sdk.core.utils import stacktrace
68
from aws_xray_sdk.ext.util import calculate_sampling_decision, \
79
calculate_segment_name, construct_xray_header, prepare_response_header
@@ -18,6 +20,11 @@ def __init__(self, app, recorder):
1820
self.app.after_request(self._after_request)
1921
self.app.teardown_request(self._handle_exception)
2022

23+
# The case when the middleware is initialized in a Lambda Context, we make sure
24+
# to use the ServerlessContext so that the middleware properly functions.
25+
if check_in_lambda() is not None:
26+
self._recorder.context = ServerlessContext()
27+
2128
_patch_render(recorder)
2229

2330
def _before_request(self):

tests/test_mimic_segment.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import pytest
2+
3+
from aws_xray_sdk.core.models.facade_segment import FacadeSegment
4+
from aws_xray_sdk.core.models.segment import Segment
5+
from aws_xray_sdk.core.models.subsegment import Subsegment
6+
from aws_xray_sdk.core.models.mimic_segment import MimicSegment
7+
from aws_xray_sdk.core.exceptions.exceptions import MimicSegmentInvalidException
8+
9+
10+
original_segment = Segment("RealSegment")
11+
facade_segment = FacadeSegment("FacadeSegment", "entityid", "traceid", True)
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def cleanup_ctx():
16+
global original_segment, facade_segment
17+
original_segment = Segment("RealSegment")
18+
facade_segment = FacadeSegment("FacadeSegment", "entityid", "traceid", True)
19+
yield
20+
original_segment = Segment("RealSegment")
21+
facade_segment = FacadeSegment("FacadeSegment", "entityid", "traceid", True)
22+
23+
24+
def test_ready():
25+
mimic_segment = MimicSegment(facade_segment=facade_segment, original_segment=original_segment)
26+
mimic_segment.in_progress = False
27+
assert mimic_segment.ready_to_send()
28+
29+
30+
def test_invalid_init():
31+
with pytest.raises(MimicSegmentInvalidException):
32+
MimicSegment(facade_segment=None, original_segment=original_segment)
33+
MimicSegment(facade_segment=facade_segment, original_segment=None)
34+
MimicSegment(facade_segment=Subsegment("Test", "local", original_segment), original_segment=None)
35+
MimicSegment(facade_segment=None, original_segment=Subsegment("Test", "local", original_segment))
36+
37+
38+
def test_init_similar():
39+
mimic_segment = MimicSegment(facade_segment=facade_segment, original_segment=original_segment) # type: MimicSegment
40+
41+
assert mimic_segment.id == original_segment.id
42+
assert mimic_segment.name == original_segment.name
43+
assert mimic_segment.in_progress == original_segment.in_progress
44+
45+
assert mimic_segment.trace_id == facade_segment.trace_id
46+
assert mimic_segment.parent_id == facade_segment.id
47+
assert mimic_segment.sampled == facade_segment.sampled
48+
49+
mimic_segment_serialized = mimic_segment.__getstate__()
50+
assert mimic_segment_serialized['namespace'] == "local"
51+
assert mimic_segment_serialized['type'] == "subsegment"
52+
53+
54+
def test_facade_segment_properties():
55+
# Sampling decision is made by Facade Segment
56+
original_segment.sampled = False
57+
facade_segment.sampled = True
58+
mimic_segment = MimicSegment(facade_segment=facade_segment, original_segment=original_segment) # type: MimicSegment
59+
60+
assert mimic_segment.sampled == facade_segment.sampled
61+
assert mimic_segment.sampled != original_segment.sampled
62+
63+
64+
def test_segment_methods_on_mimic():
65+
# Test to make sure that segment methods exist and function for the Mimic Segment
66+
mimic_segment = MimicSegment(facade_segment=facade_segment, original_segment=original_segment) # type: MimicSegment
67+
assert not getattr(mimic_segment, "service", None)
68+
assert not getattr(mimic_segment, "user", None)
69+
assert getattr(mimic_segment, "ref_counter", None)
70+
assert getattr(mimic_segment, "_subsegments_counter", None)
71+
72+
assert not getattr(original_segment, "service", None)
73+
assert not getattr(original_segment, "user", None)
74+
assert getattr(original_segment, "ref_counter", None)
75+
assert getattr(original_segment, "_subsegments_counter", None)
76+
77+
mimic_segment.set_service("SomeService")
78+
original_segment.set_service("SomeService")
79+
assert original_segment.service == original_segment.service
80+
81+
assert original_segment.get_origin_trace_header() == mimic_segment.get_origin_trace_header()
82+
mimic_segment.save_origin_trace_header("someheader")
83+
original_segment.save_origin_trace_header("someheader")
84+
assert original_segment.get_origin_trace_header() == mimic_segment.get_origin_trace_header()
85+
86+
# No exception is thrown
87+
test_dict = {"akey": "avalue"}
88+
original_segment.set_aws(test_dict)
89+
original_segment.set_rule_name(test_dict)
90+
original_segment.set_user("SomeUser")
91+
mimic_segment.set_aws(test_dict)
92+
mimic_segment.set_rule_name(test_dict)
93+
mimic_segment.set_user("SomeUser")

0 commit comments

Comments
 (0)