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