Skip to content

Commit a7ef7c0

Browse files
authored
feat(tracing): Add sampling context from AWS and GCP (getsentry#916)
1 parent 0661bce commit a7ef7c0

File tree

7 files changed

+359
-65
lines changed

7 files changed

+359
-65
lines changed

sentry_sdk/_compat.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Tuple
88
from typing import Any
99
from typing import Type
10-
1110
from typing import TypeVar
1211

1312
T = TypeVar("T")

sentry_sdk/integrations/aws_lambda.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def sentry_init_error(*args, **kwargs):
6565

6666
def _wrap_handler(handler):
6767
# type: (F) -> F
68-
def sentry_handler(aws_event, context, *args, **kwargs):
68+
def sentry_handler(aws_event, aws_context, *args, **kwargs):
6969
# type: (Any, Any, *Any, **Any) -> Any
7070

7171
# Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html,
@@ -94,21 +94,23 @@ def sentry_handler(aws_event, context, *args, **kwargs):
9494
hub = Hub.current
9595
integration = hub.get_integration(AwsLambdaIntegration)
9696
if integration is None:
97-
return handler(aws_event, context, *args, **kwargs)
97+
return handler(aws_event, aws_context, *args, **kwargs)
9898

9999
# If an integration is there, a client has to be there.
100100
client = hub.client # type: Any
101-
configured_time = context.get_remaining_time_in_millis()
101+
configured_time = aws_context.get_remaining_time_in_millis()
102102

103103
with hub.push_scope() as scope:
104104
with capture_internal_exceptions():
105105
scope.clear_breadcrumbs()
106106
scope.add_event_processor(
107107
_make_request_event_processor(
108-
request_data, context, configured_time
108+
request_data, aws_context, configured_time
109109
)
110110
)
111-
scope.set_tag("aws_region", context.invoked_function_arn.split(":")[3])
111+
scope.set_tag(
112+
"aws_region", aws_context.invoked_function_arn.split(":")[3]
113+
)
112114
if batch_size > 1:
113115
scope.set_tag("batch_request", True)
114116
scope.set_tag("batch_size", batch_size)
@@ -134,11 +136,17 @@ def sentry_handler(aws_event, context, *args, **kwargs):
134136

135137
headers = request_data.get("headers", {})
136138
transaction = Transaction.continue_from_headers(
137-
headers, op="serverless.function", name=context.function_name
139+
headers, op="serverless.function", name=aws_context.function_name
138140
)
139-
with hub.start_transaction(transaction):
141+
with hub.start_transaction(
142+
transaction,
143+
custom_sampling_context={
144+
"aws_event": aws_event,
145+
"aws_context": aws_context,
146+
},
147+
):
140148
try:
141-
return handler(aws_event, context, *args, **kwargs)
149+
return handler(aws_event, aws_context, *args, **kwargs)
142150
except Exception:
143151
exc_info = sys.exc_info()
144152
sentry_event, hint = event_from_exception(
@@ -177,23 +185,8 @@ def __init__(self, timeout_warning=False):
177185
def setup_once():
178186
# type: () -> None
179187

180-
# Python 2.7: Everything is in `__main__`.
181-
#
182-
# Python 3.7: If the bootstrap module is *already imported*, it is the
183-
# one we actually want to use (no idea what's in __main__)
184-
#
185-
# On Python 3.8 bootstrap is also importable, but will be the same file
186-
# as __main__ imported under a different name:
187-
#
188-
# sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__
189-
# sys.modules['__main__'] is not sys.modules['bootstrap']
190-
#
191-
# Such a setup would then make all monkeypatches useless.
192-
if "bootstrap" in sys.modules:
193-
lambda_bootstrap = sys.modules["bootstrap"] # type: Any
194-
elif "__main__" in sys.modules:
195-
lambda_bootstrap = sys.modules["__main__"]
196-
else:
188+
lambda_bootstrap = get_lambda_bootstrap()
189+
if not lambda_bootstrap:
197190
logger.warning(
198191
"Not running in AWS Lambda environment, "
199192
"AwsLambdaIntegration disabled (could not find bootstrap module)"
@@ -280,6 +273,29 @@ def inner(*args, **kwargs):
280273
)
281274

282275

276+
def get_lambda_bootstrap():
277+
# type: () -> Optional[Any]
278+
279+
# Python 2.7: Everything is in `__main__`.
280+
#
281+
# Python 3.7: If the bootstrap module is *already imported*, it is the
282+
# one we actually want to use (no idea what's in __main__)
283+
#
284+
# On Python 3.8 bootstrap is also importable, but will be the same file
285+
# as __main__ imported under a different name:
286+
#
287+
# sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__
288+
# sys.modules['__main__'] is not sys.modules['bootstrap']
289+
#
290+
# Such a setup would then make all monkeypatches useless.
291+
if "bootstrap" in sys.modules:
292+
return sys.modules["bootstrap"]
293+
elif "__main__" in sys.modules:
294+
return sys.modules["__main__"]
295+
else:
296+
return None
297+
298+
283299
def _make_request_event_processor(aws_event, aws_context, configured_timeout):
284300
# type: (Any, Any, Any) -> EventProcessor
285301
start_time = datetime.utcnow()

sentry_sdk/integrations/gcp.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434

3535
def _wrap_func(func):
3636
# type: (F) -> F
37-
def sentry_func(functionhandler, event, *args, **kwargs):
37+
def sentry_func(functionhandler, gcp_event, *args, **kwargs):
3838
# type: (Any, Any, *Any, **Any) -> Any
3939

4040
hub = Hub.current
4141
integration = hub.get_integration(GcpIntegration)
4242
if integration is None:
43-
return func(functionhandler, event, *args, **kwargs)
43+
return func(functionhandler, gcp_event, *args, **kwargs)
4444

4545
# If an integration is there, a client has to be there.
4646
client = hub.client # type: Any
@@ -50,7 +50,7 @@ def sentry_func(functionhandler, event, *args, **kwargs):
5050
logger.debug(
5151
"The configured timeout could not be fetched from Cloud Functions configuration."
5252
)
53-
return func(functionhandler, event, *args, **kwargs)
53+
return func(functionhandler, gcp_event, *args, **kwargs)
5454

5555
configured_time = int(configured_time)
5656

@@ -60,7 +60,9 @@ def sentry_func(functionhandler, event, *args, **kwargs):
6060
with capture_internal_exceptions():
6161
scope.clear_breadcrumbs()
6262
scope.add_event_processor(
63-
_make_request_event_processor(event, configured_time, initial_time)
63+
_make_request_event_processor(
64+
gcp_event, configured_time, initial_time
65+
)
6466
)
6567
scope.set_tag("gcp_region", environ.get("FUNCTION_REGION"))
6668
timeout_thread = None
@@ -76,22 +78,34 @@ def sentry_func(functionhandler, event, *args, **kwargs):
7678
timeout_thread.start()
7779

7880
headers = {}
79-
if hasattr(event, "headers"):
80-
headers = event.headers
81+
if hasattr(gcp_event, "headers"):
82+
headers = gcp_event.headers
8183
transaction = Transaction.continue_from_headers(
8284
headers, op="serverless.function", name=environ.get("FUNCTION_NAME", "")
8385
)
84-
with hub.start_transaction(transaction):
86+
sampling_context = {
87+
"gcp_env": {
88+
"function_name": environ.get("FUNCTION_NAME"),
89+
"function_entry_point": environ.get("ENTRY_POINT"),
90+
"function_identity": environ.get("FUNCTION_IDENTITY"),
91+
"function_region": environ.get("FUNCTION_REGION"),
92+
"function_project": environ.get("GCP_PROJECT"),
93+
},
94+
"gcp_event": gcp_event,
95+
}
96+
with hub.start_transaction(
97+
transaction, custom_sampling_context=sampling_context
98+
):
8599
try:
86-
return func(functionhandler, event, *args, **kwargs)
100+
return func(functionhandler, gcp_event, *args, **kwargs)
87101
except Exception:
88102
exc_info = sys.exc_info()
89-
event, hint = event_from_exception(
103+
sentry_event, hint = event_from_exception(
90104
exc_info,
91105
client_options=client.options,
92106
mechanism={"type": "gcp", "handled": False},
93107
)
94-
hub.capture_event(event, hint=hint)
108+
hub.capture_event(sentry_event, hint=hint)
95109
reraise(*exc_info)
96110
finally:
97111
if timeout_thread:

tests/conftest.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -355,18 +355,60 @@ class StringContaining(object):
355355
def __init__(self, substring):
356356
self.substring = substring
357357

358+
try:
359+
# unicode only exists in python 2
360+
self.valid_types = (str, unicode) # noqa
361+
except NameError:
362+
self.valid_types = (str,)
363+
358364
def __eq__(self, test_string):
359-
if not isinstance(test_string, str):
365+
if not isinstance(test_string, self.valid_types):
360366
return False
361367

362368
if len(self.substring) > len(test_string):
363369
return False
364370

365371
return self.substring in test_string
366372

373+
def __ne__(self, test_string):
374+
return not self.__eq__(test_string)
375+
367376
return StringContaining
368377

369378

379+
def _safe_is_equal(x, y):
380+
"""
381+
Compares two values, preferring to use the first's __eq__ method if it
382+
exists and is implemented.
383+
384+
Accounts for py2/py3 differences (like ints in py2 not having a __eq__
385+
method), as well as the incomparability of certain types exposed by using
386+
raw __eq__ () rather than ==.
387+
"""
388+
389+
# Prefer using __eq__ directly to ensure that examples like
390+
#
391+
# maisey = Dog()
392+
# maisey.name = "Maisey the Dog"
393+
# maisey == ObjectDescribedBy(attrs={"name": StringContaining("Maisey")})
394+
#
395+
# evaluate to True (in other words, examples where the values in self.attrs
396+
# might also have custom __eq__ methods; this makes sure those methods get
397+
# used if possible)
398+
try:
399+
is_equal = x.__eq__(y)
400+
except AttributeError:
401+
is_equal = NotImplemented
402+
403+
# this can happen on its own, too (i.e. without an AttributeError being
404+
# thrown), which is why this is separate from the except block above
405+
if is_equal == NotImplemented:
406+
# using == smoothes out weird variations exposed by raw __eq__
407+
return x == y
408+
409+
return is_equal
410+
411+
370412
@pytest.fixture(name="DictionaryContaining")
371413
def dictionary_containing_matcher():
372414
"""
@@ -397,13 +439,19 @@ def __eq__(self, test_dict):
397439
if len(self.subdict) > len(test_dict):
398440
return False
399441

400-
# Have to test self == other (rather than vice-versa) in case
401-
# any of the values in self.subdict is another matcher with a custom
402-
# __eq__ method (in LHS == RHS, LHS's __eq__ is tried before RHS's).
403-
# In other words, this order is important so that examples like
404-
# {"dogs": "are great"} == DictionaryContaining({"dogs": StringContaining("great")})
405-
# evaluate to True
406-
return all(self.subdict[key] == test_dict.get(key) for key in self.subdict)
442+
for key, value in self.subdict.items():
443+
try:
444+
test_value = test_dict[key]
445+
except KeyError: # missing key
446+
return False
447+
448+
if not _safe_is_equal(value, test_value):
449+
return False
450+
451+
return True
452+
453+
def __ne__(self, test_dict):
454+
return not self.__eq__(test_dict)
407455

408456
return DictionaryContaining
409457

@@ -442,19 +490,19 @@ def __eq__(self, test_obj):
442490
if not isinstance(test_obj, self.type):
443491
return False
444492

445-
# all checks here done with getattr rather than comparing to
446-
# __dict__ because __dict__ isn't guaranteed to exist
447493
if self.attrs:
448-
# attributes must exist AND values must match
449-
try:
450-
if any(
451-
getattr(test_obj, attr_name) != attr_value
452-
for attr_name, attr_value in self.attrs.items()
453-
):
454-
return False # wrong attribute value
455-
except AttributeError: # missing attribute
456-
return False
494+
for attr_name, attr_value in self.attrs.items():
495+
try:
496+
test_value = getattr(test_obj, attr_name)
497+
except AttributeError: # missing attribute
498+
return False
499+
500+
if not _safe_is_equal(attr_value, test_value):
501+
return False
457502

458503
return True
459504

505+
def __ne__(self, test_obj):
506+
return not self.__eq__(test_obj)
507+
460508
return ObjectDescribedBy

tests/integrations/aws_lambda/client.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ def run_lambda_function(
4949
**subprocess_kwargs
5050
)
5151

52+
subprocess.check_call(
53+
"pip install mock==3.0.0 funcsigs -t .",
54+
cwd=tmpdir,
55+
shell=True,
56+
**subprocess_kwargs
57+
)
58+
5259
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
5360
subprocess.check_call(
5461
"pip install ../*.tar.gz -t .", cwd=tmpdir, shell=True, **subprocess_kwargs
@@ -69,9 +76,19 @@ def run_lambda_function(
6976
)
7077

7178
@add_finalizer
72-
def delete_function():
79+
def clean_up():
7380
client.delete_function(FunctionName=fn_name)
7481

82+
# this closes the web socket so we don't get a
83+
# ResourceWarning: unclosed <ssl.SSLSocket ... >
84+
# warning on every test
85+
# based on https://github.com/boto/botocore/pull/1810
86+
# (if that's ever merged, this can just become client.close())
87+
session = client._endpoint.http_session
88+
managers = [session._manager] + list(session._proxy_managers.values())
89+
for manager in managers:
90+
manager.clear()
91+
7592
response = client.invoke(
7693
FunctionName=fn_name,
7794
InvocationType="RequestResponse",

0 commit comments

Comments
 (0)