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