"""Lambda that listens to DynamoDB events and saves changes as json to S3. For each event (contains batch of changes) there will be json file saved to S3. This lambda was developed to use with Python 3. """ import copy import datetime import logging import os import time import uuid import boto3 import simplejson as json import config # noqa from connectors import sentry # noqa from util import upload_to_s3 # noqa s3 = boto3.client('s3') boto3.resource('dynamodb', region_name=config.AWS_REGION) deserializer = boto3.dynamodb.types.TypeDeserializer() LOGGING_LEVEL = os.environ.get('LOGGING_LEVEL', logging.INFO) log = logging.getLogger() log.setLevel(LOGGING_LEVEL) # For logging purposes only. Since lambda is reusing the same container # multiple times, we can roughly calculate the number of retries # when the processing fails. retry_count = 0 def handler(event, context): """Extract changed rows from DynamoDB event and write them to S3 bucket.""" try: global retry_count data = parse_dynamo_events(event) json_data = json.dumps(data) s3_key = make_s3_key() log.info('Saving to S3: %s', s3_key) upload_to_s3(s3, config.S3_BUCKET_NAME, s3_key, json_data) retry_count = 0 log.info('Successfully saved to S3: %s', s3_key) return {'created_filename': s3_key} except Exception as ex: try: retry_count += 1 log.exception( 'Failed to process dynamoDB batch. Attempt: %s', retry_count) sentry.sentry_client.captureException() except: # noqa log.exception('Failed to capture exception to Sentry.') finally: # Workaround to avoid possible failures during captureException() # call. See details in comments to this ticket: # https://jira.theorchard.com/browse/MR-3329 # An exception must always be raised, that will trigger lambda # retry mechanism. log.error('Reraising the error to be reprocessed by lambda.') raise ex def parse_dynamo_events(dynamo_events): """Extract data from DynamoDB events and return it as list. Args: dynamo_events (dict): events received from DynamoDB Returns: list: dicts with events """ result = [] isrcs = set() for event in dynamo_events['Records']: parsed_event = parse_single_dynamo_event(event) result.append(parsed_event) isrcs.add(parsed_event['isrc']) log.info('ISRCs received: %s', ','.join(isrcs)) return result def parse_single_dynamo_event(dynamo_event): """Make dict of single dynamodb event payload without type declarations. Args: dynamo_event (dict): single dynamodb event Returns: dict: data from received event without type declarations """ dynamo_event_copy = copy.deepcopy(dynamo_event) result = { 'action': dynamo_event_copy['eventName'], 'isrc': dynamo_event_copy['dynamodb']['Keys']['isrc']['S'], 'event_time': ( dynamo_event_copy['dynamodb']['ApproximateCreationDateTime']), 'data': {} } is_remove_event = result['action'] == 'REMOVE' if is_remove_event: return result event_payload = { key: deserializer.deserialize(value) for key, value in dynamo_event_copy['dynamodb']['NewImage'].items()} if 'updated_timestamp' in event_payload: result['event_time'] = event_payload['updated_timestamp'] # Represents one-to-one relationship of territory with tuid (deprecated) old_tuid_dicts = { territory: [int(tuid_dict['tuid'])] for territory, tuid_dict in event_payload['territories'].items() if isinstance(tuid_dict, dict)} # Represents one-to-many relationship of territory with tuids new_tuid_lists = { territory: [int(tuid_dict['tuid']) for tuid_dict in tuid_list] for territory, tuid_list in event_payload['territories'].items() if isinstance(tuid_list, list)} old_tuid_dicts.update(new_tuid_lists) result['data']['territories'] = old_tuid_dicts result['data']['locked_territories'] = event_payload['locked_territories'] return result def make_s3_key(): """Generate directory and file names for S3 bucket. This lambda is supposed to run every day, so naming will be following: directory name will be current date in YYYY-MM-DD format, file name will be timestamp of current UTC time Returns: str: path to file where event data should be saved """ directory_name = datetime.date.today().strftime('%Y-%m-%d') timestamp = time.mktime(datetime.datetime.utcnow().timetuple()) name = str(timestamp) + str(uuid.uuid4()) filename = '.'.join([name, 'json']) s3_key = os.path.join(config.S3_BUCKET_FOLDER, directory_name, filename) return s3_key