api.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. from datetime import timedelta
  2. from uuid import UUID
  3. from django.db.models import F, Prefetch, Window
  4. from django.db.models.functions import RowNumber
  5. from django.http import HttpRequest, HttpResponse
  6. from django.shortcuts import aget_object_or_404
  7. from django.utils import timezone
  8. from ninja import Router
  9. from ninja.pagination import paginate
  10. from apps.organizations_ext.models import Organization
  11. from apps.projects.models import Project
  12. from glitchtip.api.authentication import AuthHttpRequest
  13. from glitchtip.utils import async_call_celery_task
  14. from .models import Monitor, MonitorCheck, StatusPage
  15. from .schema import (
  16. MonitorCheckResponseTimeSchema,
  17. MonitorCheckSchema,
  18. MonitorDetailSchema,
  19. MonitorIn,
  20. MonitorSchema,
  21. StatusPageIn,
  22. StatusPageSchema,
  23. )
  24. from .tasks import send_monitor_notification
  25. router = Router()
  26. def get_monitor_queryset(user_id: int, organization_slug: str):
  27. return (
  28. Monitor.objects.with_check_annotations()
  29. .filter(organization__users=user_id, organization__slug=organization_slug)
  30. # Fetch latest 60 checks for each monitor
  31. .prefetch_related(
  32. Prefetch(
  33. "checks",
  34. queryset=MonitorCheck.objects.filter( # Optimization
  35. start_check__gt=timezone.now() - timedelta(hours=12)
  36. )
  37. .annotate(
  38. row_number=Window(
  39. expression=RowNumber(),
  40. order_by="-start_check",
  41. partition_by=F("monitor"),
  42. ),
  43. )
  44. .filter(row_number__lte=60)
  45. .distinct(),
  46. )
  47. )
  48. .select_related("project", "organization")
  49. )
  50. @router.post(
  51. "organizations/{slug:organization_slug}/heartbeat_check/{uuid:endpoint_id}/",
  52. response=MonitorCheckSchema,
  53. auth=None,
  54. )
  55. async def heartbeat_check(
  56. request: HttpRequest, organization_slug: str, endpoint_id: UUID
  57. ):
  58. """
  59. Heartbeat monitors allow an external service to contact this endpoint
  60. when the service is up.
  61. """
  62. monitor = await aget_object_or_404(
  63. Monitor.objects.with_check_annotations(),
  64. organization__slug=organization_slug,
  65. endpoint_id=endpoint_id,
  66. )
  67. monitor_check = await MonitorCheck.objects.acreate(monitor=monitor)
  68. if monitor.latest_is_up is False:
  69. await async_call_celery_task(
  70. send_monitor_notification, monitor_check.pk, False, monitor.last_change
  71. )
  72. return monitor_check
  73. @router.get(
  74. "organizations/{slug:organization_slug}/monitors/",
  75. response=list[MonitorSchema],
  76. by_alias=True,
  77. )
  78. @paginate
  79. async def list_monitors(
  80. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  81. ):
  82. return get_monitor_queryset(request.auth.user_id, organization_slug)
  83. @router.get(
  84. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
  85. response=MonitorDetailSchema,
  86. by_alias=True,
  87. )
  88. async def get_monitor(
  89. request: AuthHttpRequest, organization_slug: str, monitor_id: int
  90. ):
  91. return await aget_object_or_404(
  92. get_monitor_queryset(request.auth.user_id, organization_slug),
  93. id=monitor_id,
  94. )
  95. @router.post(
  96. "organizations/{slug:organization_slug}/monitors/",
  97. response={201: MonitorSchema},
  98. by_alias=True,
  99. )
  100. async def create_monitor(
  101. request: AuthHttpRequest, organization_slug: str, payload: MonitorIn
  102. ):
  103. user_id = request.auth.user_id
  104. organization = await aget_object_or_404(
  105. Organization, slug=organization_slug, users=user_id
  106. )
  107. data = payload.dict(exclude_defaults=True)
  108. if project_id := data.pop("project", None):
  109. data["project"] = await organization.projects.filter(id=project_id).afirst()
  110. monitor = await Monitor.objects.acreate(organization=organization, **data)
  111. return 201, await get_monitor_queryset(user_id, organization_slug).aget(
  112. id=monitor.id
  113. )
  114. @router.put(
  115. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
  116. response=MonitorSchema,
  117. by_alias=True,
  118. )
  119. async def update_monitor(
  120. request: AuthHttpRequest,
  121. organization_slug: str,
  122. monitor_id: int,
  123. payload: MonitorIn,
  124. ):
  125. monitor = await aget_object_or_404(
  126. get_monitor_queryset(request.auth.user_id, organization_slug),
  127. id=monitor_id,
  128. )
  129. data = payload.dict(exclude_none=True)
  130. if project_id := data.pop("project", None):
  131. result = await Project.objects.filter(
  132. organization__slug=organization_slug,
  133. organization__users=request.auth.user_id,
  134. id=project_id,
  135. ).afirst()
  136. data["project"] = result
  137. for attr, value in data.items():
  138. setattr(monitor, attr, value)
  139. await monitor.asave()
  140. return monitor
  141. @router.get(
  142. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/checks/",
  143. response=list[MonitorCheckResponseTimeSchema],
  144. by_alias=True,
  145. )
  146. @paginate
  147. async def list_monitor_checks(
  148. request: AuthHttpRequest,
  149. response: HttpResponse,
  150. organization_slug: str,
  151. monitor_id: int,
  152. is_change: bool | None = None,
  153. ):
  154. """
  155. List checks performed for a monitor
  156. Set is_change query param to True to show only changes,
  157. This is useful to see only when a service went up and down.
  158. """
  159. checks = (
  160. MonitorCheck.objects.filter(
  161. monitor_id=monitor_id,
  162. monitor__organization__slug=organization_slug,
  163. monitor__organization__users=request.auth.user_id,
  164. )
  165. .only("is_up", "start_check", "reason", "response_time")
  166. .order_by("-start_check")
  167. )
  168. if is_change is not None:
  169. checks = checks.filter(is_change=is_change)
  170. return checks
  171. @router.get(
  172. "/organizations/{slug:organization_slug}/status-pages/",
  173. response=list[StatusPageSchema],
  174. by_alias=True,
  175. )
  176. @paginate
  177. async def list_status_pages(
  178. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  179. ):
  180. """List status pages, used for showing the current status of an uptime monitor"""
  181. return StatusPage.objects.filter(
  182. organization__users=request.auth.user_id
  183. ).prefetch_related("monitors")
  184. @router.post(
  185. "/organizations/{slug:organization_slug}/status-pages/",
  186. response={201: StatusPageSchema},
  187. by_alias=True,
  188. )
  189. async def create_status_page(
  190. request: AuthHttpRequest, organization_slug: str, payload: StatusPageIn
  191. ):
  192. organization = await aget_object_or_404(
  193. Organization, slug=organization_slug, users=request.auth.user_id
  194. )
  195. data = payload.dict()
  196. status_page = await StatusPage.objects.acreate(organization=organization, **data)
  197. return 201, await StatusPage.objects.prefetch_related("monitors").aget(
  198. id=status_page.id
  199. )