123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- from datetime import timedelta
- from uuid import UUID
- from django.db.models import F, Prefetch, Window
- from django.db.models.functions import RowNumber
- from django.http import Http404, HttpRequest, HttpResponse
- from django.shortcuts import aget_object_or_404
- from django.utils import timezone
- from ninja import Router
- from ninja.pagination import paginate
- from apps.organizations_ext.models import Organization
- from apps.projects.models import Project
- from glitchtip.api.authentication import AuthHttpRequest
- from glitchtip.utils import async_call_celery_task
- from .models import Monitor, MonitorCheck, StatusPage
- from .schema import (
- MonitorCheckResponseTimeSchema,
- MonitorCheckSchema,
- MonitorDetailSchema,
- MonitorIn,
- MonitorSchema,
- StatusPageIn,
- StatusPageSchema,
- )
- from .tasks import send_monitor_notification
- router = Router()
- def get_monitor_queryset(user_id: int, organization_slug: str):
- return (
- Monitor.objects.with_check_annotations()
- .filter(organization__users=user_id, organization__slug=organization_slug)
- # Fetch latest 60 checks for each monitor
- .prefetch_related(
- Prefetch(
- "checks",
- queryset=MonitorCheck.objects.filter( # Optimization
- start_check__gt=timezone.now() - timedelta(hours=12)
- )
- .annotate(
- row_number=Window(
- expression=RowNumber(),
- order_by="-start_check",
- partition_by=F("monitor"),
- ),
- )
- .filter(row_number__lte=60)
- .distinct(),
- )
- )
- .select_related("project", "organization")
- )
- @router.post(
- "organizations/{slug:organization_slug}/heartbeat_check/{uuid:endpoint_id}/",
- response=MonitorCheckSchema,
- auth=None,
- )
- async def heartbeat_check(
- request: HttpRequest, organization_slug: str, endpoint_id: UUID
- ):
- """
- Heartbeat monitors allow an external service to contact this endpoint
- when the service is up.
- """
- monitor = await aget_object_or_404(
- Monitor.objects.with_check_annotations(),
- organization__slug=organization_slug,
- endpoint_id=endpoint_id,
- )
- monitor_check = await MonitorCheck.objects.acreate(
- monitor=monitor,
- is_up=True,
- reason=None,
- is_change=monitor.latest_is_up is not True,
- )
- if monitor.latest_is_up is False:
- await async_call_celery_task(
- send_monitor_notification, monitor_check.pk, False, monitor.last_change
- )
- return monitor_check
- @router.get(
- "organizations/{slug:organization_slug}/monitors/",
- response=list[MonitorSchema],
- by_alias=True,
- )
- @paginate
- async def list_monitors(
- request: AuthHttpRequest, response: HttpResponse, organization_slug: str
- ):
- return get_monitor_queryset(request.auth.user_id, organization_slug)
- @router.get(
- "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
- response=MonitorDetailSchema,
- by_alias=True,
- )
- async def get_monitor(
- request: AuthHttpRequest, organization_slug: str, monitor_id: int
- ):
- return await aget_object_or_404(
- get_monitor_queryset(request.auth.user_id, organization_slug),
- id=monitor_id,
- )
- @router.post(
- "organizations/{slug:organization_slug}/monitors/",
- response={201: MonitorSchema},
- by_alias=True,
- )
- async def create_monitor(
- request: AuthHttpRequest, organization_slug: str, payload: MonitorIn
- ):
- user_id = request.auth.user_id
- organization = await aget_object_or_404(
- Organization, slug=organization_slug, users=user_id
- )
- data = payload.dict(exclude_defaults=True)
- if project_id := data.pop("project", None):
- data["project"] = await organization.projects.filter(id=project_id).afirst()
- monitor = await Monitor.objects.acreate(organization=organization, **data)
- return 201, await get_monitor_queryset(user_id, organization_slug).aget(
- id=monitor.id
- )
- @router.put(
- "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
- response=MonitorSchema,
- by_alias=True,
- )
- async def update_monitor(
- request: AuthHttpRequest,
- organization_slug: str,
- monitor_id: int,
- payload: MonitorIn,
- ):
- monitor = await aget_object_or_404(
- get_monitor_queryset(request.auth.user_id, organization_slug),
- id=monitor_id,
- )
- data = payload.dict()
- if project_id := data["project"]:
- result = await Project.objects.filter(
- organization__slug=organization_slug,
- organization__users=request.auth.user_id,
- id=project_id,
- ).afirst()
- data["project"] = result
- for attr, value in data.items():
- setattr(monitor, attr, value)
- await monitor.asave()
- return monitor
- @router.get(
- "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/checks/",
- response=list[MonitorCheckResponseTimeSchema],
- by_alias=True,
- )
- @paginate
- async def list_monitor_checks(
- request: AuthHttpRequest,
- response: HttpResponse,
- organization_slug: str,
- monitor_id: int,
- is_change: bool | None = None,
- ):
- """
- List checks performed for a monitor
- Set is_change query param to True to show only changes,
- This is useful to see only when a service went up and down.
- """
- checks = (
- MonitorCheck.objects.filter(
- monitor_id=monitor_id,
- monitor__organization__slug=organization_slug,
- monitor__organization__users=request.auth.user_id,
- )
- .only("is_up", "start_check", "reason", "response_time")
- .order_by("-start_check")
- )
- if is_change is not None:
- checks = checks.filter(is_change=is_change)
- return checks
- @router.get(
- "/organizations/{slug:organization_slug}/status-pages/",
- response=list[StatusPageSchema],
- by_alias=True,
- )
- @paginate
- async def list_status_pages(
- request: AuthHttpRequest, response: HttpResponse, organization_slug: str
- ):
- """List status pages, used for showing the current status of an uptime monitor"""
- return StatusPage.objects.filter(
- organization__users=request.auth.user_id
- ).prefetch_related("monitors")
- @router.post(
- "/organizations/{slug:organization_slug}/status-pages/",
- response={201: StatusPageSchema},
- by_alias=True,
- )
- async def create_status_page(
- request: AuthHttpRequest, organization_slug: str, payload: StatusPageIn
- ):
- organization = await aget_object_or_404(
- Organization, slug=organization_slug, users=request.auth.user_id
- )
- data = payload.dict()
- status_page = await StatusPage.objects.acreate(organization=organization, **data)
- return 201, await StatusPage.objects.prefetch_related("monitors").aget(
- id=status_page.id
- )
- @router.delete(
- "organizations/{slug:organization_slug}/monitors/{int:monitor_id}/",
- response={204: None},
- )
- async def delete_monitor(
- request: AuthHttpRequest, organization_slug: str, monitor_id: int
- ):
- result, _ = (
- await get_monitor_queryset(request.auth.user_id, organization_slug)
- .filter(id=monitor_id)
- .adelete()
- )
- if result:
- return 204, None
- raise Http404
|