diff --git a/static/images/integrations/logos/updown.png b/static/images/integrations/logos/updown.png new file mode 100644 index 0000000000..72a999a0a2 Binary files /dev/null and b/static/images/integrations/logos/updown.png differ diff --git a/static/images/integrations/updown/001.png b/static/images/integrations/updown/001.png new file mode 100644 index 0000000000..8cce8bf8b3 Binary files /dev/null and b/static/images/integrations/updown/001.png differ diff --git a/templates/zerver/integrations.html b/templates/zerver/integrations.html index 1b5bf69ee5..d176c500b8 100644 --- a/templates/zerver/integrations.html +++ b/templates/zerver/integrations.html @@ -280,6 +280,12 @@ Twitter +
+ Updown
+
+ See Updown reports in Zulip! This is great to be up to date with + downtime in the services you monitor with Updown! +
+ +First, create the stream you'd like to use for updown
+ notifications, and subscribe all interested parties to this
+ stream. We recommend the name updown.
Go to your Updown
+ settings page and in WEBHOOKS section, enter
+ the following as the URL:
{{ external_api_uri }}/v1/external/updown?api_key=abcdefgh&stream=updown
Congratulations! You're done!
+ Now you'll receive Updown notifications for your service in Zulip.
+
+
+ See your Yo App notifications in Zulip!
diff --git a/zerver/fixtures/updown/updown_check_down_one_event.json b/zerver/fixtures/updown/updown_check_down_one_event.json new file mode 100644 index 0000000000..cf823770a1 --- /dev/null +++ b/zerver/fixtures/updown/updown_check_down_one_event.json @@ -0,0 +1,27 @@ +[{ + "event": "check.down", + "check": { + "token": "ngg8", + "url": "https://updown.io", + "alias": "", + "last_status": 500, + "uptime": 100.0, + "down": true, + "down_since": "2016-02-07T13:11:43Z", + "error": "500", + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "last_check_at": "2016-02-07T13:12:13Z", + "next_check_at": "2016-02-07T13:12:43Z", + "favicon_url": "https://updown.io/favicon.png" + }, + "downtime": { + "error": "500", + "started_at": "2016-02-07T13:11:43Z", + "ended_at": null, + "duration": null + } +}] diff --git a/zerver/fixtures/updown/updown_check_multiple_events.json b/zerver/fixtures/updown/updown_check_multiple_events.json new file mode 100644 index 0000000000..88ab9260be --- /dev/null +++ b/zerver/fixtures/updown/updown_check_multiple_events.json @@ -0,0 +1,56 @@ +[ + { + "event": "check.down", + "check": { + "token": "ngg8", + "url": "https://updown.io", + "alias": "", + "last_status": 200, + "uptime": 99.954, + "down": false, + "down_since": null, + "error": null, + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "last_check_at": "2016-02-07T13:16:07Z", + "next_check_at": "2016-02-07T13:16:37Z", + "favicon_url": "https://updown.io/favicon.png" + }, + "downtime": { + "error": "500", + "started_at": "2016-02-07T13:11:43Z", + "ended_at": null, + "duration": null + } + }, + { + "event": "check.up", + "check": { + "token": "ngg8", + "url": "https://updown.io", + "alias": "", + "last_status": 200, + "uptime": 99.954, + "down": false, + "down_since": null, + "error": null, + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "last_check_at": "2016-02-07T13:16:07Z", + "next_check_at": "2016-02-07T13:16:37Z", + "favicon_url": "https://updown2.io/favicon.png" + }, + "downtime": { + "error": "500", + "started_at": "2016-02-07T13:11:43Z", + "ended_at": "2016-02-07T13:11:44Z", + "duration": 1 + } + } +] diff --git a/zerver/fixtures/updown/updown_check_up_again_one_event.json b/zerver/fixtures/updown/updown_check_up_again_one_event.json new file mode 100644 index 0000000000..22aff8ff80 --- /dev/null +++ b/zerver/fixtures/updown/updown_check_up_again_one_event.json @@ -0,0 +1,27 @@ +[{ + "event": "check.up", + "check": { + "token": "ngg8", + "url": "https://updown.io", + "alias": "", + "last_status": 200, + "uptime": 99.954, + "down": false, + "down_since": null, + "error": null, + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "last_check_at": "2016-02-07T13:16:07Z", + "next_check_at": "2016-02-07T13:16:37Z", + "favicon_url": "https://updown.io/favicon.png" + }, + "downtime": { + "error": "500", + "started_at": "2016-02-07T13:11:43Z", + "ended_at": "2016-02-07T13:16:07Z", + "duration": 265 + } +}] diff --git a/zerver/fixtures/updown/updown_check_up_first_time.json b/zerver/fixtures/updown/updown_check_up_first_time.json new file mode 100644 index 0000000000..a7a9f97d18 --- /dev/null +++ b/zerver/fixtures/updown/updown_check_up_first_time.json @@ -0,0 +1,27 @@ +[{ + "event": "check.up", + "check": { + "token": "ngg8", + "url": "https://updown.io", + "alias": "", + "last_status": 200, + "uptime": 99.954, + "down": false, + "down_since": null, + "error": null, + "period": 30, + "apdex_t": 0.25, + "string_match": "", + "enabled": true, + "published": true, + "last_check_at": "2016-02-07T13:16:07Z", + "next_check_at": "2016-02-07T13:16:37Z", + "favicon_url": "https://updown.io/favicon.png" + }, + "downtime": { + "error": null, + "started_at": null, + "ended_at": null, + "duration": null + } +}] diff --git a/zerver/lib/test_helpers.py b/zerver/lib/test_helpers.py index 412bee1a75..5cbbe4eb8c 100644 --- a/zerver/lib/test_helpers.py +++ b/zerver/lib/test_helpers.py @@ -416,6 +416,9 @@ class AuthedTestCase(TestCase): # type: () -> Message return Message.objects.latest('id') + def get_second_to_last_message(self): + return Message.objects.all().order_by('-id')[1] + def get_all_templates(): # type: () -> List[str] templates = [] diff --git a/zerver/tests/test_hooks.py b/zerver/tests/test_hooks.py index dae9096391..678de0a881 100644 --- a/zerver/tests/test_hooks.py +++ b/zerver/tests/test_hooks.py @@ -1244,3 +1244,39 @@ class AirbrakeHookTests(WebhookTestCase): expected_subject = u"ZulipIntegrationTest" expected_message = u"[ZeroDivisionError](https://zulip.airbrake.io/projects/125209/groups/1705190192091077626): \"Error message from logger\" occurred." self.send_and_test_stream_message('error_message', expected_subject, expected_message) + +class UpdownHookTests(WebhookTestCase): + STREAM_NAME = 'updown' + URL_TEMPLATE = "/api/v1/external/updown?stream={stream}&api_key={api_key}" + FIXTURE_DIR_NAME = 'updown' + + def test_updown_check_down_event(self): + expected_subject = u"https://updown.io" + expected_message = u"Service is `down`. It returned \"500\" error at 07-02-2016 13:11." + self.send_and_test_stream_message('check_down_one_event', expected_subject, expected_message) + + def test_updown_check_up_again_event(self): + expected_subject = u"https://updown.io" + expected_message = u"Service is `up` again after 4 minutes 25 seconds." + self.send_and_test_stream_message('check_up_again_one_event', expected_subject, expected_message) + + def test_updown_check_up_event(self): + expected_subject = u"https://updown.io" + expected_message = u"Service is `up`." + self.send_and_test_stream_message('check_up_first_time', expected_subject, expected_message) + + def test_updown_check_up_multiple_events(self): + first_message_expected_subject = u"https://updown.io" + first_message_expected_message = u"Service is `up` again after 1 second." + + second_message_expected_subject = u"https://updown.io" + second_message_expected_message = u"Service is `down`. It returned \"500\" error at 07-02-2016 13:11." + + self.send_and_test_stream_message('check_multiple_events') + last_message = self.get_last_message() + self.do_test_subject(last_message, first_message_expected_subject) + self.do_test_message(last_message, first_message_expected_message) + + second_to_last_message = self.get_second_to_last_message() + self.do_test_subject(second_to_last_message, second_message_expected_subject) + self.do_test_message(second_to_last_message, second_message_expected_message) diff --git a/zerver/views/webhooks/updown.py b/zerver/views/webhooks/updown.py new file mode 100644 index 0000000000..003aba0452 --- /dev/null +++ b/zerver/views/webhooks/updown.py @@ -0,0 +1,94 @@ +# Webhooks for external integrations. +from __future__ import absolute_import +import re +from datetime import datetime +from typing import Any, Dict +from django.http import HttpRequest, HttpResponse +from django.utils.translation import ugettext as _ +from zerver.lib.actions import check_send_message +from zerver.lib.response import json_success, json_error +from zerver.lib.request import JsonableError +from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view +from zerver.models import UserProfile, Client + + +SUBJECT_TEMPLATE = "{service_url}" + + +class UnsupportedUpdownEventType(JsonableError): + pass + +def send_message_for_event(event, user_profile, client, stream): + # type: (Dict[str, Any], UserProfile, Client, str) -> None + try: + event_type = get_event_type(event) + subject = SUBJECT_TEMPLATE.format(service_url=event['check']['url']) + body = EVENT_TYPE_BODY_MAPPER[event_type](event) + except KeyError as e: + return json_error(_("Missing key {} in JSON").format(e.message)) + check_send_message(user_profile, client, 'stream', [stream], subject, body) + +def get_body_for_up_event(event): + # type: (Dict[str, Any]) -> str + body = "Service is `up`" + event_downtime = event['downtime'] + if event_downtime['started_at']: + body = "{} again".format(body) + string_date = get_time_string_based_on_duration(event_downtime['duration']) + if string_date: + body = "{} after {}".format(body, string_date) + return "{}.".format(body) + +def get_time_string_based_on_duration(duration): + # type: (int) -> str + days, reminder = divmod(duration, 86400) + hours, reminder = divmod(reminder, 3600) + minutes, seconds = divmod(reminder, 60) + + string_date = '' + string_date += add_time_part_to_string_date_if_needed(days, 'day') + string_date += add_time_part_to_string_date_if_needed(hours, 'hour') + string_date += add_time_part_to_string_date_if_needed(minutes, 'minute') + string_date += add_time_part_to_string_date_if_needed(seconds, 'second') + return string_date.rstrip() + +def add_time_part_to_string_date_if_needed(value, text_name): + # type: (int, str) -> str + if value == 1: + return "1 {} ".format(text_name) + if value > 1: + return "{} {}s ".format(value, text_name) + return '' + +def get_body_for_down_event(event): + # type: (Dict[str, Any]) -> str + event_downtime = event['downtime'] + started_at = datetime.strptime(event_downtime['started_at'], "%Y-%m-%dT%H:%M:%SZ") + return "Service is `down`. It returned \"{}\" error at {}.".format( + event_downtime['error'], + started_at.strftime("%d-%m-%Y %H:%M") + ) + +@api_key_only_webhook_view('Updown') +@has_request_variables +def api_updown_webhook(request, user_profile, client, + payload=REQ(argument_type='body'), + stream=REQ(default='updown')): + # type: (HttpRequest, UserProfile, Client, List[Dict[str, Any]], str) -> HttpResponse + for event in payload: + send_message_for_event(event, user_profile, client, stream) + return json_success() + +EVENT_TYPE_BODY_MAPPER = { + 'up': get_body_for_up_event, + 'down': get_body_for_down_event +} + +def get_event_type(event): + # type: (Dict[str, Any]) -> str + event_type_match = re.match('check.(.*)', event['event']) + if event_type_match: + event_type = event_type_match.group(1) + if event_type in EVENT_TYPE_BODY_MAPPER: + return event_type + raise UnsupportedUpdownEventType(event['event']) diff --git a/zproject/urls.py b/zproject/urls.py index 3d63ce51dc..c947b9973d 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -163,6 +163,7 @@ urlpatterns += patterns('zerver.views', url(r'^api/v1/external/teamcity$', 'webhooks.teamcity.api_teamcity_webhook'), url(r'^api/v1/external/transifex$', 'webhooks.transifex.api_transifex_webhook'), url(r'^api/v1/external/travis$', 'webhooks.travis.api_travis_webhook'), + url(r'^api/v1/external/updown$', 'webhooks.updown.api_updown_webhook'), url(r'^api/v1/external/yo$', 'webhooks.yo.api_yo_app_webhook'), url(r'^api/v1/external/zendesk$', 'webhooks.zendesk.api_zendesk_webhook'),