Skip to content

Commit

Permalink
Merge pull request #1 from rasulkireev/check-status
Browse files Browse the repository at this point in the history
Add status checking
  • Loading branch information
rasulkireev authored Oct 17, 2024
2 parents d9ce8b2 + a06c4af commit 7ed5dee
Show file tree
Hide file tree
Showing 13 changed files with 525 additions and 32 deletions.
11 changes: 10 additions & 1 deletion core/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from allauth.account.forms import LoginForm, SignupForm
from django import forms

from core.models import Profile
from core.models import Profile, Service
from core.utils import DivErrorList


Expand Down Expand Up @@ -43,3 +43,12 @@ def save(self, commit=True):
user.save()
profile.save()
return profile


class ServiceForm(forms.ModelForm):
class Meta:
model = Service
fields = ['name', 'type', 'url', 'check_interval', 'additional_data', 'is_public', 'is_active']
widgets = {
'additional_data': forms.Textarea(attrs={'rows': 4}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 5.0.4 on 2024-10-17 06:57

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0002_project'),
]

operations = [
migrations.AlterField(
model_name='project',
name='name',
field=models.CharField(max_length=250, unique=True),
),
migrations.AlterField(
model_name='project',
name='slug',
field=models.SlugField(max_length=250, unique=True),
),
migrations.CreateModel(
name='Service',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=250)),
('type', models.CharField(choices=[('WEBSITE', 'Website'), ('API', 'API')], default='WEBSITE', max_length=20)),
('url', models.URLField(blank=True, max_length=500)),
('check_interval', models.PositiveIntegerField(default=5, help_text='Check interval in minutes')),
('additional_data', models.JSONField(blank=True, help_text='Additional data for service checks (e.g., auth headers, connection strings)', null=True)),
('is_public', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='core.project')),
],
options={
'unique_together': {('project', 'name')},
},
),
]
35 changes: 35 additions & 0 deletions core/migrations/0004_servicestatus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.0.4 on 2024-10-17 07:04

import django.db.models.deletion
import django.utils.timezone
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0003_alter_project_name_alter_project_slug_service'),
]

operations = [
migrations.CreateModel(
name='ServiceStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('UP', 'Up'), ('DOWN', 'Down'), ('DEGRADED', 'Degraded'), ('UNKNOWN', 'Unknown')], default='UNKNOWN', max_length=20)),
('response_time', models.FloatField(blank=True, help_text='Response time in milliseconds', null=True)),
('status_code', models.IntegerField(blank=True, null=True)),
('error_message', models.TextField(blank=True)),
('checked_at', models.DateTimeField(default=django.utils.timezone.now)),
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statuses', to='core.service')),
],
options={
'ordering': ['-checked_at'],
'get_latest_by': 'checked_at',
},
),
]
19 changes: 19 additions & 0 deletions core/migrations/0005_alter_profilestatetransition_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.4 on 2024-10-17 08:28

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0004_servicestatus'),
]

operations = [
migrations.AlterField(
model_name='profilestatetransition',
name='profile',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_transitions', to='core.profile'),
),
]
74 changes: 70 additions & 4 deletions core/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from django.utils import timezone

from core.base_models import BaseModel
from core.model_utils import generate_random_key
Expand Down Expand Up @@ -50,6 +51,7 @@ def current_state(self):
return latest_transition.to_state



class ProfileStates(models.TextChoices):
STRANGER = "stranger"
SIGNED_UP = "signed_up"
Expand All @@ -58,9 +60,8 @@ class ProfileStates(models.TextChoices):
CHURNED = "churned"
ACCOUNT_DELETED = "account_deleted"


class ProfileStateTransition(BaseModel):
profile = models.ForeignKey(Profile, null=True, on_delete=models.SET_NULL, related_name="state_transitions")
profile = models.ForeignKey(Profile, null=True, blank=True, on_delete=models.SET_NULL, related_name="state_transitions")
from_state = models.CharField(max_length=255, choices=ProfileStates.choices)
to_state = models.CharField(max_length=255, choices=ProfileStates.choices)
backup_profile_id = models.IntegerField()
Expand All @@ -86,8 +87,8 @@ def get_absolute_url(self):

class Project(BaseModel):
profile = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name="projects")
name = models.CharField(max_length=250)
slug = models.SlugField(max_length=250)
name = models.CharField(max_length=250, unique=True)
slug = models.SlugField(max_length=250, unique=True)
public = models.BooleanField(default=False)
icon = models.ImageField(upload_to="project_icons/", blank=True)

Expand All @@ -96,3 +97,68 @@ def __str__(self):

def get_absolute_url(self):
return reverse("project-status-page", kwargs={"slug": self.slug})



class Service(BaseModel):
class ServiceType(models.TextChoices):
WEBSITE = 'WEBSITE', 'Website'
API = 'API', 'API'

project = models.ForeignKey('Project', on_delete=models.CASCADE, related_name="services")
name = models.CharField(max_length=250)
type = models.CharField(
max_length=20,
choices=ServiceType.choices,
default=ServiceType.WEBSITE
)
url = models.URLField(max_length=500, blank=True)
check_interval = models.PositiveIntegerField(default=5, help_text="Check interval in minutes")
additional_data = models.JSONField(
blank=True,
null=True,
help_text="Additional data for service checks (e.g., auth headers, connection strings)"
)
is_public = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)

class Meta:
unique_together = ['project', 'name']

def __str__(self):
return f"{self.name} ({self.get_type_display()})"



class ServiceStatus(BaseModel):
class StatusChoices(models.TextChoices):
UP = 'UP', 'Up'
DOWN = 'DOWN', 'Down'
DEGRADED = 'DEGRADED', 'Degraded'
UNKNOWN = 'UNKNOWN', 'Unknown'

service = models.ForeignKey('Service', on_delete=models.CASCADE, related_name='statuses')
status = models.CharField(max_length=20, choices=StatusChoices.choices, default=StatusChoices.UNKNOWN)
response_time = models.FloatField(null=True, blank=True, help_text="Response time in milliseconds")
status_code = models.IntegerField(null=True, blank=True)
error_message = models.TextField(blank=True)
checked_at = models.DateTimeField(default=timezone.now)

class Meta:
ordering = ['-checked_at']
get_latest_by = 'checked_at'

def __str__(self):
return f"{self.service.name} - {self.status} at {self.checked_at}"

@property
def is_up(self):
return self.status == self.StatusChoices.UP

@property
def is_down(self):
return self.status == self.StatusChoices.DOWN

@property
def is_degraded(self):
return self.status == self.StatusChoices.DEGRADED
63 changes: 63 additions & 0 deletions core/tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import requests
from django.conf import settings
from django.utils import timezone
from django.db.models import Q, Max, F, ExpressionWrapper, DurationField
from django_q.tasks import async_task

from core.models import Service, ServiceStatus
from statushen.utils import get_statushen_logger

logger = get_statushen_logger(__name__)
Expand All @@ -22,3 +26,62 @@ def add_email_to_buttondown(email, tag):

return r.json()


def schedule_service_checks():
"""
Schedule service checks for services that need to be checked.
This function should be run periodically (e.g., every minute) to ensure timely checks.
"""
now = timezone.now()

services_to_check = Service.objects.filter(is_active=True).annotate(
last_checked=Max('statuses__checked_at'),
check_interval_duration=ExpressionWrapper(
F('check_interval') * timezone.timedelta(minutes=1),
output_field=DurationField()
)
).filter(
Q(last_checked__isnull=True) |
Q(last_checked__lte=now - F('check_interval_duration'))
)

count = 0
for service in services_to_check:
async_task(check_service, service.id, group=f"{service.project.name} - {service.name}")
count += 1

return f"Scheduled {count} checks out of {services_to_check.count()} required."


def check_service(service_id):
"""
Perform a check on a specific service and record the status.
"""
try:
service = Service.objects.get(id=service_id)
except Service.DoesNotExist:
return

try:
response = requests.get(service.url, timeout=10)

if response.ok:
status = ServiceStatus.StatusChoices.UP
else:
status = ServiceStatus.StatusChoices.DOWN

ServiceStatus.objects.create(
service=service,
status=status,
response_time=response.elapsed.total_seconds() * 1000,
status_code=response.status_code
)
except requests.RequestException as e:
# Handle network errors, timeouts, etc.
ServiceStatus.objects.create(
service=service,
status=ServiceStatus.StatusChoices.DOWN,
error_message=str(e)
)

return f"Check complete. Status: {status}"
2 changes: 2 additions & 0 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@
# app
path("create-project/", views.CreateProjectView.as_view(), name="create-project"),
path("<slug:slug>/", views.ProjectStatusPageView.as_view(), name="project-status-page"),
path("<slug:slug>/settings/", views.ProjectSettingsView.as_view(), name="project-settings"),

]
Loading

0 comments on commit 7ed5dee

Please sign in to comment.