From dde70cc9c984ff02f25af59efdf23d762c400774 Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Sat, 7 Oct 2017 17:36:35 +0200 Subject: [PATCH 1/6] Initial lambda integration --- raven/contrib/awslambda/__init__.py | 165 +++++++++++++++++++++++++ tests/contrib/awslambda/test_lambda.py | 158 +++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 raven/contrib/awslambda/__init__.py create mode 100644 tests/contrib/awslambda/test_lambda.py diff --git a/raven/contrib/awslambda/__init__.py b/raven/contrib/awslambda/__init__.py new file mode 100644 index 000000000..2e75d73ec --- /dev/null +++ b/raven/contrib/awslambda/__init__.py @@ -0,0 +1,165 @@ +""" +raven.contrib.awslambda +~~~~~~~~~~~~~~~~~~~~ + +Raven wrapper for AWS Lambda handlers. + +:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +# flake8: noqa + +from __future__ import absolute_import + +import os +import logging +import functools +from types import FunctionType + +from raven.base import Client +from raven.transport.http import HTTPTransport + +logger = logging.getLogger('sentry.errors.client') + + +class LambdaClient(Client): + """ + Raven decorator for AWS Lambda. + + By default, the lambda integration will capture unhandled exceptions and instrument logging. + + Usage: + + >>> from raven.contrib.awslambda import LambdaClient + >>> + >>> + >>> client = LambdaClient() + >>> + >>> @client.capture_exceptions + >>> def handler(event, context): + >>> ... + >>> raise Exception('I will be sent to sentry!') + + """ + + def __init__(self, *args, **kwargs): + super(LambdaClient, self).__init__(transport=HTTPTransport, *args, **kwargs) + + def capture(self, *args, **kwargs): + if 'data' not in kwargs: + kwargs['data'] = data = {} + else: + data = kwargs['data'] + event = kwargs.get('event', None) + context = kwargs.get('context', None) + user_info = self._get_user_interface(event) + if user_info: + data.update(user_info) + if event: + http_info = self._get_http_interface(event) + if http_info: + data.update(http_info) + data['extra'] = self._get_extra_data(event, context) + return super(LambdaClient, self).capture(*args, **kwargs) + + def build_msg(self, *args, **kwargs): + + data = super(LambdaClient, self).build_msg(*args, **kwargs) + data['tags'].setdefault('lambda', os.environ.get('AWS_LAMBDA_FUNCTION_NAME')) + data['tags'].setdefault('version', os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')) + data['tags'].setdefault('memory_size', os.environ.get('AWS_LAMBDA_FUNCTION_MEMORY_SIZE')) + data['tags'].setdefault('log_group', os.environ.get('AWS_LAMBDA_LOG_GROUP_NAME')) + data['tags'].setdefault('log_stream', os.environ.get('AWS_LAMBDA_LOG_STREAM_NAME')) + data['tags'].setdefault('region', os.environ.get('AWS_REGION')) + data.setdefault('release', os.environ.get('SENTRY_RELEASE')) + data.setdefault('environment', os.environ.get('SENTRY_ENVIRONMENT')) + return data + + def capture_exceptions(self, f=None, exceptions=None): # TODO: Ash fix kwargs in base + """ + Wrap a function or code block in try/except and automatically call + ``.captureException`` if it raises an exception, then the exception + is reraised. + + By default, it will capture ``Exception`` + + >>> @client.capture_exceptions + >>> def foo(): + >>> raise Exception() + + >>> with client.capture_exceptions(): + >>> raise Exception() + + You can also specify exceptions to be caught specifically + + >>> @client.capture_exceptions((IOError, LookupError)) + >>> def bar(): + >>> ... + + ``kwargs`` are passed through to ``.captureException``. + """ + if not isinstance(f, FunctionType): + # when the decorator has args which is not a function we except + # f to be the exceptions tuple + return functools.partial(self.capture_exceptions, exceptions=f) + + exceptions = exceptions or (Exception,) + + @functools.wraps(f) + def wrapped(event, context, *args, **kwargs): + try: + return f(event, context, *args, **kwargs) + except exceptions: + self.captureException(event=event, context=context, **kwargs) + self.context.clear() + raise + return wrapped + + @staticmethod + def _get_user_interface(event): + if event.get('requestContext'): + identity = event['requestContext']['identity'] + if identity: + user = { + 'id': identity.get('cognitoIdentityId', None) or identity.get('user', None), + 'username': identity.get('user', None), + 'ip_address': identity.get('sourceIp', None), + 'cognito_identity_pool_id': identity.get('cognitoIdentityPoolId', None), + 'cognito_authentication_type': identity.get('cognitoAuthenticationType', None), + 'user_agent': identity.get('userAgent') + } + return {'user': user} + + @staticmethod + def _get_http_interface(event): + if event.get('path') and event.get('httpMethod'): + request = { + "url": event.get('path'), + "method": event.get('httpMethod'), + "query_string": event.get('queryStringParameters', None), + "headers": event.get('headers', None) or [], + } + return {'request': request} + + @staticmethod + def _get_extra_data(event, context): + extra_context = { + 'event': event, + 'aws_request_id': context.aws_request_id, + 'context': vars(context), + } + + if context.client_context: + extra_context['client_context'] = { + 'client.installation_id': context.client_context.client.installation_id, + 'client.app_title': context.client_context.client.app_title, + 'client.app_version_name': context.client_context.client.app_version_name, + 'client.app_version_code': context.client_context.client.app_version_code, + 'client.app_package_name': context.client_context.client.app_package_name, + 'custom': context.client_context.custom, + 'env': context.client_context.env, + } + return extra_context + + + diff --git a/tests/contrib/awslambda/test_lambda.py b/tests/contrib/awslambda/test_lambda.py new file mode 100644 index 000000000..4fdc5fd44 --- /dev/null +++ b/tests/contrib/awslambda/test_lambda.py @@ -0,0 +1,158 @@ +import pytest +import time +import uuid +from raven.contrib.awslambda import LambdaClient + + +class MockClient(LambdaClient): + + def __init__(self, *args, **kwargs): + self.events = [] + super(MockClient, self).__init__(*args, **kwargs) + + def send(self, **kwargs): + self.events.append(kwargs) + + def is_enabled(self, **kwargs): + return True + + +class MyException(Exception): + pass + + +class LambdaEvent(object): + def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): + self.body = body + self.headers = headers + self.httpMethod = http_method + self.isBase64Encoded = False + self.path = path + self.queryStringParameters = query_string + self.resource = path + self.stageVariables = None + self.requestContext = { + 'accountId': '0000000', + 'apiId': 'AAAAAAAA', + 'httpMethod': http_method, + 'identity': {}, + 'path': path, + 'requestId': 'test-request', + 'resourceId': 'bbzeyv', + 'resourcePath': '/test', + 'stage': 'test-stage' + } + + def get(self, name, default=None): + return getattr(self, name, default) + + +class LambdaIndentity(object): + def __init__(self, id=None, pool_id=None): + self.cognito_identity_id = id + self.cognito_identity_pool_id = pool_id + + +class LambdaContext(object): + + def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): + self.function_name = function_name + self.memory_limit_in_mb = memory_limit_in_mb + self.timeout = timeout + self.function_version = function_version + self.timeout = timeout + self.invoked_function_arn = 'invoked_function_arn' + self.log_group_name = 'log_group_name' + self.log_stream_name = 'log_stream_name' + self.identity = LambdaIndentity(id=0, pool_id=0) + self.client_context = None + self.aws_request_id = str(uuid.uuid4()) + self.start_time = time.time() * 1000 + + def get(self, name, default=None): + return getattr(self, name, default) + + def get_remaining_time_in_millis(self): + return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) + + +def test_decorator_exception(monkeypatch): + + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions + def test_func(event, context): + raise MyException('There was an error.') + + with pytest.raises(MyException): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert client.events + assert 'user' in client.events[0].keys() + assert 'request' in client.events[0].keys() + + +def test_decorator_with_args(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise Exception + + with pytest.raises(Exception): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert not client.events + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise MyException + + with pytest.raises(Exception): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert client.events + + +def test_decorator_without_exceptions(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + return 0 + + assert test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) == 0 + + +def test_decorator_without_kwargs(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise MyException + + with pytest.raises(Exception): + test_func(LambdaEvent(), LambdaContext(function_name='test_func')) + + assert client.events From 1d9482d58f72925e7f4edb2602669972ab3a631a Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Tue, 10 Oct 2017 17:47:17 +0200 Subject: [PATCH 2/6] feature(lambda): Add lambda tests and client --- .travis.yml | 8 ++ conftest.py | 5 +- docs/integrations/awslambda.rst | 56 ++++++++++++ raven/contrib/awslambda/__init__.py | 24 +++-- tests/contrib/awslambda/conftest.py | 106 ++++++++++++++++++++++ tests/contrib/awslambda/test_lambda.py | 121 ++++--------------------- tox.ini | 3 + 7 files changed, 209 insertions(+), 114 deletions(-) create mode 100644 docs/integrations/awslambda.rst create mode 100644 tests/contrib/awslambda/conftest.py diff --git a/.travis.yml b/.travis.yml index 87fbf9980..f5c28e3f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -115,6 +115,14 @@ jobs: python: 2.7 env: TOXENV=py27-celery-4 + - stage: contrib + python: 2.7 + env: TOXENV=py27-lambda + + - stage: contrib + python: 3.6 + env: TOXENV=py36-lambda + # - stage: deploy # script: skip # deploy: diff --git a/conftest.py b/conftest.py index 247587691..3f039e4d9 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,10 @@ import pytest import sys -collect_ignore = [] +collect_ignore = [ + 'tests/contrib/awslambda' +] + if sys.version_info[0] > 2: if sys.version_info[1] < 3: collect_ignore.append('tests/contrib/flask') diff --git a/docs/integrations/awslambda.rst b/docs/integrations/awslambda.rst new file mode 100644 index 000000000..a1dacc1a1 --- /dev/null +++ b/docs/integrations/awslambda.rst @@ -0,0 +1,56 @@ +Amazon Web Services Lambda +========================== + +.. default-domain:: py + + + +Installation +------------ + +To use `Sentry`_ with `AWS Lambda`_, you have to install `raven` as an external +dependency. This involves creating a `Deployment package`_ and uploading it +to AWS. + +To install raven into your current project directory: + +.. code-block:: console + + pip install raven -t /path/to/project-dir + +Setup +----- + +Create a `LambdaClient` instance and wrap your lambda handler with +the `capture_exeptions` decorator: + + +.. sourcecode:: python + + from raven.contrib.awslambda import LambdaClient + + + client = LambdaClient() + + @client.capture_exceptions + def handler(event, context): + ... + raise Exception('I will be sent to sentry!') + + +By default this will report unhandled exceptions and errors to Sentry. + +Additional settings for the client are configured using environment variables or +subclassing `LambdaClient`. + + + +Advanced Usage +-------------- + + +.. _Sentry: https://getsentry.com/ +.. _AWS Lambda: https://aws.amazon.com/lambda +.. _Deployment package: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html + + diff --git a/raven/contrib/awslambda/__init__.py b/raven/contrib/awslambda/__init__.py index 2e75d73ec..7d6f2b812 100644 --- a/raven/contrib/awslambda/__init__.py +++ b/raven/contrib/awslambda/__init__.py @@ -22,6 +22,17 @@ logger = logging.getLogger('sentry.errors.client') +def get_default_tags(): + return { + 'lambda': 'AWS_LAMBDA_FUNCTION_NAME', + 'version': 'AWS_LAMBDA_FUNCTION_VERSION', + 'memory_size': 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', + 'log_group': 'AWS_LAMBDA_LOG_GROUP_NAME', + 'log_stream': 'AWS_LAMBDA_LOG_STREAM_NAME', + 'region': 'AWS_REGION' + } + + class LambdaClient(Client): """ Raven decorator for AWS Lambda. @@ -43,7 +54,8 @@ class LambdaClient(Client): """ def __init__(self, *args, **kwargs): - super(LambdaClient, self).__init__(transport=HTTPTransport, *args, **kwargs) + transport = kwargs.get('transport', HTTPTransport) + super(LambdaClient, self).__init__(*args, transport=transport, **kwargs) def capture(self, *args, **kwargs): if 'data' not in kwargs: @@ -65,12 +77,8 @@ def capture(self, *args, **kwargs): def build_msg(self, *args, **kwargs): data = super(LambdaClient, self).build_msg(*args, **kwargs) - data['tags'].setdefault('lambda', os.environ.get('AWS_LAMBDA_FUNCTION_NAME')) - data['tags'].setdefault('version', os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')) - data['tags'].setdefault('memory_size', os.environ.get('AWS_LAMBDA_FUNCTION_MEMORY_SIZE')) - data['tags'].setdefault('log_group', os.environ.get('AWS_LAMBDA_LOG_GROUP_NAME')) - data['tags'].setdefault('log_stream', os.environ.get('AWS_LAMBDA_LOG_STREAM_NAME')) - data['tags'].setdefault('region', os.environ.get('AWS_REGION')) + for option, default in get_default_tags().items(): + data['tags'].setdefault(option, os.environ.get(default)) data.setdefault('release', os.environ.get('SENTRY_RELEASE')) data.setdefault('environment', os.environ.get('SENTRY_ENVIRONMENT')) return data @@ -128,7 +136,7 @@ def _get_user_interface(event): 'cognito_authentication_type': identity.get('cognitoAuthenticationType', None), 'user_agent': identity.get('userAgent') } - return {'user': user} + return {'user': user} @staticmethod def _get_http_interface(event): diff --git a/tests/contrib/awslambda/conftest.py b/tests/contrib/awslambda/conftest.py new file mode 100644 index 000000000..c1a481215 --- /dev/null +++ b/tests/contrib/awslambda/conftest.py @@ -0,0 +1,106 @@ + +import pytest +from raven.contrib.awslambda import LambdaClient +import uuid +import time + + +class MockClient(LambdaClient): + def __init__(self, *args, **kwargs): + self.events = [] + super(MockClient, self).__init__(*args, **kwargs) + + def send(self, **kwargs): + self.events.append(kwargs) + + def is_enabled(self, **kwargs): + return True + + +class LambdaIndentityStub(object): + def __init__(self, id=1, pool_id=1): + self.cognito_identity_id = id + self.cognito_identity_pool_id = pool_id + + def __getitem__(self, item): + return getattr(self, item) + + def get(self, name, default=None): + return getattr(self, name, default) + + +class LambdaContextStub(object): + + def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): + self.function_name = function_name + self.memory_limit_in_mb = memory_limit_in_mb + self.timeout = timeout + self.function_version = function_version + self.timeout = timeout + self.invoked_function_arn = 'invoked_function_arn' + self.log_group_name = 'log_group_name' + self.log_stream_name = 'log_stream_name' + self.identity = LambdaIndentityStub(id=0, pool_id=0) + self.client_context = None + self.aws_request_id = str(uuid.uuid4()) + self.start_time = time.time() * 1000 + + def __getitem__(self, item, default): + return getattr(self, item, default) + + def get(self, name, default=None): + return getattr(self, name, default) + + def get_remaining_time_in_millis(self): + return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) + + +class LambdaEventStub(object): + def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): + self.body = body + self.headers = headers + self.httpMethod = http_method + self.isBase64Encoded = False + self.path = path + self.queryStringParameters = query_string + self.resource = path + self.stageVariables = None + self.requestContext = { + 'accountId': '0000000', + 'apiId': 'AAAAAAAA', + 'httpMethod': http_method, + 'identity': LambdaIndentityStub(), + 'path': path, + 'requestId': 'test-request', + 'resourceId': 'bbzeyv', + 'resourcePath': '/test', + 'stage': 'test-stage' + } + + def __getitem__(self, name): + return getattr(self, name) + + def get(self, name, default=None): + return getattr(self, name, default) + + +@pytest.fixture +def lambda_env(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + + +@pytest.fixture +def mock_client(): + return MockClient + +@pytest.fixture +def lambda_event(): + return LambdaEventStub + +@pytest.fixture +def lambda_context(): + return LambdaContextStub diff --git a/tests/contrib/awslambda/test_lambda.py b/tests/contrib/awslambda/test_lambda.py index 4fdc5fd44..39cfc95ab 100644 --- a/tests/contrib/awslambda/test_lambda.py +++ b/tests/contrib/awslambda/test_lambda.py @@ -1,116 +1,37 @@ import pytest -import time -import uuid -from raven.contrib.awslambda import LambdaClient - - -class MockClient(LambdaClient): - - def __init__(self, *args, **kwargs): - self.events = [] - super(MockClient, self).__init__(*args, **kwargs) - - def send(self, **kwargs): - self.events.append(kwargs) - - def is_enabled(self, **kwargs): - return True +from raven.transport.http import HTTPTransport class MyException(Exception): pass -class LambdaEvent(object): - def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): - self.body = body - self.headers = headers - self.httpMethod = http_method - self.isBase64Encoded = False - self.path = path - self.queryStringParameters = query_string - self.resource = path - self.stageVariables = None - self.requestContext = { - 'accountId': '0000000', - 'apiId': 'AAAAAAAA', - 'httpMethod': http_method, - 'identity': {}, - 'path': path, - 'requestId': 'test-request', - 'resourceId': 'bbzeyv', - 'resourcePath': '/test', - 'stage': 'test-stage' - } - - def get(self, name, default=None): - return getattr(self, name, default) - - -class LambdaIndentity(object): - def __init__(self, id=None, pool_id=None): - self.cognito_identity_id = id - self.cognito_identity_pool_id = pool_id - - -class LambdaContext(object): - - def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): - self.function_name = function_name - self.memory_limit_in_mb = memory_limit_in_mb - self.timeout = timeout - self.function_version = function_version - self.timeout = timeout - self.invoked_function_arn = 'invoked_function_arn' - self.log_group_name = 'log_group_name' - self.log_stream_name = 'log_stream_name' - self.identity = LambdaIndentity(id=0, pool_id=0) - self.client_context = None - self.aws_request_id = str(uuid.uuid4()) - self.start_time = time.time() * 1000 - - def get(self, name, default=None): - return getattr(self, name, default) - - def get_remaining_time_in_millis(self): - return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) - - -def test_decorator_exception(monkeypatch): - - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_exception(lambda_env, mock_client, lambda_event, lambda_context): + + client = mock_client() @client.capture_exceptions def test_func(event, context): raise MyException('There was an error.') with pytest.raises(MyException): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert client.events + assert isinstance(client.remote.get_transport(), HTTPTransport) assert 'user' in client.events[0].keys() assert 'request' in client.events[0].keys() -def test_decorator_with_args(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_with_args(lambda_env, mock_client, lambda_event, lambda_context): + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): raise Exception with pytest.raises(Exception): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert not client.events @@ -119,40 +40,30 @@ def test_func(event, context): raise MyException with pytest.raises(Exception): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert client.events -def test_decorator_without_exceptions(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_without_exceptions(lambda_env, mock_client, lambda_event, lambda_context): + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): return 0 - assert test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) == 0 + assert test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) == 0 -def test_decorator_without_kwargs(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') +def test_decorator_without_kwargs(lambda_env, mock_client, lambda_event, lambda_context): - client = MockClient() + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): raise MyException with pytest.raises(Exception): - test_func(LambdaEvent(), LambdaContext(function_name='test_func')) + test_func(lambda_event(), lambda_context(function_name='test_func')) assert client.events diff --git a/tox.ini b/tox.ini index 4dbda2787..ddb68ff46 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ envlist = py35-flask-12 py35-flask-dev py27-celery-{3,4} + py{27-36}-lambda [testenv] @@ -49,6 +50,7 @@ setenv = django: TESTPATH=tests/contrib/django flask: TESTPATH=tests/contrib/flask celery: TESTPATH=tests/contrib/test_celery.py + lambda: TESTPATH=tests/contrib/awslambda usedevelop = true extras = tests @@ -58,6 +60,7 @@ basepython = py33: python3.3 py34: python3.4 py35: python3.5 + py36: python3.6 pypy: pypy commands = From 2ccb306dfa2fa6c82adfa163022cb9fe310d1ad6 Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Tue, 10 Oct 2017 18:09:05 +0200 Subject: [PATCH 3/6] Add D107 to ignore flake8 docs --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 79fcf7509..b341e7ef8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ flake8-ignore = tests/ ALL [flake8] -ignore = F999,E501,E128,E124,E402,W503,E731,F841,D100,D101,D102,D103,D104,D105,D200,D201,D205,D400,D401,D402,D403,I100,I101 +ignore = F999,E501,E128,E124,E402,W503,E731,F841,D100,D101,D102,D103,D104,D105,D107,D200,D201,D205,D400,D401,D402,D403,I100,I101 max-line-length = 100 exclude = .tox,.git,docs,tests From a1e40734c80e756746f0c24e23784a6780b8f502 Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Sat, 7 Oct 2017 17:36:35 +0200 Subject: [PATCH 4/6] Initial lambda integration --- raven/contrib/awslambda/__init__.py | 165 +++++++++++++++++++++++++ tests/contrib/awslambda/test_lambda.py | 158 +++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 raven/contrib/awslambda/__init__.py create mode 100644 tests/contrib/awslambda/test_lambda.py diff --git a/raven/contrib/awslambda/__init__.py b/raven/contrib/awslambda/__init__.py new file mode 100644 index 000000000..2e75d73ec --- /dev/null +++ b/raven/contrib/awslambda/__init__.py @@ -0,0 +1,165 @@ +""" +raven.contrib.awslambda +~~~~~~~~~~~~~~~~~~~~ + +Raven wrapper for AWS Lambda handlers. + +:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" +# flake8: noqa + +from __future__ import absolute_import + +import os +import logging +import functools +from types import FunctionType + +from raven.base import Client +from raven.transport.http import HTTPTransport + +logger = logging.getLogger('sentry.errors.client') + + +class LambdaClient(Client): + """ + Raven decorator for AWS Lambda. + + By default, the lambda integration will capture unhandled exceptions and instrument logging. + + Usage: + + >>> from raven.contrib.awslambda import LambdaClient + >>> + >>> + >>> client = LambdaClient() + >>> + >>> @client.capture_exceptions + >>> def handler(event, context): + >>> ... + >>> raise Exception('I will be sent to sentry!') + + """ + + def __init__(self, *args, **kwargs): + super(LambdaClient, self).__init__(transport=HTTPTransport, *args, **kwargs) + + def capture(self, *args, **kwargs): + if 'data' not in kwargs: + kwargs['data'] = data = {} + else: + data = kwargs['data'] + event = kwargs.get('event', None) + context = kwargs.get('context', None) + user_info = self._get_user_interface(event) + if user_info: + data.update(user_info) + if event: + http_info = self._get_http_interface(event) + if http_info: + data.update(http_info) + data['extra'] = self._get_extra_data(event, context) + return super(LambdaClient, self).capture(*args, **kwargs) + + def build_msg(self, *args, **kwargs): + + data = super(LambdaClient, self).build_msg(*args, **kwargs) + data['tags'].setdefault('lambda', os.environ.get('AWS_LAMBDA_FUNCTION_NAME')) + data['tags'].setdefault('version', os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')) + data['tags'].setdefault('memory_size', os.environ.get('AWS_LAMBDA_FUNCTION_MEMORY_SIZE')) + data['tags'].setdefault('log_group', os.environ.get('AWS_LAMBDA_LOG_GROUP_NAME')) + data['tags'].setdefault('log_stream', os.environ.get('AWS_LAMBDA_LOG_STREAM_NAME')) + data['tags'].setdefault('region', os.environ.get('AWS_REGION')) + data.setdefault('release', os.environ.get('SENTRY_RELEASE')) + data.setdefault('environment', os.environ.get('SENTRY_ENVIRONMENT')) + return data + + def capture_exceptions(self, f=None, exceptions=None): # TODO: Ash fix kwargs in base + """ + Wrap a function or code block in try/except and automatically call + ``.captureException`` if it raises an exception, then the exception + is reraised. + + By default, it will capture ``Exception`` + + >>> @client.capture_exceptions + >>> def foo(): + >>> raise Exception() + + >>> with client.capture_exceptions(): + >>> raise Exception() + + You can also specify exceptions to be caught specifically + + >>> @client.capture_exceptions((IOError, LookupError)) + >>> def bar(): + >>> ... + + ``kwargs`` are passed through to ``.captureException``. + """ + if not isinstance(f, FunctionType): + # when the decorator has args which is not a function we except + # f to be the exceptions tuple + return functools.partial(self.capture_exceptions, exceptions=f) + + exceptions = exceptions or (Exception,) + + @functools.wraps(f) + def wrapped(event, context, *args, **kwargs): + try: + return f(event, context, *args, **kwargs) + except exceptions: + self.captureException(event=event, context=context, **kwargs) + self.context.clear() + raise + return wrapped + + @staticmethod + def _get_user_interface(event): + if event.get('requestContext'): + identity = event['requestContext']['identity'] + if identity: + user = { + 'id': identity.get('cognitoIdentityId', None) or identity.get('user', None), + 'username': identity.get('user', None), + 'ip_address': identity.get('sourceIp', None), + 'cognito_identity_pool_id': identity.get('cognitoIdentityPoolId', None), + 'cognito_authentication_type': identity.get('cognitoAuthenticationType', None), + 'user_agent': identity.get('userAgent') + } + return {'user': user} + + @staticmethod + def _get_http_interface(event): + if event.get('path') and event.get('httpMethod'): + request = { + "url": event.get('path'), + "method": event.get('httpMethod'), + "query_string": event.get('queryStringParameters', None), + "headers": event.get('headers', None) or [], + } + return {'request': request} + + @staticmethod + def _get_extra_data(event, context): + extra_context = { + 'event': event, + 'aws_request_id': context.aws_request_id, + 'context': vars(context), + } + + if context.client_context: + extra_context['client_context'] = { + 'client.installation_id': context.client_context.client.installation_id, + 'client.app_title': context.client_context.client.app_title, + 'client.app_version_name': context.client_context.client.app_version_name, + 'client.app_version_code': context.client_context.client.app_version_code, + 'client.app_package_name': context.client_context.client.app_package_name, + 'custom': context.client_context.custom, + 'env': context.client_context.env, + } + return extra_context + + + diff --git a/tests/contrib/awslambda/test_lambda.py b/tests/contrib/awslambda/test_lambda.py new file mode 100644 index 000000000..4fdc5fd44 --- /dev/null +++ b/tests/contrib/awslambda/test_lambda.py @@ -0,0 +1,158 @@ +import pytest +import time +import uuid +from raven.contrib.awslambda import LambdaClient + + +class MockClient(LambdaClient): + + def __init__(self, *args, **kwargs): + self.events = [] + super(MockClient, self).__init__(*args, **kwargs) + + def send(self, **kwargs): + self.events.append(kwargs) + + def is_enabled(self, **kwargs): + return True + + +class MyException(Exception): + pass + + +class LambdaEvent(object): + def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): + self.body = body + self.headers = headers + self.httpMethod = http_method + self.isBase64Encoded = False + self.path = path + self.queryStringParameters = query_string + self.resource = path + self.stageVariables = None + self.requestContext = { + 'accountId': '0000000', + 'apiId': 'AAAAAAAA', + 'httpMethod': http_method, + 'identity': {}, + 'path': path, + 'requestId': 'test-request', + 'resourceId': 'bbzeyv', + 'resourcePath': '/test', + 'stage': 'test-stage' + } + + def get(self, name, default=None): + return getattr(self, name, default) + + +class LambdaIndentity(object): + def __init__(self, id=None, pool_id=None): + self.cognito_identity_id = id + self.cognito_identity_pool_id = pool_id + + +class LambdaContext(object): + + def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): + self.function_name = function_name + self.memory_limit_in_mb = memory_limit_in_mb + self.timeout = timeout + self.function_version = function_version + self.timeout = timeout + self.invoked_function_arn = 'invoked_function_arn' + self.log_group_name = 'log_group_name' + self.log_stream_name = 'log_stream_name' + self.identity = LambdaIndentity(id=0, pool_id=0) + self.client_context = None + self.aws_request_id = str(uuid.uuid4()) + self.start_time = time.time() * 1000 + + def get(self, name, default=None): + return getattr(self, name, default) + + def get_remaining_time_in_millis(self): + return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) + + +def test_decorator_exception(monkeypatch): + + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions + def test_func(event, context): + raise MyException('There was an error.') + + with pytest.raises(MyException): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert client.events + assert 'user' in client.events[0].keys() + assert 'request' in client.events[0].keys() + + +def test_decorator_with_args(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise Exception + + with pytest.raises(Exception): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert not client.events + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise MyException + + with pytest.raises(Exception): + test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + + assert client.events + + +def test_decorator_without_exceptions(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + return 0 + + assert test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) == 0 + + +def test_decorator_without_kwargs(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + + client = MockClient() + + @client.capture_exceptions((MyException,)) + def test_func(event, context): + raise MyException + + with pytest.raises(Exception): + test_func(LambdaEvent(), LambdaContext(function_name='test_func')) + + assert client.events From 799b1485e0175a6337c09ff6941f3f332b3a26d4 Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Tue, 10 Oct 2017 17:47:17 +0200 Subject: [PATCH 5/6] feature(lambda): Add lambda tests and client --- .travis.yml | 8 ++ conftest.py | 5 +- docs/integrations/awslambda.rst | 56 ++++++++++++ raven/contrib/awslambda/__init__.py | 24 +++-- tests/contrib/awslambda/conftest.py | 106 ++++++++++++++++++++++ tests/contrib/awslambda/test_lambda.py | 121 ++++--------------------- tox.ini | 3 + 7 files changed, 209 insertions(+), 114 deletions(-) create mode 100644 docs/integrations/awslambda.rst create mode 100644 tests/contrib/awslambda/conftest.py diff --git a/.travis.yml b/.travis.yml index 87fbf9980..f5c28e3f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -115,6 +115,14 @@ jobs: python: 2.7 env: TOXENV=py27-celery-4 + - stage: contrib + python: 2.7 + env: TOXENV=py27-lambda + + - stage: contrib + python: 3.6 + env: TOXENV=py36-lambda + # - stage: deploy # script: skip # deploy: diff --git a/conftest.py b/conftest.py index 53a8b880d..70d582d8d 100644 --- a/conftest.py +++ b/conftest.py @@ -4,7 +4,10 @@ import pytest import sys -collect_ignore = [] +collect_ignore = [ + 'tests/contrib/awslambda' +] + if sys.version_info[0] > 2: if sys.version_info[1] < 3: collect_ignore.append('tests/contrib/flask') diff --git a/docs/integrations/awslambda.rst b/docs/integrations/awslambda.rst new file mode 100644 index 000000000..a1dacc1a1 --- /dev/null +++ b/docs/integrations/awslambda.rst @@ -0,0 +1,56 @@ +Amazon Web Services Lambda +========================== + +.. default-domain:: py + + + +Installation +------------ + +To use `Sentry`_ with `AWS Lambda`_, you have to install `raven` as an external +dependency. This involves creating a `Deployment package`_ and uploading it +to AWS. + +To install raven into your current project directory: + +.. code-block:: console + + pip install raven -t /path/to/project-dir + +Setup +----- + +Create a `LambdaClient` instance and wrap your lambda handler with +the `capture_exeptions` decorator: + + +.. sourcecode:: python + + from raven.contrib.awslambda import LambdaClient + + + client = LambdaClient() + + @client.capture_exceptions + def handler(event, context): + ... + raise Exception('I will be sent to sentry!') + + +By default this will report unhandled exceptions and errors to Sentry. + +Additional settings for the client are configured using environment variables or +subclassing `LambdaClient`. + + + +Advanced Usage +-------------- + + +.. _Sentry: https://getsentry.com/ +.. _AWS Lambda: https://aws.amazon.com/lambda +.. _Deployment package: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html + + diff --git a/raven/contrib/awslambda/__init__.py b/raven/contrib/awslambda/__init__.py index 2e75d73ec..7d6f2b812 100644 --- a/raven/contrib/awslambda/__init__.py +++ b/raven/contrib/awslambda/__init__.py @@ -22,6 +22,17 @@ logger = logging.getLogger('sentry.errors.client') +def get_default_tags(): + return { + 'lambda': 'AWS_LAMBDA_FUNCTION_NAME', + 'version': 'AWS_LAMBDA_FUNCTION_VERSION', + 'memory_size': 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', + 'log_group': 'AWS_LAMBDA_LOG_GROUP_NAME', + 'log_stream': 'AWS_LAMBDA_LOG_STREAM_NAME', + 'region': 'AWS_REGION' + } + + class LambdaClient(Client): """ Raven decorator for AWS Lambda. @@ -43,7 +54,8 @@ class LambdaClient(Client): """ def __init__(self, *args, **kwargs): - super(LambdaClient, self).__init__(transport=HTTPTransport, *args, **kwargs) + transport = kwargs.get('transport', HTTPTransport) + super(LambdaClient, self).__init__(*args, transport=transport, **kwargs) def capture(self, *args, **kwargs): if 'data' not in kwargs: @@ -65,12 +77,8 @@ def capture(self, *args, **kwargs): def build_msg(self, *args, **kwargs): data = super(LambdaClient, self).build_msg(*args, **kwargs) - data['tags'].setdefault('lambda', os.environ.get('AWS_LAMBDA_FUNCTION_NAME')) - data['tags'].setdefault('version', os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')) - data['tags'].setdefault('memory_size', os.environ.get('AWS_LAMBDA_FUNCTION_MEMORY_SIZE')) - data['tags'].setdefault('log_group', os.environ.get('AWS_LAMBDA_LOG_GROUP_NAME')) - data['tags'].setdefault('log_stream', os.environ.get('AWS_LAMBDA_LOG_STREAM_NAME')) - data['tags'].setdefault('region', os.environ.get('AWS_REGION')) + for option, default in get_default_tags().items(): + data['tags'].setdefault(option, os.environ.get(default)) data.setdefault('release', os.environ.get('SENTRY_RELEASE')) data.setdefault('environment', os.environ.get('SENTRY_ENVIRONMENT')) return data @@ -128,7 +136,7 @@ def _get_user_interface(event): 'cognito_authentication_type': identity.get('cognitoAuthenticationType', None), 'user_agent': identity.get('userAgent') } - return {'user': user} + return {'user': user} @staticmethod def _get_http_interface(event): diff --git a/tests/contrib/awslambda/conftest.py b/tests/contrib/awslambda/conftest.py new file mode 100644 index 000000000..c1a481215 --- /dev/null +++ b/tests/contrib/awslambda/conftest.py @@ -0,0 +1,106 @@ + +import pytest +from raven.contrib.awslambda import LambdaClient +import uuid +import time + + +class MockClient(LambdaClient): + def __init__(self, *args, **kwargs): + self.events = [] + super(MockClient, self).__init__(*args, **kwargs) + + def send(self, **kwargs): + self.events.append(kwargs) + + def is_enabled(self, **kwargs): + return True + + +class LambdaIndentityStub(object): + def __init__(self, id=1, pool_id=1): + self.cognito_identity_id = id + self.cognito_identity_pool_id = pool_id + + def __getitem__(self, item): + return getattr(self, item) + + def get(self, name, default=None): + return getattr(self, name, default) + + +class LambdaContextStub(object): + + def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): + self.function_name = function_name + self.memory_limit_in_mb = memory_limit_in_mb + self.timeout = timeout + self.function_version = function_version + self.timeout = timeout + self.invoked_function_arn = 'invoked_function_arn' + self.log_group_name = 'log_group_name' + self.log_stream_name = 'log_stream_name' + self.identity = LambdaIndentityStub(id=0, pool_id=0) + self.client_context = None + self.aws_request_id = str(uuid.uuid4()) + self.start_time = time.time() * 1000 + + def __getitem__(self, item, default): + return getattr(self, item, default) + + def get(self, name, default=None): + return getattr(self, name, default) + + def get_remaining_time_in_millis(self): + return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) + + +class LambdaEventStub(object): + def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): + self.body = body + self.headers = headers + self.httpMethod = http_method + self.isBase64Encoded = False + self.path = path + self.queryStringParameters = query_string + self.resource = path + self.stageVariables = None + self.requestContext = { + 'accountId': '0000000', + 'apiId': 'AAAAAAAA', + 'httpMethod': http_method, + 'identity': LambdaIndentityStub(), + 'path': path, + 'requestId': 'test-request', + 'resourceId': 'bbzeyv', + 'resourcePath': '/test', + 'stage': 'test-stage' + } + + def __getitem__(self, name): + return getattr(self, name) + + def get(self, name, default=None): + return getattr(self, name, default) + + +@pytest.fixture +def lambda_env(monkeypatch): + monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') + monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') + monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') + monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') + + +@pytest.fixture +def mock_client(): + return MockClient + +@pytest.fixture +def lambda_event(): + return LambdaEventStub + +@pytest.fixture +def lambda_context(): + return LambdaContextStub diff --git a/tests/contrib/awslambda/test_lambda.py b/tests/contrib/awslambda/test_lambda.py index 4fdc5fd44..39cfc95ab 100644 --- a/tests/contrib/awslambda/test_lambda.py +++ b/tests/contrib/awslambda/test_lambda.py @@ -1,116 +1,37 @@ import pytest -import time -import uuid -from raven.contrib.awslambda import LambdaClient - - -class MockClient(LambdaClient): - - def __init__(self, *args, **kwargs): - self.events = [] - super(MockClient, self).__init__(*args, **kwargs) - - def send(self, **kwargs): - self.events.append(kwargs) - - def is_enabled(self, **kwargs): - return True +from raven.transport.http import HTTPTransport class MyException(Exception): pass -class LambdaEvent(object): - def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None): - self.body = body - self.headers = headers - self.httpMethod = http_method - self.isBase64Encoded = False - self.path = path - self.queryStringParameters = query_string - self.resource = path - self.stageVariables = None - self.requestContext = { - 'accountId': '0000000', - 'apiId': 'AAAAAAAA', - 'httpMethod': http_method, - 'identity': {}, - 'path': path, - 'requestId': 'test-request', - 'resourceId': 'bbzeyv', - 'resourcePath': '/test', - 'stage': 'test-stage' - } - - def get(self, name, default=None): - return getattr(self, name, default) - - -class LambdaIndentity(object): - def __init__(self, id=None, pool_id=None): - self.cognito_identity_id = id - self.cognito_identity_pool_id = pool_id - - -class LambdaContext(object): - - def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'): - self.function_name = function_name - self.memory_limit_in_mb = memory_limit_in_mb - self.timeout = timeout - self.function_version = function_version - self.timeout = timeout - self.invoked_function_arn = 'invoked_function_arn' - self.log_group_name = 'log_group_name' - self.log_stream_name = 'log_stream_name' - self.identity = LambdaIndentity(id=0, pool_id=0) - self.client_context = None - self.aws_request_id = str(uuid.uuid4()) - self.start_time = time.time() * 1000 - - def get(self, name, default=None): - return getattr(self, name, default) - - def get_remaining_time_in_millis(self): - return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0) - - -def test_decorator_exception(monkeypatch): - - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_exception(lambda_env, mock_client, lambda_event, lambda_context): + + client = mock_client() @client.capture_exceptions def test_func(event, context): raise MyException('There was an error.') with pytest.raises(MyException): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert client.events + assert isinstance(client.remote.get_transport(), HTTPTransport) assert 'user' in client.events[0].keys() assert 'request' in client.events[0].keys() -def test_decorator_with_args(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_with_args(lambda_env, mock_client, lambda_event, lambda_context): + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): raise Exception with pytest.raises(Exception): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert not client.events @@ -119,40 +40,30 @@ def test_func(event, context): raise MyException with pytest.raises(Exception): - test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) + test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) assert client.events -def test_decorator_without_exceptions(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') - client = MockClient() +def test_decorator_without_exceptions(lambda_env, mock_client, lambda_event, lambda_context): + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): return 0 - assert test_func(event=LambdaEvent(), context=LambdaContext(function_name='test_func')) == 0 + assert test_func(event=lambda_event(), context=lambda_context(function_name='test_func')) == 0 -def test_decorator_without_kwargs(monkeypatch): - monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func') - monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST') - monkeypatch.setenv('SENTRY_RELEASE', '$LATEST') - monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing') +def test_decorator_without_kwargs(lambda_env, mock_client, lambda_event, lambda_context): - client = MockClient() + client = mock_client() @client.capture_exceptions((MyException,)) def test_func(event, context): raise MyException with pytest.raises(Exception): - test_func(LambdaEvent(), LambdaContext(function_name='test_func')) + test_func(lambda_event(), lambda_context(function_name='test_func')) assert client.events diff --git a/tox.ini b/tox.ini index 4dbda2787..ddb68ff46 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ envlist = py35-flask-12 py35-flask-dev py27-celery-{3,4} + py{27-36}-lambda [testenv] @@ -49,6 +50,7 @@ setenv = django: TESTPATH=tests/contrib/django flask: TESTPATH=tests/contrib/flask celery: TESTPATH=tests/contrib/test_celery.py + lambda: TESTPATH=tests/contrib/awslambda usedevelop = true extras = tests @@ -58,6 +60,7 @@ basepython = py33: python3.3 py34: python3.4 py35: python3.5 + py36: python3.6 pypy: pypy commands = From dbe9b1b6fa6af350c6059441f530ceae9a39b874 Mon Sep 17 00:00:00 2001 From: Ashley Camba Garrido Date: Wed, 11 Oct 2017 15:54:08 +0200 Subject: [PATCH 6/6] Add reference to raven-python-lambda --- docs/integrations/awslambda.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/integrations/awslambda.rst b/docs/integrations/awslambda.rst index a1dacc1a1..07ac0db39 100644 --- a/docs/integrations/awslambda.rst +++ b/docs/integrations/awslambda.rst @@ -44,13 +44,11 @@ Additional settings for the client are configured using environment variables or subclassing `LambdaClient`. - -Advanced Usage --------------- +The integration was inspired by `raven python lambda`_, another implementation that +also integrates with Serverless Framework and has SQS transport support. .. _Sentry: https://getsentry.com/ .. _AWS Lambda: https://aws.amazon.com/lambda .. _Deployment package: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html - - +.. _raven python lambda: https://github.com/Netflix-Skunkworks/raven-python-lambda