123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- import uuid
- from datetime import timedelta
- from django.conf import settings
- from django.core.exceptions import ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
- from django.db import models
- from django.db.models import OuterRef, Subquery
- from django.urls import reverse
- from django.utils.timezone import now
- from django_extensions.db.fields import AutoSlugField
- from glitchtip.base_models import CreatedModel
- from .constants import HTTP_MONITOR_TYPES, MonitorCheckReason, MonitorType
- class MonitorManager(models.Manager):
- def with_check_annotations(self):
- """
- Adds MonitorCheck annotations:
- latest_is_up - Most recent check is_up result
- last_change - Most recent check where is_up state changed
- Example: Monitor state: { latest_is_up } since { last_change }
- """
- return self.annotate(
- latest_is_up=Subquery(
- MonitorCheck.objects.filter(
- monitor_id=OuterRef("id"),
- )
- .order_by("-start_check")
- .values("is_up")[:1]
- ),
- last_change=Subquery(
- MonitorCheck.objects.filter(monitor_id=OuterRef("id"), is_change=True)
- .order_by("-start_check")
- .values("start_check")[:1]
- ),
- )
- class OptionalSchemeURLValidator(URLValidator):
- def __call__(self, value):
- if "://" in value:
- super().__call__(value)
- class Monitor(models.Model):
- created = models.DateTimeField(auto_now_add=True)
- monitor_type = models.CharField(
- max_length=12, choices=MonitorType.choices, default=MonitorType.PING
- )
- endpoint_id = models.UUIDField(
- blank=True,
- null=True,
- editable=False,
- help_text="Used for referencing heartbeat endpoint",
- )
- name = models.CharField(max_length=200)
- url = models.CharField(
- max_length=2000, blank=True, validators=[OptionalSchemeURLValidator()]
- )
- expected_status = models.PositiveSmallIntegerField(
- default=200, blank=True, null=True
- )
- expected_body = models.CharField(max_length=2000, blank=True)
- environment = models.ForeignKey(
- "environments.Environment",
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- )
- project = models.ForeignKey(
- "projects.Project",
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- )
- organization = models.ForeignKey(
- "organizations_ext.Organization", on_delete=models.CASCADE
- )
- interval = models.DurationField(
- default=timedelta(minutes=1),
- validators=[MaxValueValidator(timedelta(hours=24))],
- )
- timeout = models.PositiveSmallIntegerField(
- blank=True,
- null=True,
- validators=[MaxValueValidator(60), MinValueValidator(1)],
- help_text="Blank implies default value of 20",
- )
- objects = MonitorManager()
- class Meta:
- indexes = [models.Index(fields=["-created"])]
- def __str__(self):
- return self.name
- def save(self, *args, **kwargs):
- if self.monitor_type == MonitorType.HEARTBEAT and not self.endpoint_id:
- self.endpoint_id = uuid.uuid4()
- super().save(*args, **kwargs)
- # pylint: disable=import-outside-toplevel
- from glitchtip.uptime.tasks import perform_checks
- if self.monitor_type != MonitorType.HEARTBEAT:
- perform_checks.apply_async(args=([self.pk],), countdown=1)
- def clean(self):
- if self.monitor_type in HTTP_MONITOR_TYPES:
- URLValidator()(self.url)
- if self.monitor_type != MonitorType.HEARTBEAT and not self.url:
- raise ValidationError("Monitor URL is required")
- def get_detail_url(self):
- return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/uptime-monitors/{self.pk}"
- @property
- def int_timeout(self):
- """Get timeout as integer (coalesce null as 20)"""
- return self.timeout or 20
- class MonitorCheck(models.Model):
- monitor = models.ForeignKey(
- Monitor, on_delete=models.CASCADE, related_name="checks"
- )
- is_up = models.BooleanField()
- is_change = models.BooleanField(
- help_text="Indicates change to is_up status for associated monitor",
- )
- start_check = models.DateTimeField(
- default=now,
- help_text="Time when the start of this check was performed",
- )
- reason = models.PositiveSmallIntegerField(
- choices=MonitorCheckReason.choices, default=0, null=True, blank=True
- )
- response_time = models.DurationField(blank=True, null=True)
- data = models.JSONField(null=True, blank=True)
- class Meta:
- indexes = [
- models.Index(fields=["monitor", "-start_check"]),
- models.Index(fields=["monitor", "is_change", "-start_check"]),
- ]
- ordering = ("-start_check",)
- def __str__(self):
- return self.up_or_down
- @property
- def up_or_down(self):
- if self.is_up:
- return "Up"
- return "Down"
- class StatusPage(CreatedModel):
- """
- A status page is a collection of monitors that are available to view
- """
- organization = models.ForeignKey(
- "organizations_ext.Organization", on_delete=models.CASCADE
- )
- name = models.CharField(max_length=200)
- slug = AutoSlugField(populate_from=["name"], max_length=200)
- is_public = models.BooleanField(
- help_text="When true, the status page URL is publicly accessible"
- )
- monitors = models.ManyToManyField(Monitor, blank=True)
- class Meta:
- constraints = [
- models.UniqueConstraint(
- fields=["organization", "slug"], name="unique_organization_slug"
- )
- ]
- def __str__(self):
- return self.name
- def get_absolute_url(self):
- return reverse("status-page-detail", args=[self.organization.slug, self.slug])
|