1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Model for a group of alerts.""" 6 7import logging 8 9from google.appengine.ext import ndb 10 11from dashboard import quick_logger 12from dashboard import utils 13 14# Max number of AlertGroup entities to fetch. 15_MAX_GROUPS_TO_FETCH = 2000 16 17 18class AlertGroup(ndb.Model): 19 """Represents a group of alerts that are likely to have the same cause.""" 20 21 # Issue tracker id. 22 bug_id = ndb.IntegerProperty(indexed=True) 23 24 # The minimum start of the revision range where the anomaly occurred. 25 start_revision = ndb.IntegerProperty(indexed=True) 26 27 # The minimum end of the revision range where the anomaly occurred. 28 end_revision = ndb.IntegerProperty(indexed=False) 29 30 # A list of test suites. 31 test_suites = ndb.StringProperty(repeated=True, indexed=False) 32 33 # The kind of the alerts in this group. Each group only has one kind. 34 alert_kind = ndb.StringProperty(indexed=False) 35 36 def UpdateRevisionRange(self, grouped_alerts): 37 """Sets this group's revision range the minimum of the given group. 38 39 Args: 40 grouped_alerts: Alert entities that belong to this group. These 41 are only given here so that they don't need to be fetched. 42 """ 43 min_rev_range = utils.MinimumAlertRange(grouped_alerts) 44 start, end = min_rev_range if min_rev_range else (None, None) 45 if self.start_revision != start or self.end_revision != end: 46 self.start_revision = start 47 self.end_revision = end 48 self.put() 49 50 51def GroupAlerts(alerts, test_suite, kind): 52 """Groups alerts with matching criteria. 53 54 Assigns a bug_id or a group_id if there is a matching group, 55 otherwise creates a new group for that anomaly. 56 57 Args: 58 alerts: A list of Alerts. 59 test_suite: The test suite name for |alerts|. 60 kind: The kind string of the given alert entity. 61 """ 62 if not alerts: 63 return 64 alerts = [a for a in alerts if not getattr(a, 'is_improvement', False)] 65 alerts = sorted(alerts, key=lambda a: a.end_revision) 66 if not alerts: 67 return 68 groups = _FetchAlertGroups(alerts[-1].end_revision) 69 for alert_entity in alerts: 70 if not _FindAlertGroup(alert_entity, groups, test_suite, kind): 71 _CreateGroupForAlert(alert_entity, test_suite, kind) 72 73 74def _FetchAlertGroups(max_start_revision): 75 """Fetches AlertGroup entities up to a given revision.""" 76 query = AlertGroup.query(AlertGroup.start_revision <= max_start_revision) 77 query = query.order(-AlertGroup.start_revision) 78 groups = query.fetch(limit=_MAX_GROUPS_TO_FETCH) 79 80 return groups 81 82 83def _FindAlertGroup(alert_entity, groups, test_suite, kind): 84 """Finds and assigns a group for |alert_entity|. 85 86 An alert should only be assigned an existing group if the group if 87 the other alerts in the group are of the same kind, which should be 88 the case if the alert_kind property of the group matches the alert's 89 kind. 90 91 Args: 92 alert_entity: Alert to find group for. 93 groups: List of AlertGroup. 94 test_suite: The test suite of |alert_entity|. 95 kind: The kind string of the given alert entity. 96 97 Returns: 98 True if a group is found and assigned, False otherwise. 99 """ 100 for group in groups: 101 if (_IsOverlapping(alert_entity, group.start_revision, group.end_revision) 102 and group.alert_kind == kind 103 and test_suite in group.test_suites): 104 _AddAlertToGroup(alert_entity, group) 105 return True 106 return False 107 108 109def _CreateGroupForAlert(alert_entity, test_suite, kind): 110 """Creates an AlertGroup for |alert_entity|.""" 111 group = AlertGroup() 112 group.start_revision = alert_entity.start_revision 113 group.end_revision = alert_entity.end_revision 114 group.test_suites = [test_suite] 115 group.alert_kind = kind 116 group.put() 117 alert_entity.group = group.key 118 logging.debug('Auto triage: Created group %s.', group) 119 120 121def _AddAlertToGroup(alert_entity, group): 122 """Adds an anomaly to group and updates the group's properties.""" 123 update_group = False 124 if alert_entity.start_revision > group.start_revision: 125 # TODO(qyearsley): Add test coverage. See catapult:#1346. 126 group.start_revision = alert_entity.start_revision 127 update_group = True 128 if alert_entity.end_revision < group.end_revision: 129 group.end_revision = alert_entity.end_revision 130 update_group = True 131 if update_group: 132 group.put() 133 134 if group.bug_id: 135 alert_entity.bug_id = group.bug_id 136 _AddLogForBugAssociate(alert_entity, group.bug_id) 137 alert_entity.group = group.key 138 logging.debug('Auto triage: Associated anomaly on %s with %s.', 139 utils.TestPath(alert_entity.GetTestMetadataKey()), 140 group.key.urlsafe()) 141 142 143def _IsOverlapping(alert_entity, start, end): 144 """Whether |alert_entity| overlaps with |start| and |end| revision range.""" 145 return (alert_entity.start_revision <= end and 146 alert_entity.end_revision >= start) 147 148 149def _AddLogForBugAssociate(anomaly_entity, bug_id): 150 """Adds a log for associating alert with a bug.""" 151 sheriff = anomaly_entity.GetTestMetadataKey().get().sheriff 152 if not sheriff: 153 return 154 # TODO(qyearsley): Add test coverage. See catapult:#1346. 155 sheriff = sheriff.string_id() 156 bug_url = ('https://chromeperf.appspot.com/group_report?bug_id=' + 157 str(bug_id)) 158 test_path = utils.TestPath(anomaly_entity.GetTestMetadataKey()) 159 html_str = ('Associated alert on %s with bug <a href="%s">%s</a>.' % 160 (test_path, bug_url, bug_id)) 161 formatter = quick_logger.Formatter() 162 logger = quick_logger.QuickLogger('auto_triage', sheriff, formatter) 163 logger.Log(html_str) 164 logger.Save() 165