Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chalice #1

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions sentry_sdk/integrations/chalice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import sys

from sentry_sdk._compat import reraise
from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
)
from sentry_sdk._types import MYPY
from sentry_sdk._functools import wraps

import chalice # type: ignore
from chalice import Chalice, ChaliceViewError
from chalice.app import EventSourceHandler as ChaliceEventSourceHandler # type: ignore

if MYPY:
from typing import Any
from typing import TypeVar
from typing import Callable

F = TypeVar("F", bound=Callable[..., Any])


class EventSourceHandler(ChaliceEventSourceHandler): # type: ignore
def __call__(self, event, context):
# type: (Any, Any) -> Any
hub = Hub.current
client = hub.client # type: Any

with hub.push_scope() as scope:
with capture_internal_exceptions():
configured_time = context.get_remaining_time_in_millis()
scope.add_event_processor(
_make_request_event_processor(event, context, configured_time)
)
try:
event_obj = self.event_class(event, context)
return self.func(event_obj)
except Exception:
exc_info = sys.exc_info()
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "chalice", "handled": False},
)
hub.capture_event(event, hint=hint)
hub.flush()
reraise(*exc_info)


def _get_view_function_response(app, view_function, function_args):
# type: (Any, F, Any) -> F
@wraps(view_function)
def wrapped_view_function(**function_args):
# type: (**Any) -> Any
hub = Hub.current
client = hub.client # type: Any
with hub.push_scope() as scope:
with capture_internal_exceptions():
configured_time = app.lambda_context.get_remaining_time_in_millis()
scope.transaction = app.lambda_context.function_name
scope.add_event_processor(
_make_request_event_processor(
app.current_request.to_dict(),
app.lambda_context,
configured_time,
)
)
try:
return view_function(**function_args)
except Exception as exc:
if isinstance(exc, ChaliceViewError):
raise
exc_info = sys.exc_info()
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "chalice", "handled": False},
)
hub.capture_event(event, hint=hint)
hub.flush()
raise

return wrapped_view_function # type: ignore


class ChaliceIntegration(Integration):
identifier = "chalice"

@staticmethod
def setup_once():
# type: () -> None
old_get_view_function_response = Chalice._get_view_function_response

def sentry_event_response(app, view_function, function_args):
# type: (Any, F, **Any) -> Any
wrapped_view_function = _get_view_function_response(
app, view_function, function_args
)

return old_get_view_function_response(
app, wrapped_view_function, function_args
)

Chalice._get_view_function_response = sentry_event_response
# for everything else (like events)
chalice.app.EventSourceHandler = EventSourceHandler
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"sqlalchemy": ["sqlalchemy>=1.2"],
"pyspark": ["pyspark>=2.4.4"],
"pure_eval": ["pure_eval", "executing", "asttokens"],
"chalice": ["chalice>=1.16.0"],
},
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
3 changes: 3 additions & 0 deletions tests/integrations/chalice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest

pytest.importorskip("chalice")
111 changes: 111 additions & 0 deletions tests/integrations/chalice/test_chalice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import pytest
import time
from chalice import Chalice, BadRequestError
from chalice.local import LambdaContext, LocalGateway

from sentry_sdk.integrations.chalice import ChaliceIntegration

from pytest_chalice.handlers import RequestHandler


def _generate_lambda_context(self):
# Monkeypatch of the function _generate_lambda_context
# from the class LocalGateway
# for mock the timeout
# type: () -> LambdaContext
if self._config.lambda_timeout is None:
timeout = 10 * 1000
else:
timeout = self._config.lambda_timeout * 1000
return LambdaContext(
function_name=self._config.function_name,
memory_size=self._config.lambda_memory_size,
max_runtime_ms=timeout,
)


@pytest.fixture
def app(sentry_init):
sentry_init(integrations=[ChaliceIntegration()])
app = Chalice(app_name="sentry_chalice")

@app.route("/boom")
def boom():
raise Exception("boom goes the dynamite!")

@app.route("/context")
def has_request():
raise Exception("boom goes the dynamite!")

@app.route("/badrequest")
def badrequest():
raise BadRequestError("bad-request")

LocalGateway._generate_lambda_context = _generate_lambda_context

return app


@pytest.fixture
def lambda_context_args():
return ["lambda_name", 256]


def test_exception_boom(app, client: RequestHandler) -> None:
response = client.get("/boom")
assert response.status_code == 500
assert response.json == dict(
[
("Code", "InternalServerError"),
("Message", "An internal server error occurred."),
]
)


def test_has_request(app, capture_events, client: RequestHandler):
events = capture_events()

response = client.get("/context")
assert response.status_code == 500

(event,) = events
assert event["level"] == "error"
(exception,) = event["exception"]["values"]
assert exception["type"] == "Exception"


def test_scheduled_event(app, lambda_context_args):
@app.schedule("rate(1 minutes)")
def every_hour(event):
raise Exception("schedule event!")

context = LambdaContext(
*lambda_context_args, max_runtime_ms=10000, time_source=time
)

lambda_event = {
"version": "0",
"account": "120987654312",
"region": "us-west-1",
"detail": {},
"detail-type": "Scheduled Event",
"source": "aws.events",
"time": "1970-01-01T00:00:00Z",
"id": "event-id",
"resources": ["arn:aws:events:us-west-1:120987654312:rule/my-schedule"],
}
with pytest.raises(Exception) as exc_info:
every_hour(lambda_event, context=context)
assert str(exc_info.value) == "schedule event!"


def test_bad_reques(client: RequestHandler) -> None:
response = client.get("/badrequest")

assert response.status_code == 400
assert response.json == dict(
[
("Code", "BadRequestError"),
("Message", "BadRequestError: bad-request"),
]
)
5 changes: 5 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ envlist =

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

{py3.6,py3.7,py3.8}-chalice

[testenv]
deps =
-r test-requirements.txt
Expand Down Expand Up @@ -192,6 +194,8 @@ deps =
py3.8: hypothesis

pure_eval: pure_eval
chalice: chalice>=1.16.0
chalice: pytest-chalice==0.0.5

setenv =
PYTHONDONTWRITEBYTECODE=1
Expand All @@ -217,6 +221,7 @@ setenv =
sqlalchemy: TESTPATH=tests/integrations/sqlalchemy
spark: TESTPATH=tests/integrations/spark
pure_eval: TESTPATH=tests/integrations/pure_eval
chalice: TESTPATH=tests/integrations/chalice

COVERAGE_FILE=.coverage-{envname}
passenv =
Expand Down