Skip to content

Commit cdf21de

Browse files
shantanu73Shantanu  Dhimanuntitaker
authored
Capturing Performance monitoring transactions for AWS and GCP (getsentry#830)
Co-authored-by: Shantanu Dhiman <shantanu.dhiman@calsoftinc.com> Co-authored-by: Markus Unterwaditzer <markus@unterwaditzer.net> Co-authored-by: Markus Unterwaditzer <markus-honeypot@unterwaditzer.net>
1 parent 7022cd8 commit cdf21de

File tree

4 files changed

+252
-63
lines changed

4 files changed

+252
-63
lines changed

sentry_sdk/integrations/aws_lambda.py

+26-17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44

55
from sentry_sdk.hub import Hub, _should_send_default_pii
6+
from sentry_sdk.tracing import Transaction
67
from sentry_sdk._compat import reraise
78
from sentry_sdk.utils import (
89
AnnotatedValue,
@@ -78,10 +79,10 @@ def sentry_handler(event, context, *args, **kwargs):
7879
with hub.push_scope() as scope:
7980
with capture_internal_exceptions():
8081
scope.clear_breadcrumbs()
81-
scope.transaction = context.function_name
8282
scope.add_event_processor(
8383
_make_request_event_processor(event, context, configured_time)
8484
)
85+
scope.set_tag("aws_region", context.invoked_function_arn.split(":")[3])
8586
# Starting the Timeout thread only if the configured time is greater than Timeout warning
8687
# buffer and timeout_warning parameter is set True.
8788
if (
@@ -99,17 +100,22 @@ def sentry_handler(event, context, *args, **kwargs):
99100
# Starting the thread to raise timeout warning exception
100101
timeout_thread.start()
101102

102-
try:
103-
return handler(event, context, *args, **kwargs)
104-
except Exception:
105-
exc_info = sys.exc_info()
106-
event, hint = event_from_exception(
107-
exc_info,
108-
client_options=client.options,
109-
mechanism={"type": "aws_lambda", "handled": False},
110-
)
111-
hub.capture_event(event, hint=hint)
112-
reraise(*exc_info)
103+
headers = event.get("headers", {})
104+
transaction = Transaction.continue_from_headers(
105+
headers, op="serverless.function", name=context.function_name
106+
)
107+
with hub.start_transaction(transaction):
108+
try:
109+
return handler(event, context, *args, **kwargs)
110+
except Exception:
111+
exc_info = sys.exc_info()
112+
event, hint = event_from_exception(
113+
exc_info,
114+
client_options=client.options,
115+
mechanism={"type": "aws_lambda", "handled": False},
116+
)
117+
hub.capture_event(event, hint=hint)
118+
reraise(*exc_info)
113119

114120
return sentry_handler # type: ignore
115121

@@ -277,11 +283,6 @@ def event_processor(event, hint, start_time=start_time):
277283
if "headers" in aws_event:
278284
request["headers"] = _filter_headers(aws_event["headers"])
279285

280-
if aws_event.get("body", None):
281-
# Unfortunately couldn't find a way to get structured body from AWS
282-
# event. Meaning every body is unstructured to us.
283-
request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]})
284-
285286
if _should_send_default_pii():
286287
user_info = event.setdefault("user", {})
287288

@@ -293,6 +294,14 @@ def event_processor(event, hint, start_time=start_time):
293294
if ip is not None:
294295
user_info.setdefault("ip_address", ip)
295296

297+
if "body" in aws_event:
298+
request["data"] = aws_event.get("body", "")
299+
else:
300+
if aws_event.get("body", None):
301+
# Unfortunately couldn't find a way to get structured body from AWS
302+
# event. Meaning every body is unstructured to us.
303+
request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]})
304+
296305
event["request"] = request
297306

298307
return event

sentry_sdk/integrations/gcp.py

+52-23
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22
from os import environ
33
import sys
44

5-
from sentry_sdk.hub import Hub
5+
from sentry_sdk.hub import Hub, _should_send_default_pii
6+
from sentry_sdk.tracing import Transaction
67
from sentry_sdk._compat import reraise
78
from sentry_sdk.utils import (
9+
AnnotatedValue,
810
capture_internal_exceptions,
911
event_from_exception,
1012
logger,
1113
TimeoutThread,
1214
)
1315
from sentry_sdk.integrations import Integration
16+
from sentry_sdk.integrations._wsgi_common import _filter_headers
1417

1518
from sentry_sdk._types import MYPY
1619

@@ -31,13 +34,13 @@
3134

3235
def _wrap_func(func):
3336
# type: (F) -> F
34-
def sentry_func(*args, **kwargs):
35-
# type: (*Any, **Any) -> Any
37+
def sentry_func(functionhandler, event, *args, **kwargs):
38+
# type: (Any, Any, *Any, **Any) -> Any
3639

3740
hub = Hub.current
3841
integration = hub.get_integration(GcpIntegration)
3942
if integration is None:
40-
return func(*args, **kwargs)
43+
return func(functionhandler, event, *args, **kwargs)
4144

4245
# If an integration is there, a client has to be there.
4346
client = hub.client # type: Any
@@ -47,7 +50,7 @@ def sentry_func(*args, **kwargs):
4750
logger.debug(
4851
"The configured timeout could not be fetched from Cloud Functions configuration."
4952
)
50-
return func(*args, **kwargs)
53+
return func(functionhandler, event, *args, **kwargs)
5154

5255
configured_time = int(configured_time)
5356

@@ -56,11 +59,10 @@ def sentry_func(*args, **kwargs):
5659
with hub.push_scope() as scope:
5760
with capture_internal_exceptions():
5861
scope.clear_breadcrumbs()
59-
scope.transaction = environ.get("FUNCTION_NAME")
6062
scope.add_event_processor(
61-
_make_request_event_processor(configured_time, initial_time)
63+
_make_request_event_processor(event, configured_time, initial_time)
6264
)
63-
try:
65+
scope.set_tag("gcp_region", environ.get("FUNCTION_REGION"))
6466
if (
6567
integration.timeout_warning
6668
and configured_time > TIMEOUT_WARNING_BUFFER
@@ -71,19 +73,28 @@ def sentry_func(*args, **kwargs):
7173

7274
# Starting the thread to raise timeout warning exception
7375
timeout_thread.start()
74-
return func(*args, **kwargs)
75-
except Exception:
76-
exc_info = sys.exc_info()
77-
event, hint = event_from_exception(
78-
exc_info,
79-
client_options=client.options,
80-
mechanism={"type": "gcp", "handled": False},
81-
)
82-
hub.capture_event(event, hint=hint)
83-
reraise(*exc_info)
84-
finally:
85-
# Flush out the event queue
86-
hub.flush()
76+
77+
headers = {}
78+
if hasattr(event, "headers"):
79+
headers = event.headers
80+
transaction = Transaction.continue_from_headers(
81+
headers, op="serverless.function", name=environ.get("FUNCTION_NAME", "")
82+
)
83+
with hub.start_transaction(transaction):
84+
try:
85+
return func(functionhandler, event, *args, **kwargs)
86+
except Exception:
87+
exc_info = sys.exc_info()
88+
event, hint = event_from_exception(
89+
exc_info,
90+
client_options=client.options,
91+
mechanism={"type": "gcp", "handled": False},
92+
)
93+
hub.capture_event(event, hint=hint)
94+
reraise(*exc_info)
95+
finally:
96+
# Flush out the event queue
97+
hub.flush()
8798

8899
return sentry_func # type: ignore
89100

@@ -113,8 +124,8 @@ def setup_once():
113124
)
114125

115126

116-
def _make_request_event_processor(configured_timeout, initial_time):
117-
# type: (Any, Any) -> EventProcessor
127+
def _make_request_event_processor(gcp_event, configured_timeout, initial_time):
128+
# type: (Any, Any, Any) -> EventProcessor
118129

119130
def event_processor(event, hint):
120131
# type: (Event, Hint) -> Optional[Event]
@@ -143,6 +154,24 @@ def event_processor(event, hint):
143154

144155
request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME"))
145156

157+
if hasattr(gcp_event, "method"):
158+
request["method"] = gcp_event.method
159+
160+
if hasattr(gcp_event, "query_string"):
161+
request["query_string"] = gcp_event.query_string.decode("utf-8")
162+
163+
if hasattr(gcp_event, "headers"):
164+
request["headers"] = _filter_headers(gcp_event.headers)
165+
166+
if _should_send_default_pii():
167+
if hasattr(gcp_event, "data"):
168+
request["data"] = gcp_event.data
169+
else:
170+
if hasattr(gcp_event, "data"):
171+
# Unfortunately couldn't find a way to get structured body from GCP
172+
# event. Meaning every body is unstructured to us.
173+
request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]})
174+
146175
event["request"] = request
147176

148177
return event

tests/integrations/aws_lambda/test_aws.py

+80-9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ def event_processor(event):
4040
# to print less to logs.
4141
return event
4242
43+
def envelope_processor(envelope):
44+
(item,) = envelope.items
45+
envelope_json = json.loads(item.get_bytes())
46+
47+
envelope_data = {}
48+
envelope_data[\"contexts\"] = {}
49+
envelope_data[\"type\"] = envelope_json[\"type\"]
50+
envelope_data[\"transaction\"] = envelope_json[\"transaction\"]
51+
envelope_data[\"contexts\"][\"trace\"] = envelope_json[\"contexts\"][\"trace\"]
52+
envelope_data[\"request\"] = envelope_json[\"request\"]
53+
54+
return envelope_data
55+
4356
class TestTransport(HttpTransport):
4457
def _send_event(self, event):
4558
event = event_processor(event)
@@ -49,6 +62,10 @@ def _send_event(self, event):
4962
# us one.
5063
print("\\nEVENT: {}\\n".format(json.dumps(event)))
5164
65+
def _send_envelope(self, envelope):
66+
envelope = envelope_processor(envelope)
67+
print("\\nENVELOPE: {}\\n".format(json.dumps(envelope)))
68+
5269
def init_sdk(timeout_warning=False, **extra_init_args):
5370
sentry_sdk.init(
5471
dsn="https://123abc@example.com/123",
@@ -91,21 +108,26 @@ def inner(code, payload, timeout=30, syntax_check=True):
91108
)
92109

93110
events = []
111+
envelopes = []
94112

95113
for line in base64.b64decode(response["LogResult"]).splitlines():
96114
print("AWS:", line)
97-
if not line.startswith(b"EVENT: "):
115+
if line.startswith(b"EVENT: "):
116+
line = line[len(b"EVENT: ") :]
117+
events.append(json.loads(line.decode("utf-8")))
118+
elif line.startswith(b"ENVELOPE: "):
119+
line = line[len(b"ENVELOPE: ") :]
120+
envelopes.append(json.loads(line.decode("utf-8")))
121+
else:
98122
continue
99-
line = line[len(b"EVENT: ") :]
100-
events.append(json.loads(line.decode("utf-8")))
101123

102-
return events, response
124+
return envelopes, events, response
103125

104126
return inner
105127

106128

107129
def test_basic(run_lambda_function):
108-
events, response = run_lambda_function(
130+
envelopes, events, response = run_lambda_function(
109131
LAMBDA_PRELUDE
110132
+ dedent(
111133
"""
@@ -160,7 +182,7 @@ def test_initialization_order(run_lambda_function):
160182
as seen by AWS already runs. At this point at least draining the queue
161183
should work."""
162184

163-
events, _response = run_lambda_function(
185+
envelopes, events, _response = run_lambda_function(
164186
LAMBDA_PRELUDE
165187
+ dedent(
166188
"""
@@ -180,7 +202,7 @@ def test_handler(event, context):
180202

181203

182204
def test_request_data(run_lambda_function):
183-
events, _response = run_lambda_function(
205+
envelopes, events, _response = run_lambda_function(
184206
LAMBDA_PRELUDE
185207
+ dedent(
186208
"""
@@ -235,7 +257,7 @@ def test_init_error(run_lambda_function, lambda_runtime):
235257
if lambda_runtime == "python2.7":
236258
pytest.skip("initialization error not supported on Python 2.7")
237259

238-
events, response = run_lambda_function(
260+
envelopes, events, response = run_lambda_function(
239261
LAMBDA_PRELUDE
240262
+ (
241263
"def event_processor(event):\n"
@@ -252,7 +274,7 @@ def test_init_error(run_lambda_function, lambda_runtime):
252274

253275

254276
def test_timeout_error(run_lambda_function):
255-
events, response = run_lambda_function(
277+
envelopes, events, response = run_lambda_function(
256278
LAMBDA_PRELUDE
257279
+ dedent(
258280
"""
@@ -291,3 +313,52 @@ def test_handler(event, context):
291313
log_stream = event["extra"]["cloudwatch logs"]["log_stream"]
292314

293315
assert re.match(log_stream_re, log_stream)
316+
317+
318+
def test_performance_no_error(run_lambda_function):
319+
envelopes, events, response = run_lambda_function(
320+
LAMBDA_PRELUDE
321+
+ dedent(
322+
"""
323+
init_sdk(traces_sample_rate=1.0)
324+
325+
def test_handler(event, context):
326+
return "test_string"
327+
"""
328+
),
329+
b'{"foo": "bar"}',
330+
)
331+
332+
(envelope,) = envelopes
333+
assert envelope["type"] == "transaction"
334+
assert envelope["contexts"]["trace"]["op"] == "serverless.function"
335+
assert envelope["transaction"].startswith("test_function_")
336+
assert envelope["transaction"] in envelope["request"]["url"]
337+
338+
339+
def test_performance_error(run_lambda_function):
340+
envelopes, events, response = run_lambda_function(
341+
LAMBDA_PRELUDE
342+
+ dedent(
343+
"""
344+
init_sdk(traces_sample_rate=1.0)
345+
346+
def test_handler(event, context):
347+
raise Exception("something went wrong")
348+
"""
349+
),
350+
b'{"foo": "bar"}',
351+
)
352+
353+
(event,) = events
354+
assert event["level"] == "error"
355+
(exception,) = event["exception"]["values"]
356+
assert exception["type"] == "Exception"
357+
assert exception["value"] == "something went wrong"
358+
359+
(envelope,) = envelopes
360+
361+
assert envelope["type"] == "transaction"
362+
assert envelope["contexts"]["trace"]["op"] == "serverless.function"
363+
assert envelope["transaction"].startswith("test_function_")
364+
assert envelope["transaction"] in envelope["request"]["url"]

0 commit comments

Comments
 (0)