Skip to content

Commit

Permalink
feat(group-attributes): log a metric when certain fields for Group an…
Browse files Browse the repository at this point in the history
…d related tables are updated (#52646)

We're gathering metrics when any of the following postgres DB tables are
mutated:
- Group, GroupAssignee, GroupOwner rows are created or deleted
- Group.status, Group.substatus, Group.first_seen, Group.num_comments
are updated

These metrics will help us gauge how 'active' these columns are for the
purposes of replicating these values to snuba.
  • Loading branch information
barkbarkimashark authored and Michelle Zhang committed Jul 13, 2023
1 parent 10b54bd commit 638a4d5
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 1 deletion.
106 changes: 106 additions & 0 deletions src/sentry/issues/attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import logging
from enum import Enum
from typing import Optional

from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

from sentry.models import Group, GroupOwner
from sentry.signals import issue_assigned, issue_deleted, issue_unassigned
from sentry.utils import metrics

logger = logging.getLogger(__name__)


class Operation(Enum):
CREATED = "created"
UPDATED = "updated"
DELETED = "deleted"


def _log_group_attributes_changed(
operation: Operation,
model_inducing_snapshot: str,
column_inducing_snapshot: Optional[str] = None,
) -> None:
metrics.incr(
"group_attributes.changed",
tags={
"operation": operation.value,
"model": model_inducing_snapshot,
"column": column_inducing_snapshot,
},
)


@receiver(
post_save, sender=Group, dispatch_uid="post_save_log_group_attributes_changed", weak=False
)
def post_save_log_group_attributes_changed(
instance, sender, created, update_fields, *args, **kwargs
):
try:
if created:
_log_group_attributes_changed(Operation.CREATED, "group", None)
else:
# we have no guarantees update_fields is used everywhere save() is called
# we'll need to assume any of the attributes are updated in that case
attributes_updated = {"status", "substatus", "num_comments"}.intersection(
update_fields or ()
)
if attributes_updated:
_log_group_attributes_changed(
Operation.UPDATED, "group", "-".join(sorted(attributes_updated))
)
except Exception:
logger.error("failed to log group attributes after group post_save", exc_info=True)


@issue_deleted.connect(weak=False)
def on_issue_deleted_log_deleted(group, user, delete_type, **kwargs):
try:
_log_group_attributes_changed(Operation.DELETED, "group", "all")
except Exception:
logger.error("failed to log group attributes after group delete", exc_info=True)


@issue_assigned.connect(weak=False)
def on_issue_assigned_log_group_assignee_attributes_changed(project, group, user, **kwargs):
try:
_log_group_attributes_changed(Operation.UPDATED, "group_assignee", "all")
except Exception:
logger.error(
"failed to log group attributes after group_assignee assignment", exc_info=True
)


@issue_unassigned.connect(weak=False)
def on_issue_unassigned_log_group_assignee_attributes_changed(project, group, user, **kwargs):
try:
_log_group_attributes_changed(Operation.DELETED, "group_assignee", "all")
except Exception:
logger.error(
"failed to log group attributes after group_assignee unassignment", exc_info=True
)


@receiver(
post_save, sender=GroupOwner, dispatch_uid="post_save_log_group_owner_changed", weak=False
)
def post_save_log_group_owner_changed(instance, sender, created, update_fields, *args, **kwargs):
try:
_log_group_attributes_changed(
Operation.CREATED if created else Operation.UPDATED, "group_owner", "all"
)
except Exception:
logger.error("failed to log group attributes after group_owner updated", exc_info=True)


@receiver(
post_delete, sender=GroupOwner, dispatch_uid="post_delete_log_group_owner_changed", weak=False
)
def post_delete_log_group_owner_changed(instance, sender, created, update_fields, *args, **kwargs):
try:
_log_group_attributes_changed(Operation.DELETED, "group_owner", "all")
except Exception:
logger.error("failed to log group attributes after group_owner delete", exc_info=True)
11 changes: 11 additions & 0 deletions src/sentry/models/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.conf import settings
from django.db import models
from django.db.models import F
from django.db.models.signals import post_save
from django.utils import timezone

from sentry.db.models import (
Expand Down Expand Up @@ -132,14 +133,24 @@ def save(self, *args, **kwargs):

# HACK: support Group.num_comments
if self.type == ActivityType.NOTE.value:
from sentry.models import Group

self.group.update(num_comments=F("num_comments") + 1)
post_save.send_robust(
sender=Group, instance=self.group, created=True, update_fields=["num_comments"]
)

def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)

# HACK: support Group.num_comments
if self.type == ActivityType.NOTE.value:
from sentry.models import Group

self.group.update(num_comments=F("num_comments") - 1)
post_save.send_robust(
sender=Group, instance=self.group, created=True, update_fields=["num_comments"]
)

def send_notification(self):
activity.send_activity_notifications.delay(self.id)
Expand Down
9 changes: 8 additions & 1 deletion src/sentry/models/groupassignee.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Dict

from django.conf import settings
Expand All @@ -17,14 +18,16 @@
from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
from sentry.models.groupowner import GroupOwner
from sentry.notifications.types import GroupSubscriptionReason
from sentry.signals import issue_assigned
from sentry.signals import issue_assigned, issue_unassigned
from sentry.types.activity import ActivityType
from sentry.utils import metrics

if TYPE_CHECKING:
from sentry.models import ActorTuple, Group, Team, User
from sentry.services.hybrid_cloud.user import RpcUser

logger = logging.getLogger(__name__)


class GroupAssigneeManager(BaseManager):
def assign(
Expand Down Expand Up @@ -134,6 +137,10 @@ def deassign(self, group: Group, acting_user: User | RpcUser | None = None) -> N
):
sync_group_assignee_outbound(group, None, assign=False)

issue_unassigned.send_robust(
project=group.project, group=group, user=acting_user, sender=self.__class__
)


@region_silo_only_model
class GroupAssignee(Model):
Expand Down
1 change: 1 addition & 0 deletions src/sentry/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def send_robust(self, sender, **named) -> List[Tuple[Receiver, Union[Exception,

# issues
issue_assigned = BetterSignal() # ["project", "group", "user"]
issue_unassigned = BetterSignal() # ["project", "group", "user"]
issue_deleted = BetterSignal() # ["group", "user", "delete_type"]
# ["organization_id", "project", "group", "user", "resolution_type"]
issue_resolved = BetterSignal()
Expand Down
7 changes: 7 additions & 0 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import sentry_sdk
from django.conf import settings
from django.db.models.signals import post_save
from django.utils import timezone
from google.api_core.exceptions import ServiceUnavailable

Expand Down Expand Up @@ -368,6 +369,12 @@ def handle_group_owners(project, group, issue_owners):
)
if new_group_owners:
GroupOwner.objects.bulk_create(new_group_owners)
for go in new_group_owners:
post_save.send_robust(
sender=GroupOwner,
instance=go,
created=True,
)

except UnableToAcquireLock:
pass
Expand Down

0 comments on commit 638a4d5

Please sign in to comment.