OLD | NEW |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import alerts_history | 5 import alerts_history |
6 import datetime_encoder | |
7 import hashlib | |
8 import json | 6 import json |
9 import logging | 7 import logging |
| 8 import utils |
10 import webapp2 | 9 import webapp2 |
11 import zlib | 10 import zlib |
12 | 11 |
13 from datetime import datetime as dt | 12 from datetime import datetime as dt |
14 from google.appengine.api import memcache | 13 from google.appengine.api import memcache |
15 from google.appengine.api import users | 14 from google.appengine.api import users |
16 from google.appengine.datastore import datastore_query | |
17 from google.appengine.ext import ndb | 15 from google.appengine.ext import ndb |
18 | 16 |
19 | 17 |
20 ALLOWED_APP_IDS = ('google.com:monarch-email-alerts-parser') | 18 ALLOWED_APP_IDS = ('google.com:monarch-email-alerts-parser') |
21 INBOUND_APP_ID = 'X-Appengine-Inbound-Appid' | 19 INBOUND_APP_ID = 'X-Appengine-Inbound-Appid' |
22 | 20 |
23 | 21 |
24 def is_googler(): | |
25 user = users.get_current_user() | |
26 if user: | |
27 email = user.email() | |
28 return email.endswith('@google.com') and '+' not in email | |
29 return False | |
30 | |
31 | |
32 def convert_to_secs(duration_str): | |
33 duration_str = duration_str.strip() | |
34 if duration_str[-1] == 's': | |
35 return int(duration_str[:-1]) | |
36 elif duration_str[-1] == 'm': | |
37 return 60 * int(duration_str[:-1]) | |
38 elif duration_str[-1] == 'h': | |
39 return 3600 * int(duration_str[:-1]) | |
40 elif duration_str[-1] == 'd': | |
41 return 24 * 3600 * int(duration_str[:-1]) | |
42 elif duration_str[-1] == 'w': | |
43 return 7 * 24 * 3600 * int(duration_str[:-1]) | |
44 else: | |
45 raise Exception('Invalid duration_str ' + duration_str[-1]) | |
46 | |
47 | |
48 def secs_ago(time_string, time_now=None): | |
49 try: | |
50 time_sent = dt.strptime(time_string, '%Y-%m-%d %H:%M:%S %Z') | |
51 except ValueError: | |
52 time_sent = dt.strptime(time_string, '%Y-%m-%d %H:%M:%S') | |
53 time_now = time_now or int(dt.utcnow().strftime('%s')) | |
54 latency = int(time_now) - int(time_sent.strftime('%s')) | |
55 return latency | |
56 | |
57 | |
58 def hash_string(input_str): | |
59 return hashlib.sha1(input_str).hexdigest() | |
60 | |
61 | |
62 def generate_json_dump(alerts, human_readable=True): | |
63 if human_readable: | |
64 return json.dumps(alerts, cls=datetime_encoder.DateTimeEncoder, | |
65 indent=2, | |
66 separators=(',', ': ')) | |
67 return json.dumps(alerts, cls=datetime_encoder.DateTimeEncoder, | |
68 indent=None, | |
69 separators=(',', ':')) | |
70 | |
71 | |
72 class TSAlertsJSON(ndb.Model): | 22 class TSAlertsJSON(ndb.Model): |
73 active_until = ndb.DateTimeProperty() | 23 active_until = ndb.DateTimeProperty() |
74 json = ndb.JsonProperty(compressed=True) | 24 json = ndb.JsonProperty(compressed=True) |
75 | 25 |
76 @classmethod | 26 @classmethod |
77 def query_active(cls): | 27 def query_active(cls): |
78 return cls.query().filter(TSAlertsJSON.active_until == None) | 28 return cls.query().filter(TSAlertsJSON.active_until == None) |
79 | 29 |
80 @classmethod | 30 @classmethod |
81 def query_hash(cls, key): | 31 def query_hash(cls, key): |
82 return cls.get_by_id(key) | 32 return cls.get_by_id(key) |
83 | 33 |
84 | 34 |
85 class TimeSeriesAlertsHandler(webapp2.RequestHandler): | 35 class TimeSeriesAlertsHandler(webapp2.RequestHandler): |
86 ALERT_TYPE = 'ts-alerts' | 36 ALERT_TYPE = 'ts-alerts' |
87 MEMCACHE_COMPRESSION_LEVEL = 9 | 37 MEMCACHE_COMPRESSION_LEVEL = 9 |
88 # Alerts which have continued to fire are re-sent every 5 minutes, so stale | 38 # Alerts which have continued to fire are re-sent every 5 minutes, so stale |
89 # alerts older than 300 seconds are replaced by incoming alerts. | 39 # alerts older than 300 seconds are replaced by incoming alerts. |
90 STALE_ALERT_TIMEOUT = 300 | 40 STALE_ALERT_TIMEOUT = 300 |
91 | 41 |
92 def get(self, key=None): | 42 def get(self, key=None): |
| 43 utils.increment_monarch('ts-alerts') |
93 self.remove_expired_alerts() | 44 self.remove_expired_alerts() |
94 if not users.get_current_user(): | 45 if not users.get_current_user(): |
95 results = {'date': dt.utcnow(), | 46 results = {'date': dt.utcnow(), |
96 'redirect-url': users.create_login_url(self.request.uri)} | 47 'redirect-url': users.create_login_url(self.request.uri)} |
97 self.write_json(results) | 48 self.write_json(results) |
98 return | 49 return |
99 | 50 |
100 if key: | 51 if key: |
101 logging.info('getting the key: ' + key) | 52 logging.info('getting the key: ' + key) |
102 try: | 53 try: |
103 data = memcache.get(key) or TSAlertsJSON.query_hash(key).json | 54 data = memcache.get(key) or TSAlertsJSON.query_hash(key).json |
104 except AttributeError: | 55 except AttributeError: |
105 self.response.write('This alert does not exist.') | 56 self.response.write('This alert does not exist.') |
106 self.response.set_status(404, 'Alert does not exist') | 57 self.response.set_status(404, 'Alert does not exist') |
107 return | 58 return |
108 if not data: | 59 if not data: |
109 self.response.write('This alert does not exist.') | 60 self.response.write('This alert does not exist.') |
110 self.response.set_status(404, 'Alert does not exist') | 61 self.response.set_status(404, 'Alert does not exist') |
111 elif data.get('private', True) and not is_googler(): | 62 elif data.get('private', True) and not utils.is_googler(): |
112 logging.info('Permission denied.') | 63 logging.info('Permission denied.') |
113 self.abort(403) | 64 self.abort(403) |
114 else: | 65 else: |
115 self.write_json(data.get(json, data)) | 66 self.write_json(data.get(json, data)) |
116 else: | 67 else: |
117 query = TSAlertsJSON.query_active().fetch() | 68 query = TSAlertsJSON.query_active().fetch() |
118 data = [] | 69 data = [] |
119 for item in query: | 70 for item in query: |
120 if item.json.get('private', True) and not is_googler(): | 71 if item.json.get('private', True) and not utils.is_googler(): |
121 continue | 72 continue |
122 data.append(item.json) | 73 data.append(item.json) |
123 self.write_json({'alerts': data}) | 74 self.write_json({'alerts': data}) |
124 | 75 |
125 def post(self): | 76 def post(self): |
126 app_id = self.request.headers.get(INBOUND_APP_ID, None) | 77 app_id = self.request.headers.get(INBOUND_APP_ID, None) |
127 if app_id not in ALLOWED_APP_IDS: | 78 if app_id not in ALLOWED_APP_IDS: |
128 logging.info('Permission denied') | 79 logging.info('Permission denied') |
129 self.abort(403) | 80 self.abort(403) |
130 return | 81 return |
131 self.update_alerts() | 82 self.update_alerts() |
132 | 83 |
133 def put(self, key): | 84 def put(self, key): |
134 if not is_googler(): | 85 if not utils.is_googler(): |
135 self.response.set_status(403, 'Permission Denied') | 86 self.response.set_status(403, 'Permission Denied') |
136 return | 87 return |
137 changed_alert = TSAlertsJSON.query_hash(key) | 88 changed_alert = TSAlertsJSON.query_hash(key) |
138 if not changed_alert: | 89 if not changed_alert: |
139 self.response.write('This alert does not exist.') | 90 self.response.write('This alert does not exist.') |
140 self.response.set_status(404, 'Alert does not exist') | 91 self.response.set_status(404, 'Alert does not exist') |
141 return | 92 return |
142 try: | 93 try: |
143 data = json.loads(self.request.body) | 94 data = json.loads(self.request.body) |
144 except ValueError: | 95 except ValueError: |
145 warning = ('Content %s was not valid JSON string.', self.request.body) | 96 warning = ('Content %s was not valid JSON string.', self.request.body) |
146 self.response.set_status(400, warning) | 97 self.response.set_status(400, warning) |
147 return | 98 return |
148 logging.info('Alert before: ' + str(changed_alert)) | 99 logging.info('Alert before: ' + str(changed_alert)) |
149 logging.info('Data: ' + str(data)) | 100 logging.info('Data: ' + str(data)) |
150 changed_alert.json.update(data) | 101 changed_alert.json.update(data) |
151 logging.info('Alert after: ' + str(changed_alert)) | 102 logging.info('Alert after: ' + str(changed_alert)) |
152 changed_alert.put() | 103 changed_alert.put() |
153 memcache.set(key, changed_alert.json) | 104 memcache.set(key, changed_alert.json) |
154 self.response.write("Updated ts-alerts.") | 105 self.response.write("Updated ts-alerts.") |
155 | 106 |
156 def delete(self, key): | 107 def delete(self, key): |
157 if not is_googler(): | 108 if not utils.is_googler(): |
158 self.response.set_status(403, 'Permission Denied') | 109 self.response.set_status(403, 'Permission Denied') |
159 return | 110 return |
160 if key == 'all': | 111 if key == 'all': |
161 all_keys = TSAlertsJSON.query().fetch(keys_only=True) | 112 all_keys = TSAlertsJSON.query().fetch(keys_only=True) |
162 ndb.delete_multi(all_keys) | 113 ndb.delete_multi(all_keys) |
163 for k in all_keys: | 114 for k in all_keys: |
164 logging.info('deleting key from memcache: ' + k.id()) | 115 logging.info('deleting key from memcache: ' + k.id()) |
165 memcache.delete(k.id()) | 116 memcache.delete(k.id()) |
166 self.response.set_status(200, 'Cleared all alerts') | 117 self.response.set_status(200, 'Cleared all alerts') |
167 return | 118 return |
168 changed_alert = TSAlertsJSON.query_hash(key) | 119 changed_alert = TSAlertsJSON.query_hash(key) |
169 if not changed_alert: | 120 if not changed_alert: |
170 self.response.write('This alert does not exist.') | 121 self.response.write('This alert does not exist.') |
171 self.response.set_status(404, 'Alert does not exist') | 122 self.response.set_status(404, 'Alert does not exist') |
172 return | 123 return |
173 memcache.delete(key) | 124 memcache.delete(key) |
174 changed_alert.key.delete() | 125 changed_alert.key.delete() |
175 | 126 |
176 def write_json(self, data): | 127 def write_json(self, data): |
177 self.response.headers['Access-Control-Allow-Origin'] = '*' | 128 self.response.headers['Access-Control-Allow-Origin'] = '*' |
178 self.response.headers['Content-Type'] = 'application/json' | 129 self.response.headers['Content-Type'] = 'application/json' |
179 data = generate_json_dump(data) | 130 data = utils.generate_json_dump(data) |
180 self.response.write(data) | 131 self.response.write(data) |
181 | 132 |
182 def remove_expired_alerts(self): | 133 def remove_expired_alerts(self): |
183 active_alerts = TSAlertsJSON.query_active().fetch() | 134 active_alerts = TSAlertsJSON.query_active().fetch() |
184 | 135 |
185 for alert in active_alerts: | 136 for alert in active_alerts: |
186 alert_age = secs_ago(alert.json['alert_sent_utc']) | 137 alert_age = utils.secs_ago(alert.json['alert_sent_utc']) |
187 if alert_age > self.STALE_ALERT_TIMEOUT: | 138 if alert_age > self.STALE_ALERT_TIMEOUT: |
188 logging.info('%s expired. alert age: %d.', alert.key.id(), alert_age) | 139 logging.info('%s expired. alert age: %d.', alert.key.id(), alert_age) |
189 alert.active_until = dt.utcnow() | 140 alert.active_until = dt.utcnow() |
190 alert.json['active_until'] = dt.strftime(alert.active_until, '%s') | 141 alert.json['active_until'] = dt.strftime(alert.active_until, '%s') |
191 alert.put() | 142 alert.put() |
192 memcache.set(alert.key.id(), alert.json) | 143 memcache.set(alert.key.id(), alert.json) |
193 | 144 |
194 def update_alerts(self): | 145 def update_alerts(self): |
195 self.remove_expired_alerts() | 146 self.remove_expired_alerts() |
196 try: | 147 try: |
197 alerts = json.loads(self.request.body) | 148 alerts = json.loads(self.request.body) |
198 except ValueError: | 149 except ValueError: |
199 warning = 'Content field was not valid JSON string.' | 150 warning = 'Content field was not valid JSON string.' |
200 self.response.set_status(400, warning) | 151 self.response.set_status(400, warning) |
201 logging.warning(warning) | 152 logging.warning(warning) |
202 return | 153 return |
203 if alerts: | 154 if alerts: |
204 self.store_alerts(alerts) | 155 self.store_alerts(alerts) |
205 | 156 |
206 def store_alerts(self, alert): | 157 def store_alerts(self, alert): |
207 hash_key = hash_string(alert['mash_expression'] + alert['active_since']) | 158 pre_hash_string = alert['mash_expression'] + alert['active_since'] |
| 159 hash_key = utils.hash_string(pre_hash_string) |
208 alert['hash_key'] = hash_key | 160 alert['hash_key'] = hash_key |
209 | 161 |
210 new_entry = TSAlertsJSON( | 162 new_entry = TSAlertsJSON( |
211 id=hash_key, | 163 id=hash_key, |
212 json=alert, | 164 json=alert, |
213 active_until=None) | 165 active_until=None) |
214 new_entry.put() | 166 new_entry.put() |
215 memcache.set(hash_key, alert) | 167 memcache.set(hash_key, alert) |
216 | 168 |
217 def set_memcache(self, key, data): | 169 def set_memcache(self, key, data): |
218 json_data = generate_json_dump(data, False) | 170 json_data = utils.generate_json_dump(data, False) |
219 compression_level = self.MEMCACHE_COMPRESSION_LEVEL | 171 compression_level = self.MEMCACHE_COMPRESSION_LEVEL |
220 compressed = zlib.compress(json_data, compression_level) | 172 compressed = zlib.compress(json_data, compression_level) |
221 memcache.set(key, compressed) | 173 memcache.set(key, compressed) |
222 | 174 |
223 | 175 |
224 class TimeSeriesAlertsHistory(alerts_history.AlertsHistory): | 176 class TimeSeriesAlertsHistory(alerts_history.AlertsHistory): |
225 | 177 |
226 def get(self, timestamp=None): | 178 def get(self, timestamp=None): |
| 179 utils.increment_monarch('ts-alerts-history') |
227 result_json = {} | 180 result_json = {} |
228 if not users.get_current_user(): | 181 if not users.get_current_user(): |
229 result_json['login-url'] = users.create_login_url(self.request.uri) | 182 result_json['login-url'] = users.create_login_url(self.request.uri) |
230 return result_json | 183 return result_json |
231 | 184 |
232 alerts = TSAlertsJSON.query_active().fetch() | 185 alerts = TSAlertsJSON.query_active().fetch() |
233 if timestamp: | 186 if timestamp: |
234 try: | 187 try: |
235 time = dt.fromtimestamp(int(timestamp)) | 188 time = dt.fromtimestamp(int(timestamp)) |
236 except ValueError: | 189 except ValueError: |
237 self.response.set_status(400, 'Invalid timestamp.') | 190 self.response.set_status(400, 'Invalid timestamp.') |
238 return | 191 return |
239 if time > dt.utcnow(): | 192 if time > dt.utcnow(): |
240 self.response.write('Sheriff-o-matic cannot predict the future... yet.') | 193 self.response.write('Sheriff-o-matic cannot predict the future... yet.') |
241 self.response.set_status(400, 'Invalid timestamp.') | 194 self.response.set_status(400, 'Invalid timestamp.') |
242 else: | 195 else: |
243 time = dt.utcnow() | 196 time = dt.utcnow() |
244 alerts += TSAlertsJSON.query(TSAlertsJSON.active_until > time).fetch() | 197 alerts += TSAlertsJSON.query(TSAlertsJSON.active_until > time).fetch() |
245 | 198 |
246 history = [] | 199 history = [] |
247 for a in alerts: | 200 for a in alerts: |
248 ts, private = timestamp, a.json['private'] | 201 ts, private = timestamp, a.json['private'] |
249 in_range = not (ts and secs_ago(a.json['active_since_utc'], ts) < 0) | 202 in_range = not (ts and utils.secs_ago(a.json['active_since_utc'], ts) < 0) |
250 permission = is_googler() or not private | 203 permission = utils.is_googler() or not private |
251 if in_range and permission: | 204 if in_range and permission: |
252 history.append(a.json) | 205 history.append(a.json) |
253 | 206 |
254 result_json.update({ | 207 result_json.update({ |
255 'timestamp': time.strftime('%s'), | 208 'timestamp': time.strftime('%s'), |
256 'time_string': time.strftime('%Y-%m-%d %H:%M:%S %Z'), | 209 'time_string': time.strftime('%Y-%m-%d %H:%M:%S %Z'), |
257 'active_alerts': history | 210 'active_alerts': history |
258 }) | 211 }) |
259 | 212 |
260 self.write_json(result_json) | 213 self.write_json(result_json) |
261 | 214 |
262 def write_json(self, data): | 215 def write_json(self, data): |
263 self.response.headers['Access-Control-Allow-Origin'] = '*' | 216 self.response.headers['Access-Control-Allow-Origin'] = '*' |
264 self.response.headers['Content-Type'] = 'application/json' | 217 self.response.headers['Content-Type'] = 'application/json' |
265 data = generate_json_dump(data) | 218 data = utils.generate_json_dump(data) |
266 self.response.write(data) | 219 self.response.write(data) |
267 | 220 |
268 | 221 |
269 app = webapp2.WSGIApplication([ | 222 app = webapp2.WSGIApplication([ |
270 ('/ts-alerts', TimeSeriesAlertsHandler), | 223 ('/ts-alerts', TimeSeriesAlertsHandler), |
271 ('/ts-alerts/(.*)', TimeSeriesAlertsHandler), | 224 ('/ts-alerts/(.*)', TimeSeriesAlertsHandler), |
272 ('/ts-alerts-history', TimeSeriesAlertsHistory), | 225 ('/ts-alerts-history', TimeSeriesAlertsHistory), |
273 ('/ts-alerts-history/(.*)', TimeSeriesAlertsHistory)]) | 226 ('/ts-alerts-history/(.*)', TimeSeriesAlertsHistory)]) |
OLD | NEW |