Просмотр исходного кода

feat(monitors): Initial UI

- Add basic mocks for Monitor entries
- Fix project-based access on monitor list
- Add search to monitors list endpoint
- Add monitor details endpoint
- Add (unlinked, draft) monitor edit endpoint
- Fix monitor.id (should be guid)
- Fix monitor checkin status enums
- Fix monitor checkins list (invalid order_by)
David Cramer 6 лет назад
Родитель
Сommit
39ffb503b3

+ 38 - 1
bin/load-mocks

@@ -26,7 +26,7 @@ from sentry.models import (
     Environment, File, Group, GroupMeta, GroupRelease, GroupTombstone, Organization,
     Environment, File, Group, GroupMeta, GroupRelease, GroupTombstone, Organization,
     OrganizationAccessRequest, OrganizationMember, Project, Release,
     OrganizationAccessRequest, OrganizationMember, Project, Release,
     ReleaseCommit, ReleaseEnvironment, ReleaseProjectEnvironment, ReleaseFile, Repository,
     ReleaseCommit, ReleaseEnvironment, ReleaseProjectEnvironment, ReleaseFile, Repository,
-    Team, TOMBSTONE_FIELDS_FROM_GROUP, User, UserReport
+    Team, TOMBSTONE_FIELDS_FROM_GROUP, User, UserReport, Monitor, MonitorStatus, MonitorType, MonitorCheckIn, CheckInStatus
 )
 )
 from sentry.signals import mocks_loaded
 from sentry.signals import mocks_loaded
 from sentry.similarity import features
 from sentry.similarity import features
@@ -61,6 +61,14 @@ ENVIRONMENTS = itertools.cycle([
     ''
     ''
 ])
 ])
 
 
+MONITOR_NAMES = itertools.cycle(settings.CELERYBEAT_SCHEDULE.keys())
+
+MONITOR_SCHEDULES = itertools.cycle([
+    '* * * * *',
+    '0 * * * *',
+    '0 0 * * *',
+])
+
 LONG_MESSAGE = """Code: 0.
 LONG_MESSAGE = """Code: 0.
 DB::Exception: String is too long for DateTime: 2018-10-26T19:14:18+00:00. Stack trace:
 DB::Exception: String is too long for DateTime: 2018-10-26T19:14:18+00:00. Stack trace:
 
 
@@ -390,6 +398,35 @@ def main(num_events=1, extra_events=False):
                     flags=F('flags').bitor(Project.flags.has_releases),
                     flags=F('flags').bitor(Project.flags.has_releases),
                 )
                 )
 
 
+            monitor, created = Monitor.objects.get_or_create(
+                name=next(MONITOR_NAMES),
+                project_id=project.id,
+                organization_id=org.id,
+                type=MonitorType.CRON_JOB,
+                defaults={
+                    'config': {
+                        'schedule': next(MONITOR_SCHEDULES),
+                    },
+                    'next_checkin': timezone.now() + timedelta(minutes=60),
+                    'last_checkin': timezone.now(),
+                }
+            )
+            if not created:
+                if not (monitor.config or {}).get('schedule'):
+                    monitor.config = {'schedule': next(MONITOR_SCHEDULES)}
+                monitor.update(
+                    config=monitor.config,
+                    status=MonitorStatus.ACTIVE if randint(0, 10) < 7 else MonitorStatus.ERROR,
+                    last_checkin=timezone.now(),
+                    next_checkin=monitor.get_next_scheduled_checkin(timezone.now()),
+                )
+
+            MonitorCheckIn.objects.create(
+                project_id=monitor.project_id,
+                monitor=monitor,
+                status=CheckInStatus.OK if monitor.status == MonitorStatus.ACTIVE else CheckInStatus.ERROR,
+            )
+
             with transaction.atomic():
             with transaction.atomic():
                 has_release = Release.objects.filter(
                 has_release = Release.objects.filter(
                     version=sha1(uuid4().bytes).hexdigest(),
                     version=sha1(uuid4().bytes).hexdigest(),

+ 3 - 0
src/sentry/api/base.py

@@ -315,6 +315,9 @@ class StatsMixin(object):
         else:
         else:
             start = end - timedelta(days=1, seconds=-1)
             start = end - timedelta(days=1, seconds=-1)
 
 
+        if not resolution:
+            resolution = tsdb.get_optimal_rollup(start, end)
+
         return {
         return {
             'start': start,
             'start': start,
             'end': end,
             'end': end,

+ 45 - 0
src/sentry/api/bases/monitor.py

@@ -0,0 +1,45 @@
+from __future__ import absolute_import
+
+from sentry import features
+from sentry.api.base import Endpoint
+from sentry.api.exceptions import ResourceDoesNotExist
+from sentry.api.bases.project import ProjectPermission
+from sentry.models import Monitor, Project, ProjectStatus
+from sentry.utils.sdk import configure_scope
+
+
+class MonitorEndpoint(Endpoint):
+    permission_classes = (ProjectPermission,)
+
+    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)

+ 5 - 45
src/sentry/api/endpoints/monitor_checkins.py

@@ -3,15 +3,11 @@ from __future__ import absolute_import
 from django.db import transaction
 from django.db import transaction
 from rest_framework import serializers
 from rest_framework import serializers
 
 
-from sentry import features
 from sentry.api.authentication import DSNAuthentication
 from sentry.api.authentication import DSNAuthentication
-from sentry.api.base import Endpoint
-from sentry.api.exceptions import ResourceDoesNotExist
+from sentry.api.bases.monitor import MonitorEndpoint
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.paginator import OffsetPaginator
-from sentry.api.bases.project import ProjectPermission
 from sentry.api.serializers import serialize
 from sentry.api.serializers import serialize
-from sentry.models import Monitor, MonitorCheckIn, MonitorStatus, CheckInStatus, Project, ProjectKey, ProjectStatus
-from sentry.utils.sdk import configure_scope
+from sentry.models import Monitor, MonitorCheckIn, MonitorStatus, CheckInStatus, ProjectKey
 
 
 
 
 class CheckInSerializer(serializers.Serializer):
 class CheckInSerializer(serializers.Serializer):
@@ -25,44 +21,8 @@ class CheckInSerializer(serializers.Serializer):
     duration = serializers.IntegerField(required=False)
     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)
+class MonitorCheckInsEndpoint(MonitorEndpoint):
+    authentication_classes = MonitorEndpoint.authentication_classes + (DSNAuthentication,)
 
 
     def get(self, request, project, monitor):
     def get(self, request, project, monitor):
         """
         """
@@ -83,7 +43,7 @@ class MonitorCheckInsEndpoint(Endpoint):
         return self.paginate(
         return self.paginate(
             request=request,
             request=request,
             queryset=queryset,
             queryset=queryset,
-            order_by='name',
+            order_by='-date_added',
             on_results=lambda x: serialize(x, request.user),
             on_results=lambda x: serialize(x, request.user),
             paginator_cls=OffsetPaginator,
             paginator_cls=OffsetPaginator,
         )
         )

+ 16 - 0
src/sentry/api/endpoints/monitor_details.py

@@ -0,0 +1,16 @@
+from __future__ import absolute_import
+
+from sentry.api.bases.monitor import MonitorEndpoint
+from sentry.api.serializers import serialize
+
+
+class MonitorDetailsEndpoint(MonitorEndpoint):
+    def get(self, request, project, monitor):
+        """
+        Retrieve a monitor
+        ``````````````````
+
+        :pparam string monitor_id: the id of the monitor.
+        :auth: required
+        """
+        return self.respond(serialize(monitor, request.user))

+ 43 - 0
src/sentry/api/endpoints/monitor_stats.py

@@ -0,0 +1,43 @@
+from __future__ import absolute_import
+
+import six
+
+from collections import OrderedDict
+from rest_framework.response import Response
+
+from sentry import tsdb
+from sentry.api.base import StatsMixin
+from sentry.api.bases.monitor import MonitorEndpoint
+from sentry.models import MonitorCheckIn, CheckInStatus
+
+
+class MonitorStatsEndpoint(MonitorEndpoint, StatsMixin):
+    # TODO(dcramer): probably convert to tsdb
+    def get(self, request, project, monitor):
+        args = self._parse_args(request)
+
+        stats = OrderedDict()
+        current = tsdb.normalize_to_epoch(args['start'], args['rollup'])
+        end = tsdb.normalize_to_epoch(args['end'], args['rollup'])
+        while current <= end:
+            stats[current] = {CheckInStatus.OK: 0, CheckInStatus.ERROR: 0}
+            current += args['rollup']
+
+        history = MonitorCheckIn.objects.filter(
+            monitor=monitor,
+            status__in=[CheckInStatus.OK, CheckInStatus.ERROR],
+            date_added__gt=args['start'],
+            date_added__lte=args['end'],
+        ).values_list('date_added', 'status')
+        for datetime, status in history.iterator():
+            stats[tsdb.normalize_to_epoch(datetime, args['rollup'])][status] += 1
+
+        return Response(
+            [
+                {
+                    'ts': ts,
+                    'ok': data[CheckInStatus.OK],
+                    'error': data[CheckInStatus.ERROR],
+                } for ts, data in six.iteritems(stats)
+            ]
+        )

+ 66 - 2
src/sentry/api/endpoints/organization_monitors.py

@@ -1,11 +1,27 @@
 from __future__ import absolute_import
 from __future__ import absolute_import
 
 
+import six
+
+from django.db.models import Q
+
 from sentry import features
 from sentry import features
+from sentry.api.bases import NoProjects, OrganizationEventsError
 from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.bases.organization import OrganizationEndpoint
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import serialize
 from sentry.api.serializers import serialize
-from sentry.models import Monitor
+from sentry.models import Monitor, MonitorStatus, MonitorType
+from sentry.search.utils import tokenize_query
+from sentry.db.models.query import in_iexact
+
+
+def map_value_to_constant(constant, value):
+    value = value.upper()
+    if value == 'OK':
+        value = 'ACTIVE'
+    if not hasattr(constant, value):
+        raise ValueError(value)
+    return getattr(constant, value)
 
 
 
 
 class OrganizationMonitorsEndpoint(OrganizationEndpoint):
 class OrganizationMonitorsEndpoint(OrganizationEndpoint):
@@ -21,14 +37,62 @@ class OrganizationMonitorsEndpoint(OrganizationEndpoint):
                             organization, actor=request.user):
                             organization, actor=request.user):
             raise ResourceDoesNotExist
             raise ResourceDoesNotExist
 
 
+        try:
+            filter_params = self.get_filter_params(
+                request,
+                organization,
+                date_filter_optional=True,
+            )
+        except NoProjects:
+            return self.respond([])
+        except OrganizationEventsError as exc:
+            return self.respond({'detail': exc.message}, status=400)
+
         queryset = Monitor.objects.filter(
         queryset = Monitor.objects.filter(
             organization_id=organization.id,
             organization_id=organization.id,
+            project_id__in=filter_params['project_id'],
+        )
+        query = request.GET.get('query')
+        if query:
+            tokens = tokenize_query(query)
+            for key, value in six.iteritems(tokens):
+                if key == 'query':
+                    value = ' '.join(value)
+                    queryset = queryset.filter(Q(name__icontains=value) | Q(id__iexact=value))
+                elif key == 'id':
+                    queryset = queryset.filter(in_iexact('id', value))
+                elif key == 'name':
+                    queryset = queryset.filter(in_iexact('name', value))
+                elif key == 'status':
+                    try:
+                        queryset = queryset.filter(
+                            status__in=map_value_to_constant(
+                                MonitorStatus, value))
+                    except ValueError:
+                        queryset = queryset.none()
+                elif key == 'type':
+                    try:
+                        queryset = queryset.filter(
+                            status__in=map_value_to_constant(
+                                MonitorType, value))
+                    except ValueError:
+                        queryset = queryset.none()
+
+                elif key == 'id':
+                    queryset = queryset.filter(id__in=value)
+                else:
+                    queryset = queryset.none()
+
+        queryset = queryset.extra(
+            select={
+                'is_error': 'sentry_monitor.status = %s' % (MonitorStatus.ERROR,),
+            },
         )
         )
 
 
         return self.paginate(
         return self.paginate(
             request=request,
             request=request,
             queryset=queryset,
             queryset=queryset,
-            order_by='name',
+            order_by=('-is_error', '-name'),
             on_results=lambda x: serialize(x, request.user),
             on_results=lambda x: serialize(x, request.user),
             paginator_cls=OffsetPaginator,
             paginator_cls=OffsetPaginator,
         )
         )

+ 3 - 0
src/sentry/api/serializers/models/monitor.py

@@ -12,6 +12,9 @@ class MonitorSerializer(Serializer):
         return {
         return {
             'id': six.text_type(obj.guid),
             'id': six.text_type(obj.guid),
             'status': obj.get_status_display(),
             'status': obj.get_status_display(),
+            'type': obj.get_type_display(),
             'name': obj.name,
             'name': obj.name,
+            'lastCheckIn': obj.last_checkin,
+            'nextCheckIn': obj.next_checkin,
             'dateCreated': obj.date_added,
             'dateCreated': obj.date_added,
         }
         }

+ 4 - 0
src/sentry/api/urls.py

@@ -51,6 +51,8 @@ from .endpoints.internal_quotas import InternalQuotasEndpoint
 from .endpoints.internal_stats import InternalStatsEndpoint
 from .endpoints.internal_stats import InternalStatsEndpoint
 from .endpoints.monitor_checkins import MonitorCheckInsEndpoint
 from .endpoints.monitor_checkins import MonitorCheckInsEndpoint
 from .endpoints.monitor_checkin_details import MonitorCheckInDetailsEndpoint
 from .endpoints.monitor_checkin_details import MonitorCheckInDetailsEndpoint
+from .endpoints.monitor_details import MonitorDetailsEndpoint
+from .endpoints.monitor_stats import MonitorStatsEndpoint
 from .endpoints.organization_access_request_details import OrganizationAccessRequestDetailsEndpoint
 from .endpoints.organization_access_request_details import OrganizationAccessRequestDetailsEndpoint
 from .endpoints.organization_activity import OrganizationActivityEndpoint
 from .endpoints.organization_activity import OrganizationActivityEndpoint
 from .endpoints.organization_auditlogs import OrganizationAuditLogsEndpoint
 from .endpoints.organization_auditlogs import OrganizationAuditLogsEndpoint
@@ -299,9 +301,11 @@ urlpatterns = patterns(
         name='sentry-api-0-accept-project-transfer'),
         name='sentry-api-0-accept-project-transfer'),
 
 
     # Monitors
     # Monitors
+    url(r'^monitors/(?P<monitor_id>[^\/]+)/$', MonitorDetailsEndpoint.as_view()),
     url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/$', MonitorCheckInsEndpoint.as_view()),
     url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/$', MonitorCheckInsEndpoint.as_view()),
     url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/(?P<checkin_id>[^\/]+)/$',
     url(r'^monitors/(?P<monitor_id>[^\/]+)/checkins/(?P<checkin_id>[^\/]+)/$',
         MonitorCheckInDetailsEndpoint.as_view()),
         MonitorCheckInDetailsEndpoint.as_view()),
+    url(r'^monitors/(?P<monitor_id>[^\/]+)/stats/$', MonitorStatsEndpoint.as_view()),
 
 
     # Users
     # Users
     url(r'^users/$', UserIndexEndpoint.as_view(), name='sentry-api-0-user-index'),
     url(r'^users/$', UserIndexEndpoint.as_view(), name='sentry-api-0-user-index'),

+ 2 - 1
src/sentry/models/monitor.py

@@ -1,6 +1,7 @@
 from __future__ import absolute_import, print_function
 from __future__ import absolute_import, print_function
 
 
 import pytz
 import pytz
+import six
 
 
 from croniter import croniter
 from croniter import croniter
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
@@ -124,7 +125,7 @@ class Monitor(Model):
                 },
                 },
                 'contexts': {
                 'contexts': {
                     'monitor': {
                     'monitor': {
-                        'id': self.id,
+                        'id': six.text_type(self.guid),
                     },
                     },
                 },
                 },
             },
             },

Некоторые файлы не были показаны из-за большого количества измененных файлов