"""Generic activities to validate raw data.""" import os from garcon import task from garcon_contrib.dynamo_feed_status import garcon_feed_status from feed_ingestion.conf.config import merge_configs from feed_ingestion.flows import registered_executors from feed_ingestion.flows.apple_music_streams import config from feed_ingestion.flows.helpers import get_sf_config from feed_ingestion.util import sentry_util, task_status TASK_ID = 'validate_raw_data_tasks' STATUS_LOADED = 'LOADED' STATUS_PARTIALLY_LOADED = 'PARTIALLY_LOADED' STATUS_FAILED = 'LOAD_FAILED' @task.decorate(timeout=28800) def validate_raw_data( activity, feed_name, date, sfdb_params, temp_staging_raw_table, error_limit, key_dir, aws, kwargs=None): """Validate data on s3. Validates source files, logs data errors via Sentry and aborts execution if error rate is too high. Args: activity (ActivityWorker): The activity worker. feed_name (str): Feed name of workflow execution for status updates. date (str): Reporting date (YYYY-MM-DD). sfdb_params (dict): Dict with params to optionally override default ones (Snowflake db and schema name). key_dir (str): S3 directory to load files from. temp_staging_raw_table (str): Name of the temp staging_raw table. error_limit (int): Snowflake error limit. aws (dict): AWS credentials to parametrize COPY INTO statement. Returns: list: List of JSONParserErrors. """ task_name = '{task_id}_{table_name}'.format( task_id=TASK_ID, table_name=temp_staging_raw_table) if task_status.is_completed_task(feed_name, date, task_name): activity.logger.info( 'validate_raw_data for {date} already complete, and ' '"reload" flag was not passed, skipping...'.format(date=date)) return kwargs = kwargs or {} sf_config = get_sf_config(feed_name) sf_config_custom = merge_configs(sf_config, sfdb_params) ExecutorSR = registered_executors.get(feed_name) with ExecutorSR(sf_config_custom) as sf_executor: parser_errors = sf_executor.validate_raw_data( temp_staging_raw_table, aws, error_limit, key_dir, **kwargs) _log_parser_errors(parser_errors) if len(parser_errors) > error_limit: garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_INGESTED) err_msg = ( 'Exceeded error limit. Error limit: {}. ' 'Errors number: {}'.format( error_limit, len(parser_errors))) activity.logger.error(err_msg) raise Exception(err_msg) task_status.mark_completed_task(feed_name, date, task_name) activity.logger.info( '{task_id} of {feed_name} for {date} for {table} completed'.format( task_id=TASK_ID, feed_name=feed_name, date=date, table=temp_staging_raw_table)) def _log_parser_errors(parser_errors): """Log parser errors to Sentry. Args: parser_errors (list): List of parser errors. """ if not parser_errors: return if os.environ.get('SENTRY_DSN'): msg = 'Invalid records found in: {}\n'.format(parser_errors[0].file) for e in parser_errors: msg += ( 'Error: {}\n' 'Rejected Record: {}\n'.format(e.error, e.rejected_record)) sentry_util.send_message( msg, level='warning') @task.decorate(timeout=12600) def load_temp_staging_raw_table( activity, date, feed_name, aws, key_dir, temp_staging_raw_table, sfdb_params, error_limit=0, secrets_path=None, kwargs=None): """Load staged data from bucket to the temp datestamped table in Snowflake. It also can raise exception if there are a lot of errors while loading. Args: activity (ActivityWorker): The activity worker. aws (dict): AWS credentials to parametrize COPY INTO statement. date (str): Reporting date (YYYY-MM-DD). feed_name (str): Name of the feed to get executor class. key_dir (str): S3 directory to load files from. temp_staging_raw_table (str): Name of the temp staging_raw table. sfdb_params (dict): Dict with params to optionally override default ones (Snowflake db and schema name). error_limit (int): Snowflake error limit. secrets_path (str): Secrets manager path of the flow. kwargs (dict): Custom activity params. """ kwargs = kwargs or {} has_contexts = False contexts = task_status.get_report_contexts(feed_name, date) report_name = kwargs.get('report_name', None) completed_report = True if contexts: has_contexts = True completed_report = ( all(val[task_status.FIELD_CONTEXT_STATUS] == task_status.CONTEXT_STATUS_PROCESSED for val in contexts.values())) if (task_status.is_completed_task( feed_name, date, 'staging_raw_table_tasks') and (not has_contexts or has_contexts and completed_report # need to load common reports, because other reports depend on it and report_name not in config.common_reports)): activity.logger.info( 'load_temp_staging_raw_table for {feed_name} {date} ' 'already complete, and ' '"reload" flag was not passed, skipping...' .format(feed_name=feed_name, date=date)) return activity.logger.info( f'Loading ' f'temp_staging_raw_table: {temp_staging_raw_table}, ' f'completed: {completed_report}') kwargs.update({'error_limit': error_limit}) sf_config = get_sf_config(secrets_path) sf_config_custom = merge_configs(sf_config, sfdb_params) ExecutorSR = registered_executors.get(feed_name) with ExecutorSR(sf_config_custom) as sf_executor: result = sf_executor.load_temp_staging_raw_table( temp_staging_raw_table, aws, key_dir, **kwargs) loading_errors = list( filter(lambda x: x.status.upper() != STATUS_LOADED, result)) _warning_to_sentry([_message_for_error(e) for e in loading_errors]) for err in loading_errors: # having status STATUS_LOADED filtered out # for STATUS_PARTIALLY_LOADED should check limit and then fail # for all other statuses should just fail if err.status != STATUS_PARTIALLY_LOADED \ or err.errors_seen >= error_limit: garcon_feed_status.delete_status(feed_name, date) garcon_feed_status.set_overall_status( feed_name, date, garcon_feed_status.STATUS_NOT_INGESTED) err_msg = ( 'Load into {table} failed. {message}'.format( table=temp_staging_raw_table, message=_message_for_error(err))) activity.logger.error(err_msg) raise Exception(err_msg) def _message_for_error(err): msg = 'There are some errors in : {}\n'.format(err.file) msg += ( 'Loading status: {}\n' 'Error: {}\n' 'Count of errors: {}\n'.format( err.status, err.first_error, err.errors_seen)) return msg def _warning_to_sentry(messages): """Log parser errors to Sentry. Args: parser_errors (list): List of str """ if messages and os.environ.get('SENTRY_DSN'): sentry_util.send_message( '\n'.join(messages), level='warning' )