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"""Provides the web interface for displaying an overview of alerts.""" 6 7import json 8import logging 9 10from google.appengine.ext import ndb 11 12from dashboard import email_template 13from dashboard import request_handler 14from dashboard import utils 15from dashboard.models import anomaly 16from dashboard.models import sheriff 17from dashboard.models import stoppage_alert 18 19_MAX_ANOMALIES_TO_COUNT = 5000 20_MAX_ANOMALIES_TO_SHOW = 500 21_MAX_STOPPAGE_ALERTS = 500 22 23 24class AlertsHandler(request_handler.RequestHandler): 25 """Shows an overview of recent anomalies for perf sheriffing.""" 26 27 def get(self): 28 """Renders the UI for listing alerts.""" 29 self.RenderStaticHtml('alerts.html') 30 31 def post(self): 32 """Returns dynamic data for listing alerts in response to XHR. 33 34 Request parameters: 35 sheriff: The name of a sheriff (optional). 36 triaged: Whether to include triaged alerts (i.e. with a bug ID). 37 improvements: Whether to include improvement anomalies. 38 39 Outputs: 40 JSON data for an XHR request to show a table of alerts. 41 """ 42 sheriff_name = self.request.get('sheriff', 'Chromium Perf Sheriff') 43 sheriff_key = ndb.Key('Sheriff', sheriff_name) 44 if not _SheriffIsFound(sheriff_key): 45 self.response.out.write(json.dumps({ 46 'error': 'Sheriff "%s" not found.' % sheriff_name 47 })) 48 return 49 50 include_improvements = bool(self.request.get('improvements')) 51 include_triaged = bool(self.request.get('triaged')) 52 53 anomaly_keys = _FetchAnomalyKeys( 54 sheriff_key, include_improvements, include_triaged) 55 anomalies = ndb.get_multi(anomaly_keys[:_MAX_ANOMALIES_TO_SHOW]) 56 stoppage_alerts = _FetchStoppageAlerts(sheriff_key, include_triaged) 57 58 values = { 59 'anomaly_list': AnomalyDicts(anomalies), 60 'stoppage_alert_list': StoppageAlertDicts(stoppage_alerts), 61 'sheriff_list': _GetSheriffList(), 62 } 63 self.GetDynamicVariables(values) 64 self.response.out.write(json.dumps(values)) 65 66 67def _SheriffIsFound(sheriff_key): 68 """Checks whether the sheriff can be found for the current user.""" 69 try: 70 sheriff_entity = sheriff_key.get() 71 except AssertionError: 72 # This assertion is raised in InternalOnlyModel._post_get_hook, 73 # and indicates an internal-only Sheriff but an external user. 74 return False 75 return sheriff_entity is not None 76 77 78def _FetchAnomalyKeys(sheriff_key, include_improvements, include_triaged): 79 """Fetches the list of Anomaly keys that may be shown. 80 81 Args: 82 sheriff_key: The ndb.Key for the Sheriff to fetch alerts for. 83 include_improvements: Whether to include improvement Anomalies. 84 include_triaged: Whether to include Anomalies with a bug ID already set. 85 86 Returns: 87 A list of Anomaly keys, in reverse-chronological order. 88 """ 89 query = anomaly.Anomaly.query( 90 anomaly.Anomaly.sheriff == sheriff_key) 91 92 if not include_improvements: 93 query = query.filter( 94 anomaly.Anomaly.is_improvement == False) 95 96 if not include_triaged: 97 query = query.filter( 98 anomaly.Anomaly.bug_id == None) 99 query = query.filter( 100 anomaly.Anomaly.recovered == False) 101 102 query = query.order(-anomaly.Anomaly.timestamp) 103 return query.fetch(limit=_MAX_ANOMALIES_TO_COUNT, keys_only=True) 104 105 106def _FetchStoppageAlerts(sheriff_key, include_triaged): 107 """Fetches the list of Anomaly keys that may be shown. 108 109 Args: 110 sheriff_key: The ndb.Key for the Sheriff to fetch alerts for. 111 include_triaged: Whether to include alerts with a bug ID. 112 113 Returns: 114 A list of StoppageAlert entities, in reverse-chronological order. 115 """ 116 query = stoppage_alert.StoppageAlert.query( 117 stoppage_alert.StoppageAlert.sheriff == sheriff_key) 118 119 if not include_triaged: 120 query = query.filter( 121 stoppage_alert.StoppageAlert.bug_id == None) 122 query = query.filter( 123 stoppage_alert.StoppageAlert.recovered == False) 124 125 query = query.order(-stoppage_alert.StoppageAlert.timestamp) 126 return query.fetch(limit=_MAX_STOPPAGE_ALERTS) 127 128 129def _GetSheriffList(): 130 """Returns a list of sheriff names for all sheriffs in the datastore.""" 131 sheriff_keys = sheriff.Sheriff.query().fetch(keys_only=True) 132 return [key.string_id() for key in sheriff_keys] 133 134 135def AnomalyDicts(anomalies): 136 """Makes a list of dicts with properties of Anomaly entities.""" 137 bisect_statuses = _GetBisectStatusDict(anomalies) 138 return [GetAnomalyDict(a, bisect_statuses.get(a.bug_id)) for a in anomalies] 139 140 141def StoppageAlertDicts(stoppage_alerts): 142 """Makes a list of dicts with properties of StoppageAlert entities.""" 143 return [_GetStoppageAlertDict(a) for a in stoppage_alerts] 144 145 146def GetAnomalyDict(anomaly_entity, bisect_status=None): 147 """Returns a dictionary for an Anomaly which can be encoded as JSON. 148 149 Args: 150 anomaly_entity: An Anomaly entity. 151 bisect_status: String status of bisect run. 152 153 Returns: 154 A dictionary which is safe to be encoded as JSON. 155 """ 156 alert_dict = _AlertDict(anomaly_entity) 157 alert_dict.update({ 158 'median_after_anomaly': anomaly_entity.median_after_anomaly, 159 'median_before_anomaly': anomaly_entity.median_before_anomaly, 160 'percent_changed': '%s' % anomaly_entity.GetDisplayPercentChanged(), 161 'improvement': anomaly_entity.is_improvement, 162 'bisect_status': bisect_status, 163 'recovered': anomaly_entity.recovered, 164 }) 165 return alert_dict 166 167 168def _GetStoppageAlertDict(stoppage_alert_entity): 169 """Returns a dictionary of properties of a stoppage alert.""" 170 alert_dict = _AlertDict(stoppage_alert_entity) 171 last_row_date = stoppage_alert_entity.last_row_date 172 if not last_row_date: 173 logging.error('No date for StoppageAlert:\n%s', stoppage_alert_entity) 174 alert_dict.update({ 175 'mail_sent': stoppage_alert_entity.mail_sent, 176 'last_row_date': str(last_row_date.date()) if last_row_date else 'N/A', 177 'recovered': stoppage_alert_entity.recovered, 178 }) 179 return alert_dict 180 181 182def _AlertDict(alert_entity): 183 """Returns a base dictionary with properties common to all alerts.""" 184 test_path = utils.TestPath(alert_entity.test) 185 test_path_parts = test_path.split('/') 186 dashboard_link = email_template.GetReportPageLink( 187 test_path, rev=alert_entity.end_revision, add_protocol_and_host=False) 188 return { 189 'key': alert_entity.key.urlsafe(), 190 'group': alert_entity.group.urlsafe() if alert_entity.group else None, 191 'start_revision': alert_entity.start_revision, 192 'end_revision': alert_entity.end_revision, 193 'date': str(alert_entity.timestamp.date()), 194 'master': test_path_parts[0], 195 'bot': test_path_parts[1], 196 'testsuite': test_path_parts[2], 197 'test': '/'.join(test_path_parts[3:]), 198 'bug_id': alert_entity.bug_id, 199 'dashboard_link': dashboard_link, 200 } 201 202 203def _GetBisectStatusDict(anomalies): 204 """Returns a dictionary of bug ID to bisect status string.""" 205 bug_id_list = {a.bug_id for a in anomalies if a.bug_id > 0} 206 bugs = ndb.get_multi(ndb.Key('Bug', b) for b in bug_id_list) 207 return {b.key.id(): b.latest_bisect_status for b in bugs if b} 208