models.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import uuid
  2. from datetime import timedelta
  3. from django.conf import settings
  4. from django.core.exceptions import ValidationError
  5. from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
  6. from django.db import models
  7. from django.db.models import OuterRef, Subquery
  8. from django.urls import reverse
  9. from django.utils.timezone import now
  10. from django_extensions.db.fields import AutoSlugField
  11. from glitchtip.base_models import CreatedModel
  12. from .constants import HTTP_MONITOR_TYPES, MonitorCheckReason, MonitorType
  13. class MonitorManager(models.Manager):
  14. def with_check_annotations(self):
  15. """
  16. Adds MonitorCheck annotations:
  17. latest_is_up - Most recent check is_up result
  18. last_change - Most recent check where is_up state changed
  19. Example: Monitor state: { latest_is_up } since { last_change }
  20. """
  21. return self.annotate(
  22. latest_is_up=Subquery(
  23. MonitorCheck.objects.filter(
  24. monitor_id=OuterRef("id"),
  25. )
  26. .order_by("-start_check")
  27. .values("is_up")[:1]
  28. ),
  29. last_change=Subquery(
  30. MonitorCheck.objects.filter(monitor_id=OuterRef("id"), is_change=True)
  31. .order_by("-start_check")
  32. .values("start_check")[:1]
  33. ),
  34. )
  35. class OptionalSchemeURLValidator(URLValidator):
  36. def __call__(self, value):
  37. if "://" in value:
  38. super().__call__(value)
  39. class Monitor(models.Model):
  40. created = models.DateTimeField(auto_now_add=True)
  41. monitor_type = models.CharField(
  42. max_length=12, choices=MonitorType.choices, default=MonitorType.PING
  43. )
  44. endpoint_id = models.UUIDField(
  45. blank=True,
  46. null=True,
  47. editable=False,
  48. help_text="Used for referencing heartbeat endpoint",
  49. )
  50. name = models.CharField(max_length=200)
  51. url = models.CharField(
  52. max_length=2000, blank=True, validators=[OptionalSchemeURLValidator()]
  53. )
  54. expected_status = models.PositiveSmallIntegerField(
  55. default=200, blank=True, null=True
  56. )
  57. expected_body = models.CharField(max_length=2000, blank=True)
  58. environment = models.ForeignKey(
  59. "environments.Environment",
  60. on_delete=models.SET_NULL,
  61. null=True,
  62. blank=True,
  63. )
  64. project = models.ForeignKey(
  65. "projects.Project",
  66. on_delete=models.SET_NULL,
  67. null=True,
  68. blank=True,
  69. )
  70. organization = models.ForeignKey(
  71. "organizations_ext.Organization", on_delete=models.CASCADE
  72. )
  73. interval = models.DurationField(
  74. default=timedelta(minutes=1),
  75. validators=[MaxValueValidator(timedelta(hours=24))],
  76. )
  77. timeout = models.PositiveSmallIntegerField(
  78. blank=True,
  79. null=True,
  80. validators=[MaxValueValidator(60), MinValueValidator(1)],
  81. help_text="Blank implies default value of 20",
  82. )
  83. objects = MonitorManager()
  84. class Meta:
  85. indexes = [models.Index(fields=["-created"])]
  86. def __str__(self):
  87. return self.name
  88. def save(self, *args, **kwargs):
  89. if self.monitor_type == MonitorType.HEARTBEAT and not self.endpoint_id:
  90. self.endpoint_id = uuid.uuid4()
  91. super().save(*args, **kwargs)
  92. # pylint: disable=import-outside-toplevel
  93. from glitchtip.uptime.tasks import perform_checks
  94. if self.monitor_type != MonitorType.HEARTBEAT:
  95. perform_checks.apply_async(args=([self.pk],), countdown=1)
  96. def clean(self):
  97. if self.monitor_type in HTTP_MONITOR_TYPES:
  98. URLValidator()(self.url)
  99. if self.monitor_type != MonitorType.HEARTBEAT and not self.url:
  100. raise ValidationError("Monitor URL is required")
  101. def get_detail_url(self):
  102. return f"{settings.GLITCHTIP_URL.geturl()}/{self.project.organization.slug}/uptime-monitors/{self.pk}"
  103. @property
  104. def int_timeout(self):
  105. """Get timeout as integer (coalesce null as 20)"""
  106. return self.timeout or 20
  107. class MonitorCheck(models.Model):
  108. monitor = models.ForeignKey(
  109. Monitor, on_delete=models.CASCADE, related_name="checks"
  110. )
  111. is_up = models.BooleanField()
  112. is_change = models.BooleanField(
  113. help_text="Indicates change to is_up status for associated monitor",
  114. )
  115. start_check = models.DateTimeField(
  116. default=now,
  117. help_text="Time when the start of this check was performed",
  118. )
  119. reason = models.PositiveSmallIntegerField(
  120. choices=MonitorCheckReason.choices, default=0, null=True, blank=True
  121. )
  122. response_time = models.DurationField(blank=True, null=True)
  123. data = models.JSONField(null=True, blank=True)
  124. class Meta:
  125. indexes = [
  126. models.Index(fields=["monitor", "-start_check"]),
  127. models.Index(fields=["monitor", "is_change", "-start_check"]),
  128. ]
  129. ordering = ("-start_check",)
  130. def __str__(self):
  131. return self.up_or_down
  132. @property
  133. def up_or_down(self):
  134. if self.is_up:
  135. return "Up"
  136. return "Down"
  137. class StatusPage(CreatedModel):
  138. """
  139. A status page is a collection of monitors that are available to view
  140. """
  141. organization = models.ForeignKey(
  142. "organizations_ext.Organization", on_delete=models.CASCADE
  143. )
  144. name = models.CharField(max_length=200)
  145. slug = AutoSlugField(populate_from=["name"], max_length=200)
  146. is_public = models.BooleanField(
  147. help_text="When true, the status page URL is publicly accessible"
  148. )
  149. monitors = models.ManyToManyField(Monitor, blank=True)
  150. class Meta:
  151. constraints = [
  152. models.UniqueConstraint(
  153. fields=["organization", "slug"], name="unique_organization_slug"
  154. )
  155. ]
  156. def __str__(self):
  157. return self.name
  158. def get_absolute_url(self):
  159. return reverse("status-page-detail", args=[self.organization.slug, self.slug])