api.py 6.3 KB

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