models.py 6.1 KB

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