-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #458 from RUGSoftEng/backend/uptime_checking
Backend/uptime checking
- Loading branch information
Showing
13 changed files
with
367 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
""" | ||
Exposes the class DowntimeLog, which keeps track of a web service's downtime, | ||
and calculates downtime intervals, total downtime, and downtime percentage | ||
in an on-line manner. | ||
""" | ||
|
||
import persistent | ||
import datetime | ||
from collections import defaultdict | ||
|
||
|
||
class DowntimeLog(persistent.Persistent): | ||
""" | ||
Keeps track of downtime, and calculates downtime intervals, total downtime, and downtime percentage | ||
in an on-line manner. | ||
""" | ||
|
||
def __init__(self): | ||
self._downtime_intervals = defaultdict(list) # datetime.date -> list[(datetime.time, datetime.time)] | ||
self._total_downtime = defaultdict(datetime.timedelta) # datetime.date -> datetime.timedelta | ||
|
||
self._downtime_start = None | ||
|
||
def add_ping_result(self, is_up, ping_datetime=datetime.datetime.now(tz=datetime.timezone.utc)): | ||
""" | ||
Add the result of a ping request to the downtime log. | ||
:param is_up: Whether the web service is up or not. | ||
:param ping_datetime: When the ping took place (approximately); defaults to the current time in UTC. | ||
""" | ||
if is_up: | ||
if self._downtime_start: | ||
# Split the downtime into intervals of at most 24 hours | ||
|
||
start = self._downtime_start | ||
end = min(ping_datetime, _day_end(start)) | ||
|
||
while start <= ping_datetime: | ||
date = start.date() | ||
interval = (start.timetz(), end.timetz()) | ||
self._downtime_intervals[date].append(interval) | ||
self._total_downtime[date] += (end - start) + datetime.timedelta(microseconds=1) | ||
|
||
start = _day_start(start + datetime.timedelta(days=1)) | ||
end = min(ping_datetime, _day_end(start)) | ||
|
||
self._downtime_start = None | ||
else: | ||
if self._downtime_start is None: | ||
self._downtime_start = ping_datetime | ||
|
||
def get_downtime_intervals( | ||
self, | ||
start=datetime.datetime.now(tz=datetime.timezone.utc).date() - datetime.timedelta(days=90), | ||
end=datetime.datetime.now(tz=datetime.timezone.utc).date()): | ||
""" | ||
Return the intervals of downtime per day between two dates. | ||
:param start: The start date (exclusive; defaults to 90 days before the current day). | ||
:param end: The end date (inclusive; defaults to the current day). | ||
:return: A dict containing a list of downtime intervals per day. | ||
""" | ||
if end <= start: | ||
ValueError('Date range cannot be negative or zero') | ||
|
||
return { | ||
date.strftime('%Y-%m-%d'): list(self._downtime_intervals[date]) | ||
for date in _date_range(start, end) | ||
} | ||
|
||
def get_total_downtimes( | ||
self, | ||
start=datetime.datetime.now(tz=datetime.timezone.utc).date() - datetime.timedelta(days=90), | ||
end=datetime.datetime.now(tz=datetime.timezone.utc).date()): | ||
""" | ||
Return the total amounts of downtime per day between two dates. | ||
:param start: The start date (exclusive; defaults to 90 days before the current day). | ||
:param end: The end date (inclusive; defaults to the current day). | ||
:return: A dict containing the total downtime per day. | ||
""" | ||
if end <= start: | ||
raise ValueError('Date range cannot be negative or zero') | ||
|
||
return { | ||
date.strftime('%Y-%m-%d'): self._total_downtime[date] | ||
for date in _date_range(start, end) | ||
} | ||
|
||
def get_downtime_percentage( | ||
self, | ||
start=datetime.datetime.now(tz=datetime.timezone.utc).date() - datetime.timedelta(days=90), | ||
end=datetime.datetime.now(tz=datetime.timezone.utc).date()): | ||
""" | ||
Get the percentage of downtime between two dates. | ||
:param start: The start date (exclusive; defaults to 90 days before the current day). | ||
:param end: The end date (inclusive; defaults to the current day). | ||
:return: A float, the downtime percentage for the given date range. | ||
""" | ||
if end <= start: | ||
raise ValueError('Date range cannot be negative or zero') | ||
|
||
total_downtime = sum( | ||
(self._total_downtime[date] for date in _date_range(start, end)), | ||
datetime.timedelta(0) | ||
) | ||
|
||
total_time = end - start | ||
|
||
percentage = total_downtime/total_time*100 | ||
|
||
return percentage | ||
|
||
|
||
def _day_start(dt): | ||
return datetime.datetime.combine(dt.date(), datetime.time.min).replace(tzinfo=datetime.timezone.utc) | ||
|
||
|
||
def _day_end(dt): | ||
return datetime.datetime.combine(dt.date(), datetime.time.max).replace(tzinfo=datetime.timezone.utc) | ||
|
||
|
||
def _date_range(start, end): | ||
""" | ||
Yield dates in the range (start, end]. | ||
:param start: Start date (exclusive). | ||
:param end: End date. | ||
""" | ||
start += datetime.timedelta(days=1) | ||
while start <= end: | ||
yield start | ||
start += datetime.timedelta(days=1) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
""" | ||
Periodically pings a dashboard to see if the web service is still up. | ||
""" | ||
|
||
from functools import partial | ||
from datetime import timedelta | ||
import json | ||
|
||
import requests | ||
|
||
import flask_monitoring_dashboard_client | ||
import pydash_app.dashboard.repository as dashboard_repository | ||
import pydash_logger | ||
import periodic_tasks | ||
|
||
logger = pydash_logger.Logger(__name__) | ||
|
||
_DEFAULT_PING_INTERVAL = timedelta(minutes=5) | ||
|
||
|
||
def schedule_all_periodic_dashboard_pinging( | ||
interval=_DEFAULT_PING_INTERVAL, | ||
scheduler=periodic_tasks.default_task_scheduler): | ||
""" | ||
Set up periodic dashboard pinging tasks for all dashboards that want their uptime to be monitored. | ||
:param interval: The frequency with which to ping a dashboard, defaults to 5 minutes. | ||
:param scheduler: The task scheduler to schedule the tasks to, defaults to the default scheduler. | ||
""" | ||
for dashboard in dashboard_repository.all(): | ||
schedule_periodic_dashboard_pinging(dashboard, interval, scheduler) | ||
|
||
|
||
def schedule_periodic_dashboard_pinging( | ||
dashboard, | ||
interval=_DEFAULT_PING_INTERVAL, | ||
scheduler=periodic_tasks.default_task_scheduler): | ||
""" | ||
Set up a periodic pinging task for a dashboard if the dashboard allows it. | ||
:param dashboard: The dashboard to set up a pinging task for. | ||
:param interval: The frequency with which to ping a dashboard, defaults to 5 minutes. | ||
:param scheduler: The task scheduler to schedule this task to, defaults to the default scheduler. | ||
""" | ||
|
||
if dashboard.monitor_downtime: | ||
periodic_tasks.add_periodic_task( | ||
name=('dashboard', dashboard.id, 'pinging'), | ||
task=partial(_ping_dashboard, dashboard.id), | ||
interval=interval, | ||
scheduler=scheduler) | ||
|
||
|
||
def _ping_dashboard(dashboard_id): | ||
try: | ||
dashboard = dashboard_repository.find(dashboard_id) | ||
except KeyError: | ||
logger.warning('Dashboard does not exist') | ||
return | ||
|
||
is_up = _is_dashboard_up(dashboard.url) | ||
dashboard.add_ping_result(is_up) | ||
|
||
dashboard_repository.update(dashboard) | ||
|
||
|
||
def _is_dashboard_up(url): | ||
""" | ||
Connect to a dashboard to see if it's up. | ||
:param url: The dashboard's URL. | ||
:return: True or False depending on whether the dashboard is up. | ||
""" | ||
try: | ||
flask_monitoring_dashboard_client.get_details(url) | ||
except requests.exceptions.RequestException: | ||
return False | ||
except (json.JSONDecodeError, Exception): | ||
return True | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.