| 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 |