1
0
mirror of https://github.com/linkedin/oncall.git synced 2025-11-30 23:44:59 +02:00

Clean/refactor notifier, add user validation (#12)

* Clean/refactor notifier, add user validation

Fix a few notifier bugs, move reminder/user validator to their own
files, add Makefile to docs

* Move reminder/user validator to notifier directory
This commit is contained in:
dwang159
2017-05-08 18:19:30 -07:00
committed by Qingping Hou
parent 9e613b950d
commit 4ed569b8d6
8 changed files with 171 additions and 103 deletions

View File

@@ -53,8 +53,14 @@ notifications:
- "email"
reminder:
activated: True
polling_interval: 360
default_timezone: 'US/Pacific'
user_validator:
activated: True
subject: 'Warning: Missing phone number in Oncall'
body: 'You are scheduled for an on-call shift in the future, but have no phone number recorded. Please update your information in Oncall.'
slack_instance: foobar
header_color: '#3a3a3a'

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Oncall
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -31,6 +31,7 @@ setup(
'pycrypto',
'python-ldap',
'pytz',
'irisclient',
],
entry_points={
'console_scripts': [

View File

@@ -8,17 +8,12 @@ import yaml
import logging
import time
from importlib import import_module
from pytz import timezone
from ujson import loads as json_loads, dumps as json_dumps
from datetime import datetime
from ujson import loads as json_loads
from gevent import queue, spawn, sleep
from oncall import db, metrics, constants
from oncall import db, metrics
from oncall.messengers import init_messengers, send_message
HOUR = 60 * 60
DAY = HOUR * 24
WEEK = DAY * 7
from oncall.notifier import reminder, user_validator
# logging
logger = logging.getLogger()
@@ -43,32 +38,6 @@ send_queue = queue.Queue()
default_timezone = None
def create_reminder(user_id, mode, send_time, context, type_name, cursor):
context = json_dumps(context)
cursor.execute('''INSERT INTO `notification_queue`(`user_id`, `send_time`, `mode_id`, `active`, `context`, `type_id`)
VALUES (%s,
%s,
(SELECT `id` FROM `contact_mode` WHERE `name` = %s),
1,
%s,
(SELECT `id` FROM `notification_type` WHERE `name` = %s))''',
(user_id, send_time, mode, context, type_name))
def timestamp_to_human_str(timestamp, tz):
dt = datetime.fromtimestamp(timestamp, timezone(tz))
return ' '.join([dt.strftime('%Y-%m-%d %H:%M:%S'), tz])
def sec_to_human_str(seconds):
if seconds % WEEK == 0:
return '%d weeks' % (seconds / WEEK)
elif seconds % DAY == 0:
return '%d days' % (seconds / DAY)
else:
return '%d hours' % (seconds / HOUR)
def load_config_file(config_path):
with open(config_path) as h:
config = yaml.load(h)
@@ -167,71 +136,6 @@ def metrics_sender():
sleep(60)
def reminder(config):
interval = config['polling_interval']
default_timezone = config['default_timezone']
connection = db.connect()
cursor = connection.cursor()
cursor.execute('SELECT `last_window_end` FROM `notifier_state`')
if cursor.rowcount != 1:
window_start = int(time.time() - interval)
logger.warning('Corrupted/missing notifier state; unable to determine last window. Guessing %s',
window_start)
else:
window_start = cursor.fetchone()[0]
cursor.close()
connection.close()
query = '''
SELECT `user`.`name`, `user`.`id` AS `user_id`, `time_before`, `contact_mode`.`name` AS `mode`,
`team`.`name` AS `team`, `event`.`start`, `event`.`id`, `role`.`name` AS `role`, `user`.`time_zone`
FROM `user` JOIN `notification_setting` ON `notification_setting`.`user_id` = `user`.`id`
AND `notification_setting`.`type_id` = (SELECT `id` FROM `notification_type`
WHERE `name` = %s)
JOIN `setting_role` ON `notification_setting`.`id` = `setting_role`.`setting_id`
JOIN `event` ON `event`.`start` >= `time_before` + %s AND `event`.`start` < `time_before` + %s
AND `event`.`user_id` = `user`.`id`
AND `event`.`role_id` = `setting_role`.`role_id`
AND `event`.`team_id` = `notification_setting`.`team_id`
JOIN `contact_mode` ON `notification_setting`.`mode_id` = `contact_mode`.`id`
JOIN `team` ON `event`.`team_id` = `team`.`id`
JOIN `role` ON `event`.`role_id` = `role`.`id`
LEFT JOIN `event` AS `e` ON `event`.`link_id` = `e`.`link_id` AND `e`.`start` < `event`.`start`
WHERE `e`.`id` IS NULL
'''
while(1):
logger.info('Reminder polling loop started')
window_end = int(time.time())
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(query, (constants.ONCALL_REMINDER, window_start, window_end))
notifications = cursor.fetchall()
for row in notifications:
context = {'team': row['team'],
'start_time': timestamp_to_human_str(row['start'],
row['time_zone'] if row['time_zone'] else default_timezone),
'time_before': sec_to_human_str(row['time_before']),
'role': row['role']}
create_reminder(row['user_id'], row['mode'], row['start'] - row['time_before'],
context, 'oncall_reminder', cursor)
logger.info('Created reminder with context %s for %s', context, row['name'])
cursor.execute('UPDATE `notifier_state` SET `last_window_end` = %s', window_end)
connection.commit()
logger.info('Created reminders for window [%s, %s), sleeping for %s s', window_start, window_end, interval)
window_start = window_end
cursor.close()
connection.close()
sleep(interval)
def main():
with open(sys.argv[1], 'r') as config_file:
config = yaml.safe_load(config_file)
@@ -246,7 +150,10 @@ def main():
init_messengers(config.get('messengers', []))
worker_tasks = [spawn(worker) for x in xrange(100)]
spawn(reminder, config['reminder'])
if config['reminder']['activated']:
spawn(reminder.reminder, config['reminder'])
if config['user_validator']['activated']:
spawn(user_validator.user_validator, config['user_validator'])
interval = 60

View File

@@ -3,15 +3,14 @@
from oncall.constants import EMAIL_SUPPORT, SMS_SUPPORT, CALL_SUPPORT
from iris.client import IrisClient
from irisclient import IrisClient
class iris_messenger(object):
supports = frozenset([EMAIL_SUPPORT, SMS_SUPPORT, CALL_SUPPORT])
def __init__(self, config):
self.config = config
self.iris_client = IrisClient(config['application'], config['iris_api_key'])
self.iris_client = IrisClient(config['application'], config['iris_api_key'], config['api_host'])
def send(self, message):
self.iris_client.notification(role='user', target=message['user'], priority=message.get('priority'),

View File

View File

@@ -0,0 +1,104 @@
import time
import logging
from gevent import sleep
from ujson import dumps as json_dumps
from datetime import datetime
from pytz import timezone
from oncall import db, constants
logger = logging.getLogger(__name__)
HOUR = 60 * 60
DAY = HOUR * 24
WEEK = DAY * 7
def create_reminder(user_id, mode, send_time, context, type_name, cursor):
context = json_dumps(context)
cursor.execute('''INSERT INTO `notification_queue`(`user_id`, `send_time`, `mode_id`, `active`, `context`, `type_id`)
VALUES (%s,
%s,
(SELECT `id` FROM `contact_mode` WHERE `name` = %s),
1,
%s,
(SELECT `id` FROM `notification_type` WHERE `name` = %s))''',
(user_id, send_time, mode, context, type_name))
def timestamp_to_human_str(timestamp, tz):
dt = datetime.fromtimestamp(timestamp, timezone(tz))
return ' '.join([dt.strftime('%Y-%m-%d %H:%M:%S'), tz])
def sec_to_human_str(seconds):
if seconds % WEEK == 0:
return '%d weeks' % (seconds / WEEK)
elif seconds % DAY == 0:
return '%d days' % (seconds / DAY)
else:
return '%d hours' % (seconds / HOUR)
def reminder(config):
interval = config['polling_interval']
default_timezone = config['default_timezone']
connection = db.connect()
cursor = connection.cursor()
cursor.execute('SELECT `last_window_end` FROM `notifier_state`')
if cursor.rowcount != 1:
window_start = int(time.time() - interval)
logger.warning('Corrupted/missing notifier state; unable to determine last window. Guessing %s',
window_start)
else:
window_start = cursor.fetchone()[0]
cursor.close()
connection.close()
query = '''
SELECT `user`.`name`, `user`.`id` AS `user_id`, `time_before`, `contact_mode`.`name` AS `mode`,
`team`.`name` AS `team`, `event`.`start`, `event`.`id`, `role`.`name` AS `role`, `user`.`time_zone`
FROM `user` JOIN `notification_setting` ON `notification_setting`.`user_id` = `user`.`id`
AND `notification_setting`.`type_id` = (SELECT `id` FROM `notification_type`
WHERE `name` = %s)
JOIN `setting_role` ON `notification_setting`.`id` = `setting_role`.`setting_id`
JOIN `event` ON `event`.`start` >= `time_before` + %s AND `event`.`start` < `time_before` + %s
AND `event`.`user_id` = `user`.`id`
AND `event`.`role_id` = `setting_role`.`role_id`
AND `event`.`team_id` = `notification_setting`.`team_id`
JOIN `contact_mode` ON `notification_setting`.`mode_id` = `contact_mode`.`id`
JOIN `team` ON `event`.`team_id` = `team`.`id`
JOIN `role` ON `event`.`role_id` = `role`.`id`
LEFT JOIN `event` AS `e` ON `event`.`link_id` = `e`.`link_id` AND `e`.`start` < `event`.`start`
WHERE `e`.`id` IS NULL
'''
while(1):
logger.info('Reminder polling loop started')
window_end = int(time.time())
connection = db.connect()
cursor = connection.cursor(db.DictCursor)
cursor.execute(query, (constants.ONCALL_REMINDER, window_start, window_end))
notifications = cursor.fetchall()
for row in notifications:
context = {'team': row['team'],
'start_time': timestamp_to_human_str(row['start'],
row['time_zone'] if row['time_zone'] else default_timezone),
'time_before': sec_to_human_str(row['time_before']),
'role': row['role']}
create_reminder(row['user_id'], row['mode'], row['start'] - row['time_before'],
context, 'oncall_reminder', cursor)
logger.info('Created reminder with context %s for %s', context, row['name'])
cursor.execute('UPDATE `notifier_state` SET `last_window_end` = %s', window_end)
connection.commit()
logger.info('Created reminders for window [%s, %s), sleeping for %s s', window_start, window_end, interval)
window_start = window_end
cursor.close()
connection.close()
sleep(interval)

View File

@@ -0,0 +1,31 @@
import logging
from gevent import sleep
from oncall import db, messengers
logger = logging.getLogger(__name__)
def user_validator(config):
subject = config['subject']
body = config['body']
sleep_time = config.get('interval', 86400)
while 1:
connection = db.connect()
cursor = connection.cursor()
cursor.execute('''SELECT `user`.`name`
FROM `event` LEFT JOIN `user_contact` ON `event`.`user_id` = `user_contact`.`user_id`
AND `user_contact`.`mode_id` = (SELECT `id` FROM `contact_mode` WHERE `name` = 'call')
JOIN `user` ON `event`.`user_id` = `user`.`id`
WHERE `event`.`start` > UNIX_TIMESTAMP() AND `user_contact`.`destination` IS NULL
GROUP BY `event`.`user_id`;''')
for row in cursor:
message = {'user': row[0],
'mode': 'email',
'subject': subject,
'body': body}
messengers.send_message(message)
connection.close()
cursor.close()
sleep(sleep_time)