Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ celery>=3.1.8,<3.1.19
cffi>=1.11.5,<2.0
click>=5.0,<7.0
# 'cryptography>=1.3,<1.4
croniter>=0.3.26,<0.4.0
cssutils>=0.9.9,<0.10.0
django-crispy-forms>=1.4.0,<1.5.0
django-jsonfield>=0.9.13,<0.9.14
Expand Down
142 changes: 142 additions & 0 deletions src/sentry/api/endpoints/monitor_checkin_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from __future__ import absolute_import

from django.db import transaction
from django.utils import timezone
from rest_framework import serializers

from sentry import features
from sentry.api.authentication import DSNAuthentication
from sentry.api.base import Endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.bases.project import ProjectPermission
from sentry.api.serializers import serialize
from sentry.models import Monitor, MonitorCheckIn, CheckInStatus, MonitorStatus, Project, ProjectKey, ProjectStatus
from sentry.utils.sdk import configure_scope


class CheckInSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=(
('ok', CheckInStatus.OK),
('error', CheckInStatus.ERROR),
('in_progress', CheckInStatus.IN_PROGRESS),
),
)
duration = serializers.IntegerField(required=False)


class MonitorCheckInDetailsEndpoint(Endpoint):
authentication_classes = Endpoint.authentication_classes + (DSNAuthentication,)
permission_classes = (ProjectPermission,)

# TODO(dcramer): this code needs shared with other endpoints as its security focused
# TODO(dcramer): this doesnt handle is_global roles
def convert_args(self, request, monitor_id, checkin_id, *args, **kwargs):
try:
monitor = Monitor.objects.get(
guid=monitor_id,
)
except Monitor.DoesNotExist:
raise ResourceDoesNotExist

project = Project.objects.get_from_cache(id=monitor.project_id)
if project.status != ProjectStatus.VISIBLE:
raise ResourceDoesNotExist

if hasattr(request.auth, 'project_id') and project.id != request.auth.project_id:
return self.respond(status=400)

if not features.has('organizations:monitors',
project.organization, actor=request.user):
raise ResourceDoesNotExist

self.check_object_permissions(request, project)

with configure_scope() as scope:
scope.set_tag("organization", project.organization_id)
scope.set_tag("project", project.id)

try:
checkin = MonitorCheckIn.objects.get(
monitor=monitor,
guid=checkin_id,
)
except MonitorCheckIn.DoesNotExist:
raise ResourceDoesNotExist

request._request.organization = project.organization

kwargs.update({
'checkin': checkin,
'monitor': monitor,
'project': project,
})
return (args, kwargs)

def get(self, request, project, monitor, checkin):
"""
Retrieve a check-in
``````````````````

:pparam string monitor_id: the id of the monitor.
:pparam string checkin_id: the id of the check-in.
:auth: required
"""
# we dont allow read permission with DSNs
if isinstance(request.auth, ProjectKey):
return self.respond(status=401)

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

def put(self, request, project, monitor, checkin):
"""
Update a check-in
`````````````````

:pparam string monitor_id: the id of the monitor.
:pparam string checkin_id: the id of the check-in.
:auth: required
"""
if checkin.status in CheckInStatus.FINISHED_VALUES:
return self.respond(status=400)

serializer = CheckInSerializer(
data=request.DATA,
partial=True,
context={
'project': project,
'request': request,
},
)
if not serializer.is_valid():
return self.respond(serializer.errors, status=400)

result = serializer.object

current_datetime = timezone.now()
params = {
'date_updated': current_datetime,
}
if 'duration' in result:
params['duration'] = result['duration']
if 'status' in result:
params['status'] = getattr(CheckInStatus, result['status'].upper())

with transaction.atomic():
checkin.update(**params)
if checkin.status == CheckInStatus.ERROR:
monitor.mark_failed(current_datetime)
else:
monitor_params = {
'last_checkin': current_datetime,
'next_checkin': monitor.get_next_scheduled_checkin(current_datetime),
}
if checkin.status == CheckInStatus.OK:
monitor_params['status'] = MonitorStatus.OK
Monitor.objects.filter(
id=monitor.id,
).exclude(
last_checkin__gt=current_datetime,
).update(**monitor_params)

return self.respond(serialize(checkin, request.user))
133 changes: 133 additions & 0 deletions src/sentry/api/endpoints/monitor_checkins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from __future__ import absolute_import

from django.db import transaction
from rest_framework import serializers

from sentry import features
from sentry.api.authentication import DSNAuthentication
from sentry.api.base import Endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.paginator import OffsetPaginator
from sentry.api.bases.project import ProjectPermission
from sentry.api.serializers import serialize
from sentry.models import Monitor, MonitorCheckIn, MonitorStatus, CheckInStatus, Project, ProjectKey, ProjectStatus
from sentry.utils.sdk import configure_scope


class CheckInSerializer(serializers.Serializer):
status = serializers.ChoiceField(
choices=(
('ok', CheckInStatus.OK),
('error', CheckInStatus.ERROR),
('in_progress', CheckInStatus.IN_PROGRESS),
),
)
duration = serializers.IntegerField(required=False)


class MonitorCheckInsEndpoint(Endpoint):
authentication_classes = Endpoint.authentication_classes + (DSNAuthentication,)
permission_classes = (ProjectPermission,)

# TODO(dcramer): this code needs shared with other endpoints as its security focused
# TODO(dcramer): this doesnt handle is_global roles
def convert_args(self, request, monitor_id, *args, **kwargs):
try:
monitor = Monitor.objects.get(
guid=monitor_id,
)
except Monitor.DoesNotExist:
raise ResourceDoesNotExist

project = Project.objects.get_from_cache(id=monitor.project_id)
if project.status != ProjectStatus.VISIBLE:
raise ResourceDoesNotExist

if hasattr(request.auth, 'project_id') and project.id != request.auth.project_id:
return self.respond(status=400)

if not features.has('organizations:monitors',
project.organization, actor=request.user):
raise ResourceDoesNotExist

self.check_object_permissions(request, project)

with configure_scope() as scope:
scope.set_tag("organization", project.organization_id)
scope.set_tag("project", project.id)

request._request.organization = project.organization

kwargs.update({
'monitor': monitor,
'project': project,
})
return (args, kwargs)

def get(self, request, project, monitor):
"""
Retrieve check-ins for an monitor
`````````````````````````````````

:pparam string monitor_id: the id of the monitor.
:auth: required
"""
# we dont allow read permission with DSNs
if isinstance(request.auth, ProjectKey):
return self.respond(status=401)

queryset = MonitorCheckIn.objects.filter(
monitor_id=monitor.id,
)

return self.paginate(
request=request,
queryset=queryset,
order_by='name',
on_results=lambda x: serialize(x, request.user),
paginator_cls=OffsetPaginator,
)

def post(self, request, project, monitor):
"""
Create a new check-in for a monitor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you thinking that users would make an API request directly, or would monitor creation be part of the SDKs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will happen via the UI or API

```````````````````````````````````

:pparam string monitor_id: the id of the monitor.
:auth: required
"""
serializer = CheckInSerializer(
data=request.DATA,
context={
'project': project,
'request': request,
},
)
if not serializer.is_valid():
return self.respond(serializer.errors, status=400)

result = serializer.object

with transaction.atomic():
checkin = MonitorCheckIn.objects.create(
project_id=project.id,
monitor_id=monitor.id,
duration=result.get('duration'),
status=getattr(CheckInStatus, result['status'].upper()),
)
if checkin.status == CheckInStatus.ERROR:
monitor.mark_failed(last_checkin=checkin.date_added)
else:
monitor_params = {
'last_checkin': checkin.date_added,
'next_checkin': monitor.get_next_scheduled_checkin(checkin.date_added),
}
if checkin.status == CheckInStatus.OK:
monitor_params['status'] = MonitorStatus.OK
Monitor.objects.filter(
id=monitor.id,
).exclude(
last_checkin__gt=checkin.date_added,
).update(**monitor_params)

return self.respond(serialize(checkin, request.user))
17 changes: 17 additions & 0 deletions src/sentry/api/serializers/models/monitorcheckin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import absolute_import

import six

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


@register(MonitorCheckIn)
class MonitorCheckInSerializer(Serializer):
def serialize(self, obj, attrs, user):
return {
'id': six.text_type(obj.guid),
'status': obj.get_status_display(),
'duration': obj.duration,
'dateCreated': obj.date_added,
}
7 changes: 7 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
from .endpoints.internal_queue_tasks import InternalQueueTasksEndpoint
from .endpoints.internal_quotas import InternalQuotasEndpoint
from .endpoints.internal_stats import InternalStatsEndpoint
from .endpoints.monitor_checkins import MonitorCheckInsEndpoint
from .endpoints.monitor_checkin_details import MonitorCheckInDetailsEndpoint
from .endpoints.organization_access_request_details import OrganizationAccessRequestDetailsEndpoint
from .endpoints.organization_activity import OrganizationActivityEndpoint
from .endpoints.organization_auditlogs import OrganizationAuditLogsEndpoint
Expand Down Expand Up @@ -290,6 +292,11 @@
url(r'^accept-transfer/$', AcceptProjectTransferEndpoint.as_view(),
name='sentry-api-0-accept-project-transfer'),

# Monitors
url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/$', MonitorCheckInsEndpoint.as_view()),
url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/(?P<checkin_id>[^\/]+)/$',
MonitorCheckInDetailsEndpoint.as_view()),

# Users
url(r'^users/$', UserIndexEndpoint.as_view(), name='sentry-api-0-user-index'),
url(
Expand Down
7 changes: 7 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,13 @@ def create_partitioned_queues(name):
'expires': 30,
},
},
'check-monitors': {
'task': 'sentry.tasks.check_monitors',
'schedule': timedelta(minutes=1),
'options': {
'expires': 60,
},
},
'clear-expired-snoozes': {
'task': 'sentry.tasks.clear_expired_snoozes',
'schedule': timedelta(minutes=5),
Expand Down
1 change: 1 addition & 0 deletions src/sentry/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
from .gzippeddict import * # NOQA
from .node import * # NOQA
from .pickle import * # NOQA
from .uuid import * # NOQA
Loading