|
| 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 | + |
0 commit comments