forked from GithubBackups/healthchecks
Check model supports cron-style schedule
This commit is contained in:
parent
ce57a1cc8b
commit
8633a5a892
30
hc/api/migrations/0027_auto_20161205_0833.py
Normal file
30
hc/api/migrations/0027_auto_20161205_0833.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.1 on 2016-12-05 08:33
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0026_auto_20160415_1824'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='check',
|
||||||
|
name='schedule',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='check',
|
||||||
|
name='tz',
|
||||||
|
field=models.CharField(default='UTC', max_length=36),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='channel',
|
||||||
|
name='kind',
|
||||||
|
field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('po', 'Pushover'), ('pushbullet', 'Pushbullet'), ('opsgenie', 'OpsGenie'), ('victorops', 'VictorOps')], max_length=20),
|
||||||
|
),
|
||||||
|
]
|
@ -3,8 +3,9 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import timedelta as td
|
from datetime import datetime, timedelta as td
|
||||||
|
|
||||||
|
from croniter import croniter
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -53,6 +54,8 @@ class Check(models.Model):
|
|||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
timeout = models.DurationField(default=DEFAULT_TIMEOUT)
|
timeout = models.DurationField(default=DEFAULT_TIMEOUT)
|
||||||
grace = models.DurationField(default=DEFAULT_GRACE)
|
grace = models.DurationField(default=DEFAULT_GRACE)
|
||||||
|
schedule = models.CharField(max_length=100, blank=True)
|
||||||
|
tz = models.CharField(max_length=36, default="UTC")
|
||||||
n_pings = models.IntegerField(default=0)
|
n_pings = models.IntegerField(default=0)
|
||||||
last_ping = models.DateTimeField(null=True, blank=True)
|
last_ping = models.DateTimeField(null=True, blank=True)
|
||||||
alert_after = models.DateTimeField(null=True, blank=True, editable=False)
|
alert_after = models.DateTimeField(null=True, blank=True, editable=False)
|
||||||
@ -85,27 +88,45 @@ class Check(models.Model):
|
|||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def get_status(self):
|
def get_grace_start(self):
|
||||||
|
""" Return the datetime when grace period starts. """
|
||||||
|
|
||||||
|
# The common case, grace starts after timeout
|
||||||
|
if not self.schedule:
|
||||||
|
return self.last_ping + self.timeout
|
||||||
|
|
||||||
|
# The complex case, next ping is expected based on cron schedule
|
||||||
|
with timezone.override(self.tz):
|
||||||
|
last_naive = timezone.make_naive(self.last_ping)
|
||||||
|
it = croniter(self.schedule, last_naive)
|
||||||
|
next_naive = it.get_next(datetime)
|
||||||
|
return timezone.make_aware(next_naive, is_dst=False)
|
||||||
|
|
||||||
|
def get_status(self, now=None):
|
||||||
|
""" Return "up" if the check is up or in grace, otherwise "down". """
|
||||||
|
|
||||||
if self.status in ("new", "paused"):
|
if self.status in ("new", "paused"):
|
||||||
return self.status
|
return self.status
|
||||||
|
|
||||||
now = timezone.now()
|
if now is None:
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
if self.last_ping + self.timeout + self.grace > now:
|
return "up" if self.get_grace_start() + self.grace > now else "down"
|
||||||
return "up"
|
|
||||||
|
|
||||||
return "down"
|
|
||||||
|
|
||||||
def get_alert_after(self):
|
def get_alert_after(self):
|
||||||
return self.last_ping + self.timeout + self.grace
|
""" Return the datetime when check potentially goes down. """
|
||||||
|
|
||||||
|
return self.get_grace_start() + self.grace
|
||||||
|
|
||||||
def in_grace_period(self):
|
def in_grace_period(self):
|
||||||
|
""" Return True if check is currently in grace period. """
|
||||||
|
|
||||||
if self.status in ("new", "paused"):
|
if self.status in ("new", "paused"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
up_ends = self.last_ping + self.timeout
|
grace_start = self.get_grace_start()
|
||||||
grace_ends = up_ends + self.grace
|
grace_end = grace_start + self.grace
|
||||||
return up_ends < timezone.now() < grace_ends
|
return grace_start < timezone.now() < grace_end
|
||||||
|
|
||||||
def assign_all_channels(self):
|
def assign_all_channels(self):
|
||||||
if self.user:
|
if self.user:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -37,3 +37,40 @@ class CheckModelTestCase(TestCase):
|
|||||||
|
|
||||||
check.status = "paused"
|
check.status = "paused"
|
||||||
self.assertFalse(check.in_grace_period())
|
self.assertFalse(check.in_grace_period())
|
||||||
|
|
||||||
|
def test_status_works_with_cron_syntax(self):
|
||||||
|
dt = timezone.make_aware(datetime(2000, 1, 1), timezone=timezone.utc)
|
||||||
|
|
||||||
|
# Expect ping every midnight, default grace is 1 hour
|
||||||
|
check = Check()
|
||||||
|
check.timeout = timedelta(minutes=0)
|
||||||
|
check.schedule = "0 0 * * *"
|
||||||
|
check.status = "up"
|
||||||
|
check.last_ping = dt
|
||||||
|
|
||||||
|
# 00:30am
|
||||||
|
now = dt + timedelta(days=1, minutes=30)
|
||||||
|
self.assertEqual(check.get_status(now), "up")
|
||||||
|
|
||||||
|
# 1:30am
|
||||||
|
now = dt + timedelta(days=1, minutes=90)
|
||||||
|
self.assertEqual(check.get_status(now), "down")
|
||||||
|
|
||||||
|
def test_status_works_with_timezone(self):
|
||||||
|
dt = timezone.make_aware(datetime(2000, 1, 1), timezone=timezone.utc)
|
||||||
|
|
||||||
|
# Expect ping every day at 10am, default grace is 1 hour
|
||||||
|
check = Check()
|
||||||
|
check.timeout = timedelta(minutes=0)
|
||||||
|
check.schedule = "0 10 * * *"
|
||||||
|
check.status = "up"
|
||||||
|
check.last_ping = dt
|
||||||
|
check.tz = "Australia/Brisbane" # UTC+10
|
||||||
|
|
||||||
|
# 10:30am
|
||||||
|
now = dt + timedelta(days=1, minutes=30)
|
||||||
|
self.assertEqual(check.get_status(now), "up")
|
||||||
|
|
||||||
|
# 11:30am
|
||||||
|
now = dt + timedelta(days=1, minutes=90)
|
||||||
|
self.assertEqual(check.get_status(now), "down")
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
croniter
|
||||||
django-appconf==1.0.1
|
django-appconf==1.0.1
|
||||||
django-ses-backend==0.1.1
|
django-ses-backend==0.1.1
|
||||||
Django==1.10.1
|
Django==1.10.1
|
||||||
@ -5,4 +6,5 @@ django_compressor==2.1
|
|||||||
djmail==0.11.0
|
djmail==0.11.0
|
||||||
premailer==2.9.6
|
premailer==2.9.6
|
||||||
psycopg2==2.6.1
|
psycopg2==2.6.1
|
||||||
|
pytz==2016.7
|
||||||
requests==2.9.1
|
requests==2.9.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user