Skip to content

Commit

Permalink
feat: Add OpenAPI docs for monitor endpoints (#42850)
Browse files Browse the repository at this point in the history
Refs #42283
  • Loading branch information
dcramer authored Jan 6, 2023
1 parent ccb5b57 commit 37c3ee8
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 39 deletions.
68 changes: 54 additions & 14 deletions src/sentry/api/endpoints/monitor_checkin_details.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from django.db import transaction
from django.utils import timezone
from rest_framework import serializers
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response

from sentry.api.authentication import DSNAuthentication
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.bases.monitor import MonitorEndpoint, ProjectMonitorPermission
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.fields.empty_integer import EmptyIntegerField
from sentry.api.serializers import serialize
from sentry.api.serializers.models.monitorcheckin import MonitorCheckInSerializerResponse
from sentry.api.validators import MonitorCheckInValidator
from sentry.apidocs.constants import (
RESPONSE_ALREADY_REPORTED,
RESPONSE_BAD_REQUEST,
RESPONSE_FORBIDDEN,
RESPONSE_NOTFOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models import (
CheckInStatus,
Monitor,
Expand All @@ -22,21 +32,12 @@
from sentry.utils.sdk import bind_organization_context, configure_scope


class CheckInSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=(
("ok", CheckInStatus.OK),
("error", CheckInStatus.ERROR),
("in_progress", CheckInStatus.IN_PROGRESS),
)
)
duration = EmptyIntegerField(required=False, allow_null=True)


@region_silo_endpoint
@extend_schema(tags=["Crons"])
class MonitorCheckInDetailsEndpoint(Endpoint):
authentication_classes = MonitorEndpoint.authentication_classes + (DSNAuthentication,)
permission_classes = (ProjectMonitorPermission,)
public = {"GET", "PUT"}

# TODO(dcramer): this code needs shared with other endpoints as its security focused
# TODO(dcramer): this doesnt handle is_global roles
Expand Down Expand Up @@ -82,6 +83,23 @@ def convert_args(self, request: Request, monitor_id, checkin_id, *args, **kwargs
kwargs.update({"checkin": checkin, "monitor": monitor, "project": project})
return (args, kwargs)

@extend_schema(
operation_id="Retrieve a check-in",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
MONITOR_PARAMS.CHECKIN_ID,
],
request=None,
responses={
201: inline_sentry_response_serializer(
"MonitorCheckIn", MonitorCheckInSerializerResponse
),
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def get(self, request: Request, project, monitor, checkin) -> Response:
"""
Retrieve a check-in
Expand All @@ -100,6 +118,28 @@ def get(self, request: Request, project, monitor, checkin) -> Response:

return self.respond(serialize(checkin, request.user))

@extend_schema(
operation_id="Update a check-in",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
MONITOR_PARAMS.CHECKIN_ID,
],
request=MonitorCheckInValidator,
responses={
200: inline_sentry_response_serializer(
"MonitorCheckIn2", MonitorCheckInSerializerResponse
),
208: RESPONSE_ALREADY_REPORTED,
400: RESPONSE_BAD_REQUEST,
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
examples=[
# OpenApiExample()
],
)
def put(self, request: Request, project, monitor, checkin) -> Response:
"""
Update a check-in
Expand All @@ -115,7 +155,7 @@ def put(self, request: Request, project, monitor, checkin) -> Response:
if checkin.status in CheckInStatus.FINISHED_VALUES:
return self.respond(status=400)

serializer = CheckInSerializer(
serializer = MonitorCheckInValidator(
data=request.data, partial=True, context={"project": project, "request": request}
)
if not serializer.is_valid():
Expand Down
75 changes: 58 additions & 17 deletions src/sentry/api/endpoints/monitor_checkins.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
from __future__ import annotations

from typing import List

from django.db import transaction
from rest_framework import serializers
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response

from sentry.api.authentication import DSNAuthentication
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.monitor import MonitorEndpoint
from sentry.api.fields.empty_integer import EmptyIntegerField
from sentry.api.paginator import OffsetPaginator
from sentry.api.serializers import serialize
from sentry.api.serializers.models.monitorcheckin import MonitorCheckInSerializerResponse
from sentry.api.validators import MonitorCheckInValidator
from sentry.apidocs.constants import (
RESPONSE_BAD_REQUEST,
RESPONSE_FORBIDDEN,
RESPONSE_NOTFOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models import CheckInStatus, Monitor, MonitorCheckIn, MonitorStatus, ProjectKey


class CheckInSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=(
("ok", CheckInStatus.OK),
("error", CheckInStatus.ERROR),
("in_progress", CheckInStatus.IN_PROGRESS),
)
)
duration = EmptyIntegerField(required=False, allow_null=True)


@region_silo_endpoint
@extend_schema(tags=["Crons"])
class MonitorCheckInsEndpoint(MonitorEndpoint):
authentication_classes = MonitorEndpoint.authentication_classes + (DSNAuthentication,)

public = {"GET", "POST"}

@extend_schema(
operation_id="Retrieve check-ins for a monitor",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
MONITOR_PARAMS.CHECKIN_ID,
],
responses={
200: inline_sentry_response_serializer(
"CheckInList", List[MonitorCheckInSerializerResponse]
),
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def get(
self, request: Request, project, monitor, organization_slug: str | None = None
) -> Response:
"""
Retrieve check-ins for an monitor
`````````````````````````````````
Retrieve check-ins for a monitor
````````````````````````````````
:pparam string monitor_id: the id of the monitor.
:auth: required
Expand All @@ -57,6 +75,27 @@ def get(
paginator_cls=OffsetPaginator,
)

@extend_schema(
operation_id="Create a new check-in",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
MONITOR_PARAMS.CHECKIN_ID,
],
request=MonitorCheckInValidator,
responses={
200: inline_sentry_response_serializer(
"MonitorCheckIn", MonitorCheckInSerializerResponse
),
201: inline_sentry_response_serializer(
"MonitorCheckIn", MonitorCheckInSerializerResponse
),
400: RESPONSE_BAD_REQUEST,
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def post(
self, request: Request, project, monitor, organization_slug: str | None = None
) -> Response:
Expand All @@ -66,6 +105,8 @@ def post(
:pparam string monitor_id: the id of the monitor.
:auth: required
Note: If a DSN is utilized for authentication, the response will be limited in details.
"""
if organization_slug:
if project.organization.slug != organization_slug:
Expand All @@ -74,7 +115,7 @@ def post(
if monitor.status in [MonitorStatus.PENDING_DELETION, MonitorStatus.DELETION_IN_PROGRESS]:
return self.respond(status=404)

serializer = CheckInSerializer(
serializer = MonitorCheckInValidator(
data=request.data, context={"project": project, "request": request}
)
if not serializer.is_valid():
Expand Down
54 changes: 54 additions & 0 deletions src/sentry/api/endpoints/monitor_details.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
from __future__ import annotations

from django.db import transaction
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import audit_log
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.monitor import MonitorEndpoint
from sentry.api.serializers import serialize
from sentry.api.serializers.models.monitor import MonitorSerializerResponse
from sentry.api.validators import MonitorValidator
from sentry.apidocs.constants import (
RESPONSE_ACCEPTED,
RESPONSE_FORBIDDEN,
RESPONSE_NOTFOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models import Monitor, MonitorStatus, ScheduledDeletion


@region_silo_endpoint
@extend_schema(tags=["Crons"])
class MonitorDetailsEndpoint(MonitorEndpoint):
public = {"GET", "PUT", "DELETE"}

@extend_schema(
operation_id="Retrieve a monitor",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
],
responses={
200: inline_sentry_response_serializer("Monitor", MonitorSerializerResponse),
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def get(
self, request: Request, project, monitor, organization_slug: str | None = None
) -> Response:
Expand All @@ -30,6 +56,20 @@ def get(

return self.respond(serialize(monitor, request.user))

@extend_schema(
operation_id="Update a monitor",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
],
request=MonitorValidator,
responses={
200: inline_sentry_response_serializer("Monitor", MonitorSerializerResponse),
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def put(
self, request: Request, project, monitor, organization_slug: str | None = None
) -> Response:
Expand Down Expand Up @@ -87,6 +127,20 @@ def put(

return self.respond(serialize(monitor, request.user))

@extend_schema(
operation_id="Delete a monitor",
parameters=[
GLOBAL_PARAMS.ORG_SLUG,
MONITOR_PARAMS.MONITOR_ID,
],
request=MonitorValidator,
responses={
202: RESPONSE_ACCEPTED,
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOTFOUND,
},
)
def delete(
self, request: Request, project, monitor, organization_slug: str | None = None
) -> Response:
Expand Down
19 changes: 19 additions & 0 deletions src/sentry/api/serializers/models/monitor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from datetime import datetime
from typing import Any

from typing_extensions import TypedDict

from sentry.api.serializers import Serializer, register, serialize
from sentry.models import Monitor, Project

from .project import ProjectSerializerResponse


@register(Monitor)
class MonitorSerializer(Serializer):
Expand Down Expand Up @@ -33,3 +40,15 @@ def serialize(self, obj, attrs, user):
"dateCreated": obj.date_added,
"project": attrs["project"],
}


class MonitorSerializerResponse(TypedDict):
id: str
name: str
status: str
type: str
config: Any
dateCreated: datetime
lastCheckIn: datetime
nextCheckIn: datetime
project: ProjectSerializerResponse
11 changes: 11 additions & 0 deletions src/sentry/api/serializers/models/monitorcheckin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from datetime import datetime

from typing_extensions import TypedDict

from sentry.api.serializers import Serializer, register
from sentry.models import MonitorCheckIn

Expand All @@ -11,3 +15,10 @@ def serialize(self, obj, attrs, user):
"duration": obj.duration,
"dateCreated": obj.date_added,
}


class MonitorCheckInSerializerResponse(TypedDict):
id: str
status: str
duration: int
dateCreated: datetime
2 changes: 2 additions & 0 deletions src/sentry/api/serializers/rest_framework/project.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from sentry.models import Project

ValidationError = serializers.ValidationError


@extend_schema_field(str)
class ProjectField(serializers.Field):
def __init__(self, scope="project:write"):
self.scope = scope
Expand Down
Loading

0 comments on commit 37c3ee8

Please sign in to comment.