Browse Source

feat(crons): Creates/Updates MonitorEnvironment object during check in (#45192)

Richard Ortenberg 2 years ago
parent
commit
cd5d4348f1

+ 1 - 1
migrations_lockfile.txt

@@ -6,5 +6,5 @@ To resolve this, rebase against latest master and regenerate your migration. Thi
 will then be regenerated, and you should be able to merge without conflicts.
 
 nodestore: 0002_nodestore_no_dictfield
-sentry: 0369_break_useroption_org_fk
+sentry: 0370_integrate_monitor_environment
 social_auth: 0001_initial

+ 33 - 2
src/sentry/api/endpoints/monitor_checkins.py

@@ -24,7 +24,16 @@ from sentry.apidocs.constants import (
 )
 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, Project, ProjectKey
+from sentry.models import (
+    CheckInStatus,
+    Environment,
+    Monitor,
+    MonitorCheckIn,
+    MonitorEnvironment,
+    MonitorStatus,
+    Project,
+    ProjectKey,
+)
 from sentry.signals import first_cron_checkin_received, first_cron_monitor_created
 from sentry.utils import metrics
 
@@ -137,9 +146,26 @@ class MonitorCheckInsEndpoint(MonitorEndpoint):
         result = serializer.validated_data
 
         with transaction.atomic():
+            environment_name = result.get("environment")
+            if not environment_name:
+                environment_name = "production"
+
+            environment = Environment.get_or_create(project=project, name=environment_name)
+
+            monitor_environment, created = MonitorEnvironment.objects.get_or_create(
+                monitor=monitor, environment=environment
+            )
+
+            if created:
+                monitor_environment.status = monitor.status
+                monitor_environment.next_checkin = monitor.next_checkin
+                monitor_environment.last_checkin = monitor.last_checkin
+                monitor_environment.save()
+
             checkin = MonitorCheckIn.objects.create(
                 project_id=project.id,
                 monitor_id=monitor.id,
+                monitor_environment=monitor_environment,
                 duration=result.get("duration"),
                 status=getattr(CheckInStatus, result["status"].upper()),
             )
@@ -155,7 +181,9 @@ class MonitorCheckInsEndpoint(MonitorEndpoint):
                 )
 
             if checkin.status == CheckInStatus.ERROR and monitor.status != MonitorStatus.DISABLED:
-                if not monitor.mark_failed(last_checkin=checkin.date_added):
+                monitor_failed = monitor.mark_failed(last_checkin=checkin.date_added)
+                monitor_environment.mark_failed(last_checkin=checkin.date_added)
+                if not monitor_failed:
                     if isinstance(request.auth, ProjectKey):
                         return self.respond(status=200)
                     return self.respond(serialize(checkin, request.user), status=200)
@@ -169,6 +197,9 @@ class MonitorCheckInsEndpoint(MonitorEndpoint):
                 Monitor.objects.filter(id=monitor.id).exclude(
                     last_checkin__gt=checkin.date_added
                 ).update(**monitor_params)
+                MonitorEnvironment.objects.filter(id=monitor_environment.id).exclude(
+                    last_checkin__gt=checkin.date_added
+                ).update(**monitor_params)
 
         if isinstance(request.auth, ProjectKey):
             return self.respond({"id": str(checkin.guid)}, status=201)

+ 1 - 0
src/sentry/api/validators/monitor.py

@@ -145,3 +145,4 @@ class MonitorCheckInValidator(serializers.Serializer):
         )
     )
     duration = EmptyIntegerField(required=False, allow_null=True)
+    environment = serializers.CharField(required=False, allow_null=True)

+ 1 - 0
src/sentry/deletions/defaults/project.py

@@ -37,6 +37,7 @@ class ProjectDeletionTask(ModelDeletionTask):
             models.GroupShare,
             models.GroupSubscription,
             models.LatestAppConnectBuildsCheck,
+            models.Monitor,
             models.ProjectBookmark,
             models.ProjectKey,
             models.ProjectTeam,

+ 37 - 0
src/sentry/migrations/0370_integrate_monitor_environment.py

@@ -0,0 +1,37 @@
+# Generated by Django 2.2.28 on 2023-03-02 00:44
+
+import django.db.models.deletion
+from django.db import migrations
+
+import sentry.db.models.fields.foreignkey
+from sentry.new_migrations.migrations import CheckedMigration
+
+
+class Migration(CheckedMigration):
+    # This flag is used to mark that a migration shouldn't be automatically run in production. For
+    # the most part, this should only be used for operations where it's safe to run the migration
+    # after your code has deployed. So this should not be used for most operations that alter the
+    # schema of a table.
+    # Here are some things that make sense to mark as dangerous:
+    # - Large data migrations. Typically we want these to be run manually by ops so that they can
+    #   be monitored and not block the deploy for a long period of time while they run.
+    # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
+    #   have ops run this and not block the deploy. Note that while adding an index is a schema
+    #   change, it's completely safe to run the operation after the code has deployed.
+    is_dangerous = False
+
+    dependencies = [
+        ("sentry", "0369_break_useroption_org_fk"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="monitorcheckin",
+            name="monitor_environment",
+            field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                to="sentry.MonitorEnvironment",
+            ),
+        ),
+    ]

+ 1 - 0
src/sentry/models/monitorcheckin.py

@@ -51,6 +51,7 @@ class MonitorCheckIn(Model):
     guid = UUIDField(unique=True, auto_add=True)
     project_id = BoundedBigIntegerField(db_index=True)
     monitor = FlexibleForeignKey("sentry.Monitor")
+    monitor_environment = FlexibleForeignKey("sentry.MonitorEnvironment", null=True)
     location = FlexibleForeignKey("sentry.MonitorLocation", null=True)
     status = BoundedPositiveIntegerField(
         default=CheckInStatus.UNKNOWN, choices=CheckInStatus.as_choices(), db_index=True

+ 29 - 1
src/sentry/models/monitorenvironment.py

@@ -1,4 +1,5 @@
 from django.db import models
+from django.db.models import Q
 from django.utils import timezone
 
 from sentry.db.models import (
@@ -8,7 +9,7 @@ from sentry.db.models import (
     region_silo_only_model,
     sane_repr,
 )
-from sentry.models.monitor import MonitorStatus
+from sentry.models.monitor import MonitorFailure, MonitorStatus
 
 
 @region_silo_only_model
@@ -30,3 +31,30 @@ class MonitorEnvironment(Model):
         indexes = [models.Index(fields=["monitor", "environment"])]
 
     __repr__ = sane_repr("monitor_id", "environment_id")
+
+    def mark_failed(self, last_checkin=None, reason=MonitorFailure.UNKNOWN):
+        if last_checkin is None:
+            next_checkin_base = timezone.now()
+            last_checkin = self.last_checkin or timezone.now()
+        else:
+            next_checkin_base = last_checkin
+
+        new_status = MonitorStatus.ERROR
+        if reason == MonitorFailure.MISSED_CHECKIN:
+            new_status = MonitorStatus.MISSED_CHECKIN
+
+        affected = (
+            type(self)
+            .objects.filter(
+                Q(last_checkin__lte=last_checkin) | Q(last_checkin__isnull=True), id=self.id
+            )
+            .update(
+                next_checkin=self.monitor.get_next_scheduled_checkin(next_checkin_base),
+                status=new_status,
+                last_checkin=last_checkin,
+            )
+        )
+        if not affected:
+            return False
+
+        return True

+ 29 - 1
tests/sentry/api/endpoints/test_monitor_checkins.py

@@ -7,7 +7,14 @@ from django.utils import timezone
 from django.utils.http import urlquote
 from freezegun import freeze_time
 
-from sentry.models import CheckInStatus, Monitor, MonitorCheckIn, MonitorStatus, MonitorType
+from sentry.models import (
+    CheckInStatus,
+    Monitor,
+    MonitorCheckIn,
+    MonitorEnvironment,
+    MonitorStatus,
+    MonitorType,
+)
 from sentry.testutils import MonitorTestCase
 from sentry.testutils.silo import region_silo_test
 
@@ -61,6 +68,13 @@ class CreateMonitorCheckInTest(MonitorTestCase):
             assert monitor.last_checkin == checkin.date_added
             assert monitor.next_checkin == monitor.get_next_scheduled_checkin(checkin.date_added)
 
+            monitor_environment = MonitorEnvironment.objects.get(id=checkin.monitor_environment.id)
+            assert monitor_environment.status == MonitorStatus.OK
+            assert monitor_environment.last_checkin == checkin.date_added
+            assert monitor_environment.next_checkin == monitor.get_next_scheduled_checkin(
+                checkin.date_added
+            )
+
         self.project.refresh_from_db()
         assert self.project.flags.has_cron_checkins
 
@@ -90,6 +104,13 @@ class CreateMonitorCheckInTest(MonitorTestCase):
             assert monitor.last_checkin == checkin.date_added
             assert monitor.next_checkin == monitor.get_next_scheduled_checkin(checkin.date_added)
 
+            monitor_environment = MonitorEnvironment.objects.get(id=checkin.monitor_environment.id)
+            assert monitor_environment.status == MonitorStatus.ERROR
+            assert monitor_environment.last_checkin == checkin.date_added
+            assert monitor_environment.next_checkin == monitor.get_next_scheduled_checkin(
+                checkin.date_added
+            )
+
     def test_disabled(self):
         self.login_as(self.user)
 
@@ -115,6 +136,13 @@ class CreateMonitorCheckInTest(MonitorTestCase):
             assert monitor.last_checkin == checkin.date_added
             assert monitor.next_checkin == monitor.get_next_scheduled_checkin(checkin.date_added)
 
+            monitor_environment = MonitorEnvironment.objects.get(id=checkin.monitor_environment.id)
+            assert monitor_environment.status == MonitorStatus.DISABLED
+            assert monitor_environment.last_checkin == checkin.date_added
+            assert monitor_environment.next_checkin == monitor.get_next_scheduled_checkin(
+                checkin.date_added
+            )
+
     def test_pending_deletion(self):
         self.login_as(self.user)