def _invoke_successive_app(self): """Invoke a successive app function to handle more logs This is useful when there were more logs to collect than could be accomplished in this execution. Instead of marking the config with 'success' and waiting for the next scheduled execution, this will invoke the lambda again with an 'event' indicating there are more logs to collect. Other scheduled executions will not have an 'event' to allow for this type of override, and will exit when checking the 'self._config.is_running' property. This allows for chained invocations without the worry of duplicated effort or collisions. """ lambda_client = boto3.client('lambda') try: response = lambda_client.invoke( FunctionName=self._config.function_name, InvocationType='Event', Payload=self._config.successive_event, Qualifier=self._config.function_version) except ClientError as err: LOGGER.error( 'An error occurred while invoking a subsequent app function ' '(\'%s:%s\'). Error is: %s', self._config.function_name, self._config.function_version, err.response) raise LOGGER.info( 'Invoking successive apps function \'%s\' with Lambda request ID \'%s\'', self._config.function_name, response['ResponseMetadata']['RequestId'])
def _gather(self): """Protected entry point to peform the gather that returns the time the process took Returns: float: time, in seconds, for which the function ran """ # Make this request sleep if the API throttles requests self._sleep() # Increment the poll count self._poll_count += 1 logs = self._gather_logs() # Make sure there are logs, this can be False if there was an issue polling # of if there are no new logs to be polled if not logs: self._more_to_poll = False LOGGER.error( '[%s] Gather process was not able to poll any logs ' 'on poll #%d', self, self._poll_count) return # Increment the count of logs gathered self._gathered_log_count += len(logs) # Utilize the batcher to send logs to the rule processor self._batcher.send_logs(logs) LOGGER.debug('Updating config last timestamp from %s to %s', self._config.last_timestamp, self._last_timestamp) # Save the config's last timestamp after each function run self._config.last_timestamp = self._last_timestamp
def _finalize(self): """Method for performing any final steps, like saving applicable state This function is also used to invoke a new copy of this lambda in the case that there are more logs available to collect. """ if not self._last_timestamp: LOGGER.error( 'Ending last timestamp is 0. This should not happen and is likely ' 'due to the subclass not setting this value.') if self._last_timestamp == self._config.start_last_timestamp: LOGGER.info( 'Ending last timestamp is the same as the beginning last timestamp. ' 'This could occur if there were no logs collected for this execution.' ) LOGGER.info('[%s] App complete; gathered %d logs in %d polls.', self, self._gathered_log_count, self._poll_count) self._config.last_timestamp = self._last_timestamp # If there are more logs to poll, invoke this app function again and mark # the config as 'partial'. Marking the state as 'partial' prevents # scheduled function invocations from running alongside chained invocations. if self._more_to_poll: self._config.mark_partial() self._invoke_successive_app() return self._config.mark_success()
def _initialize(self): """Method for performing any startup steps, like setting state to running""" # Perform another safety check to make sure this is not being invoked already if self._config.is_running: LOGGER.error('[%s] App already running', self) return False # Check if this is an invocation spawned from a previous partial execution # Return if the config is marked as 'partial' but the invocation type is wrong if not self._config.is_successive_invocation and self._config.is_partial: LOGGER.error('[%s] App in partial execution state, exiting', self) return False LOGGER.info('[%s] Starting app', self) LOGGER.info('App executing as a successive invocation: %s', self._config.is_successive_invocation) # Validate the auth in the config. This raises an exception upon failure self._config.validate_auth(set(self.required_auth_info())) self._config.set_starting_timestamp(self.date_formatter()) self._last_timestamp = self._config.last_timestamp # Mark this app as running, which updates the parameter store self._config.mark_running() return True
def _check_http_response(self, response): """Method for checking for a valid HTTP response code Returns: bool: Indicator of whether or not this request was successful """ success = response is not None and (200 <= response.status_code <= 299) if not success: LOGGER.error('[%s] HTTP request failed: [%d] %s', self, response.status_code, response.content) return success
def current_state(self, state): """Set the current state of the execution""" if not getattr(self.States, str(state).upper(), None): LOGGER.error('Current state cannot be saved with value \'%s\'', state) return if self._current_state == state: LOGGER.debug('State is unchanged and will not be saved: %s', state) return LOGGER.debug('Setting current state to: %s', state) self._current_state = state self._save_state()
def _send_logs_to_lambda(self, logs): """Protected method for sending logs to the rule processor lambda function for processing. This performs some size checks before sending. Args: source_function (str): The app function name from which the logs came logs (list): List of the logs that have been gathered """ # Create a payload to be sent to the rule processor that contains the # service these logs were collected from and the list of logs payload = {'Records': [{'stream_alert_app': self._source_function, 'logs': logs}]} payload_json = json.dumps(payload, separators=(',', ':')) if len(payload_json) > self.MAX_LAMBDA_PAYLOAD_SIZE: if len(logs) == 1: LOGGER.error('Log payload size for single log exceeds input limit and will be ' 'dropped (%d > %d max).', len(payload_json), self.MAX_LAMBDA_PAYLOAD_SIZE) return True LOGGER.debug('Log payload size for %d logs exceeds limit and will be ' 'segmented (%d > %d max).', len(logs), len(payload_json), self.MAX_LAMBDA_PAYLOAD_SIZE) return False LOGGER.debug('Sending %d logs to rule processor with payload size %d', len(logs), len(payload_json)) try: response = Batcher.LAMBDA_CLIENT.invoke( FunctionName=self._destination_function, InvocationType='Event', Payload=payload_json, Qualifier='production' ) except ClientError as err: LOGGER.error('An error occurred while sending logs to ' '\'%s:production\'. Error is: %s', self._destination_function, err.response) raise LOGGER.info('Sent %d logs to \'%s\' with Lambda request ID \'%s\'', len(logs), self._destination_function, response['ResponseMetadata']['RequestId']) return True