Skip to content

Commit 3f206c2

Browse files
Gleekzonesentry-botuntitaker
authored
feat: Integration for Chalice (getsentry#779)
Co-authored-by: sentry-bot <markus+ghbot@sentry.io> Co-authored-by: Markus Unterwaditzer <markus-honeypot@unterwaditzer.net>
1 parent a5883a3 commit 3f206c2

File tree

5 files changed

+229
-0
lines changed

5 files changed

+229
-0
lines changed

sentry_sdk/integrations/chalice.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import sys
2+
3+
from sentry_sdk._compat import reraise
4+
from sentry_sdk.hub import Hub
5+
from sentry_sdk.integrations import Integration
6+
from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
7+
from sentry_sdk.utils import (
8+
capture_internal_exceptions,
9+
event_from_exception,
10+
)
11+
from sentry_sdk._types import MYPY
12+
from sentry_sdk._functools import wraps
13+
14+
import chalice # type: ignore
15+
from chalice import Chalice, ChaliceViewError
16+
from chalice.app import EventSourceHandler as ChaliceEventSourceHandler # type: ignore
17+
18+
if MYPY:
19+
from typing import Any
20+
from typing import TypeVar
21+
from typing import Callable
22+
23+
F = TypeVar("F", bound=Callable[..., Any])
24+
25+
26+
class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore
27+
def __call__(self, event, context):
28+
# type: (Any, Any) -> Any
29+
hub = Hub.current
30+
client = hub.client # type: Any
31+
32+
with hub.push_scope() as scope:
33+
with capture_internal_exceptions():
34+
configured_time = context.get_remaining_time_in_millis()
35+
scope.add_event_processor(
36+
_make_request_event_processor(event, context, configured_time)
37+
)
38+
try:
39+
event_obj = self.event_class(event, context)
40+
return self.func(event_obj)
41+
except Exception:
42+
exc_info = sys.exc_info()
43+
event, hint = event_from_exception(
44+
exc_info,
45+
client_options=client.options,
46+
mechanism={"type": "chalice", "handled": False},
47+
)
48+
hub.capture_event(event, hint=hint)
49+
hub.flush()
50+
reraise(*exc_info)
51+
52+
53+
def _get_view_function_response(app, view_function, function_args):
54+
# type: (Any, F, Any) -> F
55+
@wraps(view_function)
56+
def wrapped_view_function(**function_args):
57+
# type: (**Any) -> Any
58+
hub = Hub.current
59+
client = hub.client # type: Any
60+
with hub.push_scope() as scope:
61+
with capture_internal_exceptions():
62+
configured_time = app.lambda_context.get_remaining_time_in_millis()
63+
scope.transaction = app.lambda_context.function_name
64+
scope.add_event_processor(
65+
_make_request_event_processor(
66+
app.current_request.to_dict(),
67+
app.lambda_context,
68+
configured_time,
69+
)
70+
)
71+
try:
72+
return view_function(**function_args)
73+
except Exception as exc:
74+
if isinstance(exc, ChaliceViewError):
75+
raise
76+
exc_info = sys.exc_info()
77+
event, hint = event_from_exception(
78+
exc_info,
79+
client_options=client.options,
80+
mechanism={"type": "chalice", "handled": False},
81+
)
82+
hub.capture_event(event, hint=hint)
83+
hub.flush()
84+
raise
85+
86+
return wrapped_view_function # type: ignore
87+
88+
89+
class ChaliceIntegration(Integration):
90+
identifier = "chalice"
91+
92+
@staticmethod
93+
def setup_once():
94+
# type: () -> None
95+
old_get_view_function_response = Chalice._get_view_function_response
96+
97+
def sentry_event_response(app, view_function, function_args):
98+
# type: (Any, F, **Any) -> Any
99+
wrapped_view_function = _get_view_function_response(
100+
app, view_function, function_args
101+
)
102+
103+
return old_get_view_function_response(
104+
app, wrapped_view_function, function_args
105+
)
106+
107+
Chalice._get_view_function_response = sentry_event_response
108+
# for everything else (like events)
109+
chalice.app.EventSourceHandler = EventSourceHandler

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"sqlalchemy": ["sqlalchemy>=1.2"],
3939
"pyspark": ["pyspark>=2.4.4"],
4040
"pure_eval": ["pure_eval", "executing", "asttokens"],
41+
"chalice": ["chalice>=1.16.0"],
4142
},
4243
classifiers=[
4344
"Development Status :: 5 - Production/Stable",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pytest.importorskip("chalice")
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
import time
3+
from chalice import Chalice, BadRequestError
4+
from chalice.local import LambdaContext, LocalGateway
5+
6+
from sentry_sdk.integrations.chalice import ChaliceIntegration
7+
8+
from pytest_chalice.handlers import RequestHandler
9+
10+
11+
def _generate_lambda_context(self):
12+
# Monkeypatch of the function _generate_lambda_context
13+
# from the class LocalGateway
14+
# for mock the timeout
15+
# type: () -> LambdaContext
16+
if self._config.lambda_timeout is None:
17+
timeout = 10 * 1000
18+
else:
19+
timeout = self._config.lambda_timeout * 1000
20+
return LambdaContext(
21+
function_name=self._config.function_name,
22+
memory_size=self._config.lambda_memory_size,
23+
max_runtime_ms=timeout,
24+
)
25+
26+
27+
@pytest.fixture
28+
def app(sentry_init):
29+
sentry_init(integrations=[ChaliceIntegration()])
30+
app = Chalice(app_name="sentry_chalice")
31+
32+
@app.route("/boom")
33+
def boom():
34+
raise Exception("boom goes the dynamite!")
35+
36+
@app.route("/context")
37+
def has_request():
38+
raise Exception("boom goes the dynamite!")
39+
40+
@app.route("/badrequest")
41+
def badrequest():
42+
raise BadRequestError("bad-request")
43+
44+
LocalGateway._generate_lambda_context = _generate_lambda_context
45+
46+
return app
47+
48+
49+
@pytest.fixture
50+
def lambda_context_args():
51+
return ["lambda_name", 256]
52+
53+
54+
def test_exception_boom(app, client: RequestHandler) -> None:
55+
response = client.get("/boom")
56+
assert response.status_code == 500
57+
assert response.json == dict(
58+
[
59+
("Code", "InternalServerError"),
60+
("Message", "An internal server error occurred."),
61+
]
62+
)
63+
64+
65+
def test_has_request(app, capture_events, client: RequestHandler):
66+
events = capture_events()
67+
68+
response = client.get("/context")
69+
assert response.status_code == 500
70+
71+
(event,) = events
72+
assert event["level"] == "error"
73+
(exception,) = event["exception"]["values"]
74+
assert exception["type"] == "Exception"
75+
76+
77+
def test_scheduled_event(app, lambda_context_args):
78+
@app.schedule("rate(1 minutes)")
79+
def every_hour(event):
80+
raise Exception("schedule event!")
81+
82+
context = LambdaContext(
83+
*lambda_context_args, max_runtime_ms=10000, time_source=time
84+
)
85+
86+
lambda_event = {
87+
"version": "0",
88+
"account": "120987654312",
89+
"region": "us-west-1",
90+
"detail": {},
91+
"detail-type": "Scheduled Event",
92+
"source": "aws.events",
93+
"time": "1970-01-01T00:00:00Z",
94+
"id": "event-id",
95+
"resources": ["arn:aws:events:us-west-1:120987654312:rule/my-schedule"],
96+
}
97+
with pytest.raises(Exception) as exc_info:
98+
every_hour(lambda_event, context=context)
99+
assert str(exc_info.value) == "schedule event!"
100+
101+
102+
def test_bad_reques(client: RequestHandler) -> None:
103+
response = client.get("/badrequest")
104+
105+
assert response.status_code == 400
106+
assert response.json == dict(
107+
[
108+
("Code", "BadRequestError"),
109+
("Message", "BadRequestError: bad-request"),
110+
]
111+
)

tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ envlist =
7575

7676
{py3.5,py3.6,py3.7,py3.8,py3.9}-pure_eval
7777

78+
{py3.6,py3.7,py3.8}-chalice
79+
7880
[testenv]
7981
deps =
8082
-r test-requirements.txt
@@ -194,6 +196,8 @@ deps =
194196
py3.8: hypothesis
195197

196198
pure_eval: pure_eval
199+
chalice: chalice>=1.16.0
200+
chalice: pytest-chalice==0.0.5
197201

198202
setenv =
199203
PYTHONDONTWRITEBYTECODE=1
@@ -219,6 +223,7 @@ setenv =
219223
sqlalchemy: TESTPATH=tests/integrations/sqlalchemy
220224
spark: TESTPATH=tests/integrations/spark
221225
pure_eval: TESTPATH=tests/integrations/pure_eval
226+
chalice: TESTPATH=tests/integrations/chalice
222227

223228
COVERAGE_FILE=.coverage-{envname}
224229
passenv =

0 commit comments

Comments
 (0)