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

feat(monitors): Adds ability to search monitors by environment (#45792)

Richard Ortenberg 1 год назад
Родитель
Сommit
2df5268e53

+ 1 - 1
src/sentry/api/serializers/base.py

@@ -45,7 +45,7 @@ def serialize(
 
     :param objects: A list of objects
     :param user: The user who will be viewing the objects. Omit to view as `AnonymousUser`.
-    :param serializer: The `Serializer` class who's logic we'll use to serialize
+    :param serializer: The `Serializer` class whose logic we'll use to serialize
         `objects` (see below.) Omit to just look up the Serializer in the
         registry by the `objects`'s type.
     :param kwargs Any

+ 6 - 0
src/sentry/monitors/endpoints/organization_monitor_checkin_index.py

@@ -8,6 +8,7 @@ from rest_framework.request import Request
 from rest_framework.response import Response
 
 from sentry.api.base import region_silo_endpoint
+from sentry.api.helpers.environments import get_environments
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import serialize
 from sentry.api.utils import get_date_range_from_params
@@ -58,6 +59,11 @@ class OrganizationMonitorCheckInIndexEndpoint(MonitorEndpoint):
             monitor_id=monitor.id, date_added__gte=start, date_added__lte=end
         )
 
+        environments = get_environments(request, organization)
+
+        if environments:
+            queryset = queryset.filter(monitor_environment__environment__in=environments)
+
         return self.paginate(
             request=request,
             queryset=queryset,

+ 8 - 2
src/sentry/monitors/endpoints/organization_monitor_details.py

@@ -8,6 +8,7 @@ from rest_framework.response import Response
 from sentry import audit_log
 from sentry.api.base import region_silo_endpoint
 from sentry.api.exceptions import ParameterValidationError
+from sentry.api.helpers.environments import get_environments
 from sentry.api.serializers import serialize
 from sentry.apidocs.constants import (
     RESPONSE_ACCEPTED,
@@ -20,7 +21,7 @@ from sentry.apidocs.parameters import GLOBAL_PARAMS, MONITOR_PARAMS
 from sentry.apidocs.utils import inline_sentry_response_serializer
 from sentry.models import ScheduledDeletion
 from sentry.monitors.models import Monitor, MonitorStatus
-from sentry.monitors.serializers import MonitorSerializerResponse
+from sentry.monitors.serializers import MonitorSerializer, MonitorSerializerResponse
 from sentry.monitors.validators import MonitorValidator
 
 from .base import MonitorEndpoint
@@ -48,7 +49,12 @@ class OrganizationMonitorDetailsEndpoint(MonitorEndpoint):
         """
         Retrieves details for a monitor.
         """
-        return self.respond(serialize(monitor, request.user))
+
+        environments = get_environments(request, organization)
+
+        return self.respond(
+            serialize(monitor, request.user, MonitorSerializer(environments=environments))
+        )
 
     @extend_schema(
         operation_id="Update a monitor",

+ 11 - 2
src/sentry/monitors/endpoints/organization_monitors.py

@@ -9,6 +9,7 @@ from sentry.api.serializers import serialize
 from sentry.db.models.query import in_iexact
 from sentry.models import Organization, Project
 from sentry.monitors.models import Monitor, MonitorStatus, MonitorType
+from sentry.monitors.serializers import MonitorSerializer
 from sentry.monitors.validators import MonitorValidator
 from sentry.search.utils import tokenize_query
 from sentry.signals import first_cron_monitor_created
@@ -69,6 +70,12 @@ class OrganizationMonitorsEndpoint(OrganizationEndpoint):
             )
         )
         query = request.GET.get("query")
+
+        environments = None
+        if "environment" in filter_params:
+            environments = filter_params["environment_objects"]
+            queryset = queryset.filter(monitorenvironment__environment__in=environments)
+
         if query:
             tokens = tokenize_query(query)
             for key, value in tokens.items():
@@ -89,7 +96,7 @@ class OrganizationMonitorsEndpoint(OrganizationEndpoint):
                 elif key == "type":
                     try:
                         queryset = queryset.filter(
-                            status__in=map_value_to_constant(MonitorType, value)
+                            type__in=map_value_to_constant(MonitorType, value)
                         )
                     except ValueError:
                         queryset = queryset.none()
@@ -100,7 +107,9 @@ class OrganizationMonitorsEndpoint(OrganizationEndpoint):
             request=request,
             queryset=queryset,
             order_by=("status_order", "-last_checkin"),
-            on_results=lambda x: serialize(x, request.user),
+            on_results=lambda x: serialize(
+                x, request.user, MonitorSerializer(environments=environments)
+            ),
             paginator_cls=OffsetPaginator,
         )
 

+ 60 - 5
src/sentry/monitors/serializers.py

@@ -1,27 +1,71 @@
+from collections import defaultdict
 from datetime import datetime
 from typing import Any
 
+from django.db.models import prefetch_related_objects
 from typing_extensions import TypedDict
 
 from sentry.api.serializers import ProjectSerializerResponse, Serializer, register, serialize
 from sentry.models import Project
 
-from .models import Monitor, MonitorCheckIn
+from .models import Monitor, MonitorCheckIn, MonitorEnvironment
+
+
+@register(MonitorEnvironment)
+class MonitorEnvironmentSerializer(Serializer):
+    def serialize(self, obj, attrs, user):
+        return {
+            "name": obj.environment.name,
+            "status": obj.get_status_display(),
+            "lastCheckIn": obj.last_checkin,
+            "nextCheckIn": obj.next_checkin,
+            "dateCreated": obj.monitor.date_added,
+        }
+
+
+class MonitorEnvironmentSerializerResponse(TypedDict):
+    name: str
+    status: str
+    dateCreated: datetime
+    lastCheckIn: datetime
+    nextCheckIn: datetime
 
 
 @register(Monitor)
 class MonitorSerializer(Serializer):
-    def get_attrs(self, item_list, user):
+    def __init__(self, environments=None):
+        self.environments = environments
+
+    def get_attrs(self, item_list, user, **kwargs):
         # TODO(dcramer): assert on relations
         projects = {
-            d["id"]: d
-            for d in serialize(
+            p["id"]: p
+            for p in serialize(
                 list(Project.objects.filter(id__in=[i.project_id for i in item_list])), user
             )
         }
 
+        environment_data = {}
+        if self.environments:
+            monitor_environments = defaultdict(list)
+            for monitor_environment in MonitorEnvironment.objects.filter(
+                monitor__in=item_list, environment__in=self.environments
+            ).select_related("environment"):
+                # individually serialize as related objects are prefetched
+                monitor_environments[monitor_environment.monitor_id].append(
+                    serialize(
+                        monitor_environment,
+                        user,
+                    )
+                )
+
+            environment_data = {str(item.id): monitor_environments[item.id] for item in item_list}
+
         return {
-            item: {"project": projects[str(item.project_id)] if item.project_id else None}
+            item: {
+                "project": projects[str(item.project_id)] if item.project_id else None,
+                "environments": environment_data[str(item.id)] if self.environments else None,
+            }
             for item in item_list
         }
 
@@ -40,6 +84,7 @@ class MonitorSerializer(Serializer):
             "nextCheckIn": obj.next_checkin,
             "dateCreated": obj.date_added,
             "project": attrs["project"],
+            "environments": attrs["environments"],
         }
 
 
@@ -54,13 +99,22 @@ class MonitorSerializerResponse(TypedDict):
     lastCheckIn: datetime
     nextCheckIn: datetime
     project: ProjectSerializerResponse
+    environments: MonitorEnvironmentSerializerResponse
 
 
 @register(MonitorCheckIn)
 class MonitorCheckInSerializer(Serializer):
+    def get_attrs(self, item_list, user, **kwargs):
+        # prefetch monitor environment data
+        prefetch_related_objects(item_list, "monitor_environment__environment")
+        return {}
+
     def serialize(self, obj, attrs, user):
         return {
             "id": str(obj.guid),
+            "environment": obj.monitor_environment.environment.name
+            if obj.monitor_environment
+            else None,
             "status": obj.get_status_display(),
             "duration": obj.duration,
             "dateCreated": obj.date_added,
@@ -70,6 +124,7 @@ class MonitorCheckInSerializer(Serializer):
 
 class MonitorCheckInSerializerResponse(TypedDict):
     id: str
+    environment: str
     status: str
     duration: int
     dateCreated: datetime

+ 15 - 1
src/sentry/testutils/cases.py

@@ -95,6 +95,7 @@ from sentry.models import (
     DashboardWidgetQuery,
     DeletedOrganization,
     Deploy,
+    Environment,
     File,
     GroupMeta,
     Identity,
@@ -109,7 +110,7 @@ from sentry.models import (
     UserEmail,
     UserOption,
 )
-from sentry.monitors.models import Monitor, MonitorType, ScheduleType
+from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorType, ScheduleType
 from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
 from sentry.plugins.base import plugins
 from sentry.replays.models import ReplayRecordingSegment
@@ -2309,6 +2310,19 @@ class MonitorTestCase(APITestCase):
             **kwargs,
         )
 
+    def _create_monitor_environment(self, monitor, name="production"):
+        environment = Environment.get_or_create(project=self.project, name=name)
+
+        monitorenvironment_defaults = {
+            "status": monitor.status,
+            "next_checkin": monitor.next_checkin,
+            "last_checkin": monitor.last_checkin,
+        }
+
+        return MonitorEnvironment.objects.create(
+            monitor=monitor, environment=environment, **monitorenvironment_defaults
+        )
+
 
 class MonitorIngestTestCase(MonitorTestCase):
     """

+ 53 - 0
tests/sentry/monitors/endpoints/test_organization_monitor_checkin_index.py

@@ -3,6 +3,7 @@ from datetime import timedelta
 from django.utils import timezone
 from freezegun import freeze_time
 
+from sentry.models import Environment
 from sentry.monitors.models import CheckInStatus, MonitorCheckIn, MonitorStatus
 from sentry.testutils import MonitorTestCase
 from sentry.testutils.silo import region_silo_test
@@ -72,3 +73,55 @@ class ListMonitorCheckInsTest(MonitorTestCase):
             **{"start": startOneDayAgo.isoformat(), "end": end.isoformat()},
         )
         assert resp.data[0]["id"] == str(checkin.guid)
+
+    def test_simple_environment(self):
+        self.login_as(self.user)
+
+        monitor = self._create_monitor()
+        monitor_environment = self._create_monitor_environment(monitor, name="jungle")
+        checkin1 = MonitorCheckIn.objects.create(
+            monitor=monitor,
+            monitor_environment=monitor_environment,
+            project_id=self.project.id,
+            date_added=monitor.date_added - timedelta(minutes=2),
+            status=CheckInStatus.OK,
+        )
+        MonitorCheckIn.objects.create(
+            monitor=monitor,
+            project_id=self.project.id,
+            date_added=monitor.date_added - timedelta(minutes=1),
+            status=CheckInStatus.OK,
+        )
+
+        resp = self.get_success_response(
+            self.organization.slug, monitor.slug, **{"statsPeriod": "1d", "environment": "jungle"}
+        )
+        assert len(resp.data) == 1
+        assert resp.data[0]["id"] == str(checkin1.guid)
+        assert resp.data[0]["environment"] == str(checkin1.monitor_environment.environment.name)
+
+    def test_bad_monitorenvironment(self):
+        self.login_as(self.user)
+
+        monitor = self._create_monitor()
+        monitor_environment = self._create_monitor_environment(monitor, name="jungle")
+        Environment.objects.create(name="volcano", organization_id=self.organization.id)
+        MonitorCheckIn.objects.create(
+            monitor=monitor,
+            monitor_environment=monitor_environment,
+            project_id=self.project.id,
+            date_added=monitor.date_added - timedelta(minutes=2),
+            status=CheckInStatus.OK,
+        )
+        MonitorCheckIn.objects.create(
+            monitor=monitor,
+            monitor_environment=monitor_environment,
+            project_id=self.project.id,
+            date_added=monitor.date_added - timedelta(minutes=1),
+            status=CheckInStatus.OK,
+        )
+
+        resp = self.get_success_response(
+            self.organization.slug, monitor.slug, **{"statsPeriod": "1d", "environment": "volcano"}
+        )
+        assert len(resp.data) == 0

+ 9 - 0
tests/sentry/monitors/endpoints/test_organization_monitor_details.py

@@ -22,6 +22,15 @@ class OrganizationMonitorDetailsTest(MonitorTestCase):
         monitor = self._create_monitor()
         self.get_error_response("asdf", monitor.slug, status_code=404)
 
+    def test_monitor_environment(self):
+        monitor = self._create_monitor()
+        self._create_monitor_environment(monitor)
+
+        self.get_success_response(self.organization.slug, monitor.guid, environment="production")
+        self.get_error_response(
+            self.organization.slug, monitor.guid, environment="jungle", status_code=404
+        )
+
 
 @region_silo_test(stable=True)
 class UpdateMonitorTest(MonitorTestCase):

+ 10 - 0
tests/sentry/monitors/endpoints/test_organization_monitors.py

@@ -58,6 +58,16 @@ class ListOrganizationMonitorsTest(MonitorTestCase):
             ],
         )
 
+    def test_monitor_environment(self):
+        monitor = self._create_monitor()
+        self._create_monitor_environment(monitor)
+
+        monitor_hidden = self._create_monitor(name="hidden")
+        self._create_monitor_environment(monitor_hidden, name="hidden")
+
+        response = self.get_success_response(self.organization.slug, environment="production")
+        self.check_valid_response(response, [monitor])
+
 
 @region_silo_test(stable=True)
 class CreateOrganizationMonitorTest(MonitorTestCase):