api.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 Http404, 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(
  68. monitor=monitor,
  69. is_up=True,
  70. reason=None,
  71. is_change=monitor.latest_is_up is not True,
  72. )
  73. if monitor.latest_is_up is False:
  74. await async_call_celery_task(
  75. send_monitor_notification, monitor_check.pk, False, monitor.last_change
  76. )
  77. return monitor_check
  78. @router.get(
  79. "organizations/{slug:organization_slug}/monitors/",
  80. response=list[MonitorSchema],
  81. by_alias=True,
  82. )
  83. @paginate
  84. async def list_monitors(
  85. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  86. ):
  87. return get_monitor_queryset(request.auth.user_id, organization_slug)
  88. @router.get(
  89. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
  90. response=MonitorDetailSchema,
  91. by_alias=True,
  92. )
  93. async def get_monitor(
  94. request: AuthHttpRequest, organization_slug: str, monitor_id: int
  95. ):
  96. return await aget_object_or_404(
  97. get_monitor_queryset(request.auth.user_id, organization_slug),
  98. id=monitor_id,
  99. )
  100. @router.post(
  101. "organizations/{slug:organization_slug}/monitors/",
  102. response={201: MonitorSchema},
  103. by_alias=True,
  104. )
  105. async def create_monitor(
  106. request: AuthHttpRequest, organization_slug: str, payload: MonitorIn
  107. ):
  108. user_id = request.auth.user_id
  109. organization = await aget_object_or_404(
  110. Organization, slug=organization_slug, users=user_id
  111. )
  112. data = payload.dict(exclude_defaults=True)
  113. if project_id := data.pop("project", None):
  114. data["project"] = await organization.projects.filter(id=project_id).afirst()
  115. monitor = await Monitor.objects.acreate(organization=organization, **data)
  116. return 201, await get_monitor_queryset(user_id, organization_slug).aget(
  117. id=monitor.id
  118. )
  119. @router.put(
  120. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
  121. response=MonitorSchema,
  122. by_alias=True,
  123. )
  124. async def update_monitor(
  125. request: AuthHttpRequest,
  126. organization_slug: str,
  127. monitor_id: int,
  128. payload: MonitorIn,
  129. ):
  130. monitor = await aget_object_or_404(
  131. get_monitor_queryset(request.auth.user_id, organization_slug),
  132. id=monitor_id,
  133. )
  134. data = payload.dict()
  135. if project_id := data["project"]:
  136. result = await Project.objects.filter(
  137. organization__slug=organization_slug,
  138. organization__users=request.auth.user_id,
  139. id=project_id,
  140. ).afirst()
  141. data["project"] = result
  142. for attr, value in data.items():
  143. setattr(monitor, attr, value)
  144. await monitor.asave()
  145. return monitor
  146. @router.get(
  147. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/checks/",
  148. response=list[MonitorCheckResponseTimeSchema],
  149. by_alias=True,
  150. )
  151. @paginate
  152. async def list_monitor_checks(
  153. request: AuthHttpRequest,
  154. response: HttpResponse,
  155. organization_slug: str,
  156. monitor_id: int,
  157. is_change: bool | None = None,
  158. ):
  159. """
  160. List checks performed for a monitor
  161. Set is_change query param to True to show only changes,
  162. This is useful to see only when a service went up and down.
  163. """
  164. checks = (
  165. MonitorCheck.objects.filter(
  166. monitor_id=monitor_id,
  167. monitor__organization__slug=organization_slug,
  168. monitor__organization__users=request.auth.user_id,
  169. )
  170. .only("is_up", "start_check", "reason", "response_time")
  171. .order_by("-start_check")
  172. )
  173. if is_change is not None:
  174. checks = checks.filter(is_change=is_change)
  175. return checks
  176. @router.get(
  177. "/organizations/{slug:organization_slug}/status-pages/",
  178. response=list[StatusPageSchema],
  179. by_alias=True,
  180. )
  181. @paginate
  182. async def list_status_pages(
  183. request: AuthHttpRequest, response: HttpResponse, organization_slug: str
  184. ):
  185. """List status pages, used for showing the current status of an uptime monitor"""
  186. return StatusPage.objects.filter(
  187. organization__users=request.auth.user_id
  188. ).prefetch_related("monitors")
  189. @router.post(
  190. "/organizations/{slug:organization_slug}/status-pages/",
  191. response={201: StatusPageSchema},
  192. by_alias=True,
  193. )
  194. async def create_status_page(
  195. request: AuthHttpRequest, organization_slug: str, payload: StatusPageIn
  196. ):
  197. organization = await aget_object_or_404(
  198. Organization, slug=organization_slug, users=request.auth.user_id
  199. )
  200. data = payload.dict()
  201. status_page = await StatusPage.objects.acreate(organization=organization, **data)
  202. return 201, await StatusPage.objects.prefetch_related("monitors").aget(
  203. id=status_page.id
  204. )
  205. @router.delete(
  206. "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
  207. response={204: None},
  208. )
  209. async def delete_monitor(
  210. request: AuthHttpRequest, organization_slug: str, monitor_id: int
  211. ):
  212. result, _ = (
  213. await get_monitor_queryset(request.auth.user_id, organization_slug)
  214. .filter(id=monitor_id)
  215. .adelete()
  216. )
  217. if result:
  218. return 204, None
  219. raise Http404