From e29a59235445fe2b9ac82e72ecdae0d0bf47e11e Mon Sep 17 00:00:00 2001 From: Gleekzone <46584253+Gleekzone@users.noreply.github.com> Date: Mon, 17 Aug 2020 17:37:03 -0500 Subject: [PATCH] Add sentry events (#13) * chalice-events-sentry * format * add-data-key * clear-breadcrumbs * add-old-if * add-test * add-info-lambda * format * add-more-test * bump version Co-authored-by: Matin Tamizi --- sentry_chalice/sentry_chalice.py | 99 +++++++++++++------------------- sentry_chalice/version.py | 2 +- tests/conftest.py | 6 +- tests/test_sentry_chalice.py | 47 +++++++++++++-- 4 files changed, 89 insertions(+), 65 deletions(-) diff --git a/sentry_chalice/sentry_chalice.py b/sentry_chalice/sentry_chalice.py index 0f721eb..9c5343c 100644 --- a/sentry_chalice/sentry_chalice.py +++ b/sentry_chalice/sentry_chalice.py @@ -1,14 +1,16 @@ import sys import traceback -from datetime import datetime, timedelta -from os import environ +from datetime import datetime +import chalice from chalice import Chalice, ChaliceViewError, Response +from chalice.app import EventSourceHandler as ChaliceEventSourceHandler +from sentry_sdk._compat import reraise from sentry_sdk._types import MYPY from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration +from sentry_sdk.integrations.aws_lambda import _get_cloudwatch_logs_url from sentry_sdk.utils import ( AnnotatedValue, capture_internal_exceptions, @@ -21,6 +23,30 @@ from sentry_sdk._types import Event, EventProcessor, Hint +class EventSourceHandler(ChaliceEventSourceHandler): + def __call__(self, event, context): + hub = Hub.current + client = hub.client + + with hub.push_scope() as scope: + try: + event_obj = self.event_class(event, context) + return self.func(event_obj) + except Exception: + scope.add_event_processor( + _make_request_event_processor(event, context) + ) + 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): hub = Hub.current client = hub.client @@ -83,7 +109,7 @@ def setup_once(): # for @app.route() Chalice._get_view_function_response = _get_view_function_response # for everything else (like events) - AwsLambdaIntegration.setup_once() + chalice.app.EventSourceHandler = EventSourceHandler def _make_request_event_processor(current_request, lambda_context): @@ -92,36 +118,32 @@ def _make_request_event_processor(current_request, lambda_context): def event_processor(event, hint, start_time=start_time): # type: (Event, Hint, datetime) -> Optional[Event] + extra = event.setdefault("extra", {}) - extra["lambda"] = { + + extra["Chalice-lambda"] = { "function_name": lambda_context.function_name, "function_version": lambda_context.function_version, - "invoked_function_arn": lambda_context.invoked_function_arn, + "Lambda ARN": lambda_context.invoked_function_arn, "aws_request_id": lambda_context.aws_request_id, } - extra["cloudwatch logs"] = { + extra["cloudwatch info"] = { "url": _get_cloudwatch_logs_url(lambda_context, start_time), "log_group": lambda_context.log_group_name, "log_stream": lambda_context.log_stream_name, } - request = event.get("request", {}) - if "httpMethod" in current_request.context: - request["method"] = current_request.context["httpMethod"] + request_info = event.get("request", {}) - request["url"] = _get_url(current_request, lambda_context) + request_info["method"] = current_request.context["httpMethod"] - if "query_params" in current_request.to_dict(): - request["query_string"] = current_request.query_params + request_info["query_string"] = current_request.query_params - if "headers" in current_request.to_dict(): - request["headers"] = _filter_headers(current_request.headers) + request_info["headers"] = _filter_headers(current_request.headers) if current_request._body is None: - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue( + request_info["data"] = AnnotatedValue( "", {"rem": [["!raw", "x", 0, 0]]} ) @@ -136,47 +158,8 @@ def event_processor(event, hint, start_time=start_time): if ip is not None: user_info.setdefault("ip_address", ip) - event["request"] = request + event["request"] = request_info return event return event_processor - - -def _get_url(event, context): - # type: (Any, Any) -> str - path = event.context["path"] - headers = event.headers - host = headers.get("Host", None) - proto = headers.get("X-Forwarded-Proto", None) - if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(context.function_name) - - -def _get_cloudwatch_logs_url(context, start_time): - # type: (Any, datetime) -> str - """ - Generates a CloudWatchLogs console URL based on the context object - Arguments: - context {Any} -- context from lambda handler - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%S" - - url = ( - "https://console.aws.amazon.com/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - region=environ.get("AWS_REGION"), - log_group=context.log_group_name, - log_stream=context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now() + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url diff --git a/sentry_chalice/version.py b/sentry_chalice/version.py index fc79d63..0404d81 100644 --- a/sentry_chalice/version.py +++ b/sentry_chalice/version.py @@ -1 +1 @@ -__version__ = '0.2.1' +__version__ = '0.3.0' diff --git a/tests/conftest.py b/tests/conftest.py index 9a725f3..24ca574 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest import sentry_sdk -from chalice import Chalice +from chalice import BadRequestError, Chalice from sentry_chalice import ChaliceIntegration @@ -20,6 +20,10 @@ def boom(): def has_request(): raise Exception('boom goes the dynamite!') + @app.route('/badrequest') + def badrequest(): + raise BadRequestError('bad-request') + return app diff --git a/tests/test_sentry_chalice.py b/tests/test_sentry_chalice.py index 38bbb43..a7ccabc 100644 --- a/tests/test_sentry_chalice.py +++ b/tests/test_sentry_chalice.py @@ -1,3 +1,4 @@ +import pytest from chalice.test import Client @@ -20,8 +21,44 @@ def test_has_request(app, capture_events): response = client.http.get('/context') assert response.status_code == 500 - (event,) = events - assert event["transaction"] == "api_handler" - assert "data" not in event["request"] - assert event["request"]["url"] == "awslambda:///api_handler" - assert event["request"]["headers"] == {} + (event,) = events + assert event["transaction"] == "api_handler" + assert "data" not in event["request"] + assert event["request"]["headers"] == {} + + +def test_scheduled_event(app): + @app.schedule('rate(1 minutes)') + def every_hour(event): + raise Exception('only chalice event!') + + lambda_event = { + "version": "0", + "account": "123456789012", + "region": "us-west-2", + "detail": {}, + "detail-type": "Scheduled Event", + "source": "aws.events", + "time": "1970-01-01T00:00:00Z", + "id": "event-id", + "resources": [ + "arn:aws:events:us-west-2:123456789012:rule/my-schedule" + ], + } + with pytest.raises(Exception) as exc_info: + every_hour(lambda_event, context=None) + assert str(exc_info.value) == 'only chalice event!' + + +def test_bad_reques(app) -> None: + with Client(app) as client: + + response = client.http.get('/badrequest') + + assert response.status_code == 400 + assert response.json_body == dict( + [ + ('Code', 'BadRequestError'), + ('Message', 'BadRequestError: bad-request'), + ] + )