def create_alert_rule( organization, projects, name, query, aggregation, time_window, threshold_period, include_all_projects=False, excluded_projects=None, ): """ Creates an alert rule for an organization. :param organization: :param projects: A list of projects to subscribe to the rule. This will be overriden if `include_all_projects` is True :param name: Name for the alert rule. This will be used as part of the incident name, and must be unique per project :param query: An event search query to subscribe to and monitor for alerts :param aggregation: A QueryAggregation to fetch for this alert rule :param time_window: Time period to aggregate over, in minutes :param threshold_period: How many update periods the value of the subscription needs to exceed the threshold before triggering :param include_all_projects: Whether to include all current and future projects from this organization :param excluded_projects: List of projects to exclude if we're using `include_all_projects`. :return: The created `AlertRule` """ dataset = QueryDatasets.EVENTS resolution = DEFAULT_ALERT_RULE_RESOLUTION validate_alert_rule_query(query) if AlertRule.objects.filter(organization=organization, name=name).exists(): raise AlertRuleNameAlreadyUsedError() with transaction.atomic(): alert_rule = AlertRule.objects.create( organization=organization, name=name, dataset=dataset.value, query=query, aggregation=aggregation.value, time_window=time_window, resolution=resolution, threshold_period=threshold_period, include_all_projects=include_all_projects, ) if include_all_projects: excluded_projects = excluded_projects if excluded_projects else [] projects = Project.objects.filter(organization=organization).exclude( id__in=[p.id for p in excluded_projects] ) exclusions = [ AlertRuleExcludedProjects(alert_rule=alert_rule, project=project) for project in excluded_projects ] AlertRuleExcludedProjects.objects.bulk_create(exclusions) subscribe_projects_to_alert_rule(alert_rule, projects) return alert_rule
def update_alert_rule( alert_rule, projects=None, name=None, query=None, aggregation=None, time_window=None, environment=None, threshold_period=None, include_all_projects=None, excluded_projects=None, ): """ Updates an alert rule. :param alert_rule: The alert rule to update :param excluded_projects: List of projects to subscribe to the rule. Ignored if `include_all_projects` is True :param name: Name for the alert rule. This will be used as part of the incident name, and must be unique per project. :param query: An event search query to subscribe to and monitor for alerts :param aggregation: An AlertRuleAggregation that we want to fetch for this alert rule :param time_window: Time period to aggregate over, in minutes. :param environment: List of environments that this rule applies to :param threshold_period: How many update periods the value of the subscription needs to exceed the threshold before triggering :param include_all_projects: Whether to include all current and future projects from this organization :param excluded_projects: List of projects to exclude if we're using `include_all_projects`. Ignored otherwise. :return: The updated `AlertRule` """ if (name and alert_rule.name != name and AlertRule.objects.filter(organization=alert_rule.organization, name=name).exists()): raise AlertRuleNameAlreadyUsedError() updated_fields = {} if name: updated_fields["name"] = name if query is not None: validate_alert_rule_query(query) updated_fields["query"] = query if aggregation is not None: updated_fields["aggregation"] = aggregation.value if time_window: updated_fields["time_window"] = time_window if threshold_period: updated_fields["threshold_period"] = threshold_period if include_all_projects is not None: updated_fields["include_all_projects"] = include_all_projects with transaction.atomic(): incidents = Incident.objects.filter(alert_rule=alert_rule).exists() if incidents: snapshot_alert_rule(alert_rule) alert_rule.update(**updated_fields) existing_subs = [] if (query is not None or aggregation is not None or time_window is not None or projects is not None or include_all_projects is not None or excluded_projects is not None): existing_subs = alert_rule.query_subscriptions.all( ).select_related("project") new_projects = [] deleted_subs = [] if not alert_rule.include_all_projects: # We don't want to have any exclusion rows present if we're not in # `include_all_projects` mode get_excluded_projects_for_alert_rule(alert_rule).delete() if alert_rule.include_all_projects: if include_all_projects or excluded_projects is not None: # If we're in `include_all_projects` mode, we want to just fetch # projects that aren't already subscribed, and haven't been excluded so # we can add them. excluded_project_ids = ({p.id for p in excluded_projects} if excluded_projects else set()) project_exclusions = get_excluded_projects_for_alert_rule( alert_rule) project_exclusions.exclude( project_id__in=excluded_project_ids).delete() existing_excluded_project_ids = { pe.project_id for pe in project_exclusions } new_exclusions = [ AlertRuleExcludedProjects(alert_rule=alert_rule, project_id=project_id) for project_id in excluded_project_ids if project_id not in existing_excluded_project_ids ] AlertRuleExcludedProjects.objects.bulk_create(new_exclusions) new_projects = Project.objects.filter( organization=alert_rule.organization).exclude( id__in=set([sub.project_id for sub in existing_subs]) | excluded_project_ids) # If we're subscribed to any of the excluded projects then we want to # remove those subscriptions deleted_subs = [ sub for sub in existing_subs if sub.project_id in excluded_project_ids ] elif projects is not None: existing_project_slugs = { sub.project.slug for sub in existing_subs } # Determine whether we've added any new projects as part of this update new_projects = [ project for project in projects if project.slug not in existing_project_slugs ] updated_project_slugs = {project.slug for project in projects} # Find any subscriptions that were removed as part of this update deleted_subs = [ sub for sub in existing_subs if sub.project.slug not in updated_project_slugs ] if new_projects: subscribe_projects_to_alert_rule(alert_rule, new_projects) if deleted_subs: bulk_delete_snuba_subscriptions(deleted_subs) # Remove any deleted subscriptions from `existing_subscriptions`, so that # if we need to update any subscriptions we don't end up doing it twice. We # don't add new subscriptions here since they'll already have the updated # values existing_subs = [sub for sub in existing_subs if sub.id] if environment: # Delete rows we don't have present in the updated data. AlertRuleEnvironment.objects.filter(alert_rule=alert_rule).exclude( environment__in=environment).delete() for e in environment: AlertRuleEnvironment.objects.get_or_create( alert_rule=alert_rule, environment=e) else: AlertRuleEnvironment.objects.filter(alert_rule=alert_rule).delete() if existing_subs and (query is not None or aggregation is not None or time_window is not None): # If updating any subscription details, update related Snuba subscriptions # too bulk_update_snuba_subscriptions( existing_subs, alert_rule.query, QueryAggregations(alert_rule.aggregation), timedelta(minutes=alert_rule.time_window), timedelta(minutes=DEFAULT_ALERT_RULE_RESOLUTION), list(alert_rule.environment.all()), ) return alert_rule
def update_alert_rule( alert_rule, projects=None, name=None, query=None, aggregate=None, time_window=None, environment=None, threshold_period=None, include_all_projects=None, excluded_projects=None, ): """ Updates an alert rule. :param alert_rule: The alert rule to update :param excluded_projects: List of projects to subscribe to the rule. Ignored if `include_all_projects` is True :param name: Name for the alert rule. This will be used as part of the incident name, and must be unique per project. :param query: An event search query to subscribe to and monitor for alerts :param aggregate: A string representing the aggregate used in this alert rule :param time_window: Time period to aggregate over, in minutes. :param environment: An optional environment that this rule applies to :param threshold_period: How many update periods the value of the subscription needs to exceed the threshold before triggering :param include_all_projects: Whether to include all current and future projects from this organization :param excluded_projects: List of projects to exclude if we're using `include_all_projects`. Ignored otherwise. :return: The updated `AlertRule` """ if ( name and alert_rule.name != name and AlertRule.objects.filter(organization=alert_rule.organization, name=name).exists() ): raise AlertRuleNameAlreadyUsedError() updated_fields = {} updated_query_fields = {} if name: updated_fields["name"] = name if query is not None: validate_alert_rule_query(query) updated_query_fields["query"] = query if aggregate is not None: updated_query_fields["aggregate"] = aggregate if time_window: updated_query_fields["time_window"] = timedelta(minutes=time_window) if threshold_period: updated_fields["threshold_period"] = threshold_period if include_all_projects is not None: updated_fields["include_all_projects"] = include_all_projects with transaction.atomic(): incidents = Incident.objects.filter(alert_rule=alert_rule).exists() if incidents: snapshot_alert_rule(alert_rule) alert_rule.update(**updated_fields) if updated_query_fields or environment != alert_rule.snuba_query.environment: snuba_query = alert_rule.snuba_query updated_query_fields.setdefault("query", snuba_query.query) # XXX: We use the alert rule aggregation here since currently we're # expecting the enum value to be passed. updated_query_fields.setdefault("aggregate", snuba_query.aggregate) updated_query_fields.setdefault( "time_window", timedelta(seconds=snuba_query.time_window) ) update_snuba_query( alert_rule.snuba_query, resolution=timedelta(minutes=DEFAULT_ALERT_RULE_RESOLUTION), environment=environment, **updated_query_fields ) existing_subs = [] if ( query is not None or aggregate is not None or time_window is not None or projects is not None or include_all_projects is not None or excluded_projects is not None ): existing_subs = alert_rule.snuba_query.subscriptions.all().select_related("project") new_projects = [] deleted_subs = [] if not alert_rule.include_all_projects: # We don't want to have any exclusion rows present if we're not in # `include_all_projects` mode get_excluded_projects_for_alert_rule(alert_rule).delete() if alert_rule.include_all_projects: if include_all_projects or excluded_projects is not None: # If we're in `include_all_projects` mode, we want to just fetch # projects that aren't already subscribed, and haven't been excluded so # we can add them. excluded_project_ids = ( {p.id for p in excluded_projects} if excluded_projects else set() ) project_exclusions = get_excluded_projects_for_alert_rule(alert_rule) project_exclusions.exclude(project_id__in=excluded_project_ids).delete() existing_excluded_project_ids = {pe.project_id for pe in project_exclusions} new_exclusions = [ AlertRuleExcludedProjects(alert_rule=alert_rule, project_id=project_id) for project_id in excluded_project_ids if project_id not in existing_excluded_project_ids ] AlertRuleExcludedProjects.objects.bulk_create(new_exclusions) new_projects = Project.objects.filter(organization=alert_rule.organization).exclude( id__in=set([sub.project_id for sub in existing_subs]) | excluded_project_ids ) # If we're subscribed to any of the excluded projects then we want to # remove those subscriptions deleted_subs = [ sub for sub in existing_subs if sub.project_id in excluded_project_ids ] elif projects is not None: existing_project_slugs = {sub.project.slug for sub in existing_subs} # Determine whether we've added any new projects as part of this update new_projects = [ project for project in projects if project.slug not in existing_project_slugs ] updated_project_slugs = {project.slug for project in projects} # Find any subscriptions that were removed as part of this update deleted_subs = [ sub for sub in existing_subs if sub.project.slug not in updated_project_slugs ] if new_projects: subscribe_projects_to_alert_rule(alert_rule, new_projects) if deleted_subs: bulk_delete_snuba_subscriptions(deleted_subs) return alert_rule
def create_alert_rule( organization, projects, name, query, aggregate, time_window, threshold_type, threshold_period, resolve_threshold=None, environment=None, include_all_projects=False, excluded_projects=None, dataset=QueryDatasets.EVENTS, ): """ Creates an alert rule for an organization. :param organization: :param projects: A list of projects to subscribe to the rule. This will be overriden if `include_all_projects` is True :param name: Name for the alert rule. This will be used as part of the incident name, and must be unique per project :param query: An event search query to subscribe to and monitor for alerts :param aggregate: A string representing the aggregate used in this alert rule :param time_window: Time period to aggregate over, in minutes :param environment: An optional environment that this rule applies to :param threshold_type: An AlertRuleThresholdType :param threshold_period: How many update periods the value of the subscription needs to exceed the threshold before triggering :param resolve_threshold: Optional value that the subscription needs to reach to resolve the alert :param include_all_projects: Whether to include all current and future projects from this organization :param excluded_projects: List of projects to exclude if we're using `include_all_projects`. :param dataset: The dataset that this query will be executed on :return: The created `AlertRule` """ resolution = DEFAULT_ALERT_RULE_RESOLUTION validate_alert_rule_query(query) if AlertRule.objects.filter(organization=organization, name=name).exists(): raise AlertRuleNameAlreadyUsedError() with transaction.atomic(): snuba_query = create_snuba_query( dataset, query, aggregate, timedelta(minutes=time_window), timedelta(minutes=resolution), environment, ) alert_rule = AlertRule.objects.create( organization=organization, snuba_query=snuba_query, name=name, threshold_type=threshold_type.value, resolve_threshold=resolve_threshold, threshold_period=threshold_period, include_all_projects=include_all_projects, ) if include_all_projects: excluded_projects = excluded_projects if excluded_projects else [] projects = Project.objects.filter(organization=organization).exclude( id__in=[p.id for p in excluded_projects] ) exclusions = [ AlertRuleExcludedProjects(alert_rule=alert_rule, project=project) for project in excluded_projects ] AlertRuleExcludedProjects.objects.bulk_create(exclusions) subscribe_projects_to_alert_rule(alert_rule, projects) return alert_rule