Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.

Commit e1fd30b

Browse files
authored
AWS Lambda Integration (#1107)
* Initial lambda integration * feature(lambda): Add lambda tests and client * Add D107 to ignore flake8 docs * Initial lambda integration * feature(lambda): Add lambda tests and client * Add reference to raven-python-lambda
1 parent 608d34e commit e1fd30b

File tree

7 files changed

+417
-1
lines changed

7 files changed

+417
-1
lines changed

.travis.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ jobs:
115115
python: 2.7
116116
env: TOXENV=py27-celery-4
117117

118+
- stage: contrib
119+
python: 2.7
120+
env: TOXENV=py27-lambda
121+
122+
- stage: contrib
123+
python: 3.6
124+
env: TOXENV=py36-lambda
125+
118126
# - stage: deploy
119127
# script: skip
120128
# deploy:

conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import pytest
55
import sys
66

7-
collect_ignore = []
7+
collect_ignore = [
8+
'tests/contrib/awslambda'
9+
]
10+
811
if sys.version_info[0] > 2:
912
if sys.version_info[1] < 3:
1013
collect_ignore.append('tests/contrib/flask')

docs/integrations/awslambda.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Amazon Web Services Lambda
2+
==========================
3+
4+
.. default-domain:: py
5+
6+
7+
8+
Installation
9+
------------
10+
11+
To use `Sentry`_ with `AWS Lambda`_, you have to install `raven` as an external
12+
dependency. This involves creating a `Deployment package`_ and uploading it
13+
to AWS.
14+
15+
To install raven into your current project directory:
16+
17+
.. code-block:: console
18+
19+
pip install raven -t /path/to/project-dir
20+
21+
Setup
22+
-----
23+
24+
Create a `LambdaClient` instance and wrap your lambda handler with
25+
the `capture_exeptions` decorator:
26+
27+
28+
.. sourcecode:: python
29+
30+
from raven.contrib.awslambda import LambdaClient
31+
32+
33+
client = LambdaClient()
34+
35+
@client.capture_exceptions
36+
def handler(event, context):
37+
...
38+
raise Exception('I will be sent to sentry!')
39+
40+
41+
By default this will report unhandled exceptions and errors to Sentry.
42+
43+
Additional settings for the client are configured using environment variables or
44+
subclassing `LambdaClient`.
45+
46+
47+
The integration was inspired by `raven python lambda`_, another implementation that
48+
also integrates with Serverless Framework and has SQS transport support.
49+
50+
51+
.. _Sentry: https://getsentry.com/
52+
.. _AWS Lambda: https://aws.amazon.com/lambda
53+
.. _Deployment package: https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
54+
.. _raven python lambda: https://github.com/Netflix-Skunkworks/raven-python-lambda
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
raven.contrib.awslambda
3+
~~~~~~~~~~~~~~~~~~~~
4+
5+
Raven wrapper for AWS Lambda handlers.
6+
7+
:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
8+
:license: BSD, see LICENSE for more details.
9+
"""
10+
# flake8: noqa
11+
12+
from __future__ import absolute_import
13+
14+
import os
15+
import logging
16+
import functools
17+
from types import FunctionType
18+
19+
from raven.base import Client
20+
from raven.transport.http import HTTPTransport
21+
22+
logger = logging.getLogger('sentry.errors.client')
23+
24+
25+
def get_default_tags():
26+
return {
27+
'lambda': 'AWS_LAMBDA_FUNCTION_NAME',
28+
'version': 'AWS_LAMBDA_FUNCTION_VERSION',
29+
'memory_size': 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
30+
'log_group': 'AWS_LAMBDA_LOG_GROUP_NAME',
31+
'log_stream': 'AWS_LAMBDA_LOG_STREAM_NAME',
32+
'region': 'AWS_REGION'
33+
}
34+
35+
36+
class LambdaClient(Client):
37+
"""
38+
Raven decorator for AWS Lambda.
39+
40+
By default, the lambda integration will capture unhandled exceptions and instrument logging.
41+
42+
Usage:
43+
44+
>>> from raven.contrib.awslambda import LambdaClient
45+
>>>
46+
>>>
47+
>>> client = LambdaClient()
48+
>>>
49+
>>> @client.capture_exceptions
50+
>>> def handler(event, context):
51+
>>> ...
52+
>>> raise Exception('I will be sent to sentry!')
53+
54+
"""
55+
56+
def __init__(self, *args, **kwargs):
57+
transport = kwargs.get('transport', HTTPTransport)
58+
super(LambdaClient, self).__init__(*args, transport=transport, **kwargs)
59+
60+
def capture(self, *args, **kwargs):
61+
if 'data' not in kwargs:
62+
kwargs['data'] = data = {}
63+
else:
64+
data = kwargs['data']
65+
event = kwargs.get('event', None)
66+
context = kwargs.get('context', None)
67+
user_info = self._get_user_interface(event)
68+
if user_info:
69+
data.update(user_info)
70+
if event:
71+
http_info = self._get_http_interface(event)
72+
if http_info:
73+
data.update(http_info)
74+
data['extra'] = self._get_extra_data(event, context)
75+
return super(LambdaClient, self).capture(*args, **kwargs)
76+
77+
def build_msg(self, *args, **kwargs):
78+
79+
data = super(LambdaClient, self).build_msg(*args, **kwargs)
80+
for option, default in get_default_tags().items():
81+
data['tags'].setdefault(option, os.environ.get(default))
82+
data.setdefault('release', os.environ.get('SENTRY_RELEASE'))
83+
data.setdefault('environment', os.environ.get('SENTRY_ENVIRONMENT'))
84+
return data
85+
86+
def capture_exceptions(self, f=None, exceptions=None): # TODO: Ash fix kwargs in base
87+
"""
88+
Wrap a function or code block in try/except and automatically call
89+
``.captureException`` if it raises an exception, then the exception
90+
is reraised.
91+
92+
By default, it will capture ``Exception``
93+
94+
>>> @client.capture_exceptions
95+
>>> def foo():
96+
>>> raise Exception()
97+
98+
>>> with client.capture_exceptions():
99+
>>> raise Exception()
100+
101+
You can also specify exceptions to be caught specifically
102+
103+
>>> @client.capture_exceptions((IOError, LookupError))
104+
>>> def bar():
105+
>>> ...
106+
107+
``kwargs`` are passed through to ``.captureException``.
108+
"""
109+
if not isinstance(f, FunctionType):
110+
# when the decorator has args which is not a function we except
111+
# f to be the exceptions tuple
112+
return functools.partial(self.capture_exceptions, exceptions=f)
113+
114+
exceptions = exceptions or (Exception,)
115+
116+
@functools.wraps(f)
117+
def wrapped(event, context, *args, **kwargs):
118+
try:
119+
return f(event, context, *args, **kwargs)
120+
except exceptions:
121+
self.captureException(event=event, context=context, **kwargs)
122+
self.context.clear()
123+
raise
124+
return wrapped
125+
126+
@staticmethod
127+
def _get_user_interface(event):
128+
if event.get('requestContext'):
129+
identity = event['requestContext']['identity']
130+
if identity:
131+
user = {
132+
'id': identity.get('cognitoIdentityId', None) or identity.get('user', None),
133+
'username': identity.get('user', None),
134+
'ip_address': identity.get('sourceIp', None),
135+
'cognito_identity_pool_id': identity.get('cognitoIdentityPoolId', None),
136+
'cognito_authentication_type': identity.get('cognitoAuthenticationType', None),
137+
'user_agent': identity.get('userAgent')
138+
}
139+
return {'user': user}
140+
141+
@staticmethod
142+
def _get_http_interface(event):
143+
if event.get('path') and event.get('httpMethod'):
144+
request = {
145+
"url": event.get('path'),
146+
"method": event.get('httpMethod'),
147+
"query_string": event.get('queryStringParameters', None),
148+
"headers": event.get('headers', None) or [],
149+
}
150+
return {'request': request}
151+
152+
@staticmethod
153+
def _get_extra_data(event, context):
154+
extra_context = {
155+
'event': event,
156+
'aws_request_id': context.aws_request_id,
157+
'context': vars(context),
158+
}
159+
160+
if context.client_context:
161+
extra_context['client_context'] = {
162+
'client.installation_id': context.client_context.client.installation_id,
163+
'client.app_title': context.client_context.client.app_title,
164+
'client.app_version_name': context.client_context.client.app_version_name,
165+
'client.app_version_code': context.client_context.client.app_version_code,
166+
'client.app_package_name': context.client_context.client.app_package_name,
167+
'custom': context.client_context.custom,
168+
'env': context.client_context.env,
169+
}
170+
return extra_context
171+
172+
173+
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
2+
import pytest
3+
from raven.contrib.awslambda import LambdaClient
4+
import uuid
5+
import time
6+
7+
8+
class MockClient(LambdaClient):
9+
def __init__(self, *args, **kwargs):
10+
self.events = []
11+
super(MockClient, self).__init__(*args, **kwargs)
12+
13+
def send(self, **kwargs):
14+
self.events.append(kwargs)
15+
16+
def is_enabled(self, **kwargs):
17+
return True
18+
19+
20+
class LambdaIndentityStub(object):
21+
def __init__(self, id=1, pool_id=1):
22+
self.cognito_identity_id = id
23+
self.cognito_identity_pool_id = pool_id
24+
25+
def __getitem__(self, item):
26+
return getattr(self, item)
27+
28+
def get(self, name, default=None):
29+
return getattr(self, name, default)
30+
31+
32+
class LambdaContextStub(object):
33+
34+
def __init__(self, function_name, memory_limit_in_mb=128, timeout=300, function_version='$LATEST'):
35+
self.function_name = function_name
36+
self.memory_limit_in_mb = memory_limit_in_mb
37+
self.timeout = timeout
38+
self.function_version = function_version
39+
self.timeout = timeout
40+
self.invoked_function_arn = 'invoked_function_arn'
41+
self.log_group_name = 'log_group_name'
42+
self.log_stream_name = 'log_stream_name'
43+
self.identity = LambdaIndentityStub(id=0, pool_id=0)
44+
self.client_context = None
45+
self.aws_request_id = str(uuid.uuid4())
46+
self.start_time = time.time() * 1000
47+
48+
def __getitem__(self, item, default):
49+
return getattr(self, item, default)
50+
51+
def get(self, name, default=None):
52+
return getattr(self, name, default)
53+
54+
def get_remaining_time_in_millis(self):
55+
return max(self.timeout * 1000 - int((time.time() * 1000) - self.start_time), 0)
56+
57+
58+
class LambdaEventStub(object):
59+
def __init__(self, body=None, headers=None, http_method='GET', path='/test', query_string=None):
60+
self.body = body
61+
self.headers = headers
62+
self.httpMethod = http_method
63+
self.isBase64Encoded = False
64+
self.path = path
65+
self.queryStringParameters = query_string
66+
self.resource = path
67+
self.stageVariables = None
68+
self.requestContext = {
69+
'accountId': '0000000',
70+
'apiId': 'AAAAAAAA',
71+
'httpMethod': http_method,
72+
'identity': LambdaIndentityStub(),
73+
'path': path,
74+
'requestId': 'test-request',
75+
'resourceId': 'bbzeyv',
76+
'resourcePath': '/test',
77+
'stage': 'test-stage'
78+
}
79+
80+
def __getitem__(self, name):
81+
return getattr(self, name)
82+
83+
def get(self, name, default=None):
84+
return getattr(self, name, default)
85+
86+
87+
@pytest.fixture
88+
def lambda_env(monkeypatch):
89+
monkeypatch.setenv('SENTRY_DSN', 'http://public:secret@example.com/1')
90+
monkeypatch.setenv('AWS_LAMBDA_FUNCTION_NAME', 'test_func')
91+
monkeypatch.setenv('AWS_LAMBDA_FUNCTION_VERSION', '$LATEST')
92+
monkeypatch.setenv('SENTRY_RELEASE', '$LATEST')
93+
monkeypatch.setenv('SENTRY_ENVIRONMENT', 'testing')
94+
95+
96+
@pytest.fixture
97+
def mock_client():
98+
return MockClient
99+
100+
@pytest.fixture
101+
def lambda_event():
102+
return LambdaEventStub
103+
104+
@pytest.fixture
105+
def lambda_context():
106+
return LambdaContextStub

0 commit comments

Comments
 (0)