Skip to content

Commit 9afaa38

Browse files
feat(group-attributes): log a metric when certain fields for Group and 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.
1 parent e35cf9e commit 9afaa38

File tree

5 files changed

+133
-1
lines changed

5 files changed

+133
-1
lines changed

src/sentry/issues/attributes.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import logging
2+
from enum import Enum
3+
from typing import Optional
4+
5+
from django.db.models.signals import post_delete, post_save
6+
from django.dispatch import receiver
7+
8+
from sentry.models import Group, GroupOwner
9+
from sentry.signals import issue_assigned, issue_deleted, issue_unassigned
10+
from sentry.utils import metrics
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class Operation(Enum):
16+
CREATED = "created"
17+
UPDATED = "updated"
18+
DELETED = "deleted"
19+
20+
21+
def _log_group_attributes_changed(
22+
operation: Operation,
23+
model_inducing_snapshot: str,
24+
column_inducing_snapshot: Optional[str] = None,
25+
) -> None:
26+
metrics.incr(
27+
"group_attributes.changed",
28+
tags={
29+
"operation": operation.value,
30+
"model": model_inducing_snapshot,
31+
"column": column_inducing_snapshot,
32+
},
33+
)
34+
35+
36+
@receiver(
37+
post_save, sender=Group, dispatch_uid="post_save_log_group_attributes_changed", weak=False
38+
)
39+
def post_save_log_group_attributes_changed(
40+
instance, sender, created, update_fields, *args, **kwargs
41+
):
42+
try:
43+
if created:
44+
_log_group_attributes_changed(Operation.CREATED, "group", None)
45+
else:
46+
# we have no guarantees update_fields is used everywhere save() is called
47+
# we'll need to assume any of the attributes are updated in that case
48+
attributes_updated = {"status", "substatus", "num_comments"}.intersection(
49+
update_fields or ()
50+
)
51+
if attributes_updated:
52+
_log_group_attributes_changed(
53+
Operation.UPDATED, "group", "-".join(sorted(attributes_updated))
54+
)
55+
except Exception:
56+
logger.error("failed to log group attributes after group post_save", exc_info=True)
57+
58+
59+
@issue_deleted.connect(weak=False)
60+
def on_issue_deleted_log_deleted(group, user, delete_type, **kwargs):
61+
try:
62+
_log_group_attributes_changed(Operation.DELETED, "group", "all")
63+
except Exception:
64+
logger.error("failed to log group attributes after group delete", exc_info=True)
65+
66+
67+
@issue_assigned.connect(weak=False)
68+
def on_issue_assigned_log_group_assignee_attributes_changed(project, group, user, **kwargs):
69+
try:
70+
_log_group_attributes_changed(Operation.UPDATED, "group_assignee", "all")
71+
except Exception:
72+
logger.error(
73+
"failed to log group attributes after group_assignee assignment", exc_info=True
74+
)
75+
76+
77+
@issue_unassigned.connect(weak=False)
78+
def on_issue_unassigned_log_group_assignee_attributes_changed(project, group, user, **kwargs):
79+
try:
80+
_log_group_attributes_changed(Operation.DELETED, "group_assignee", "all")
81+
except Exception:
82+
logger.error(
83+
"failed to log group attributes after group_assignee unassignment", exc_info=True
84+
)
85+
86+
87+
@receiver(
88+
post_save, sender=GroupOwner, dispatch_uid="post_save_log_group_owner_changed", weak=False
89+
)
90+
def post_save_log_group_owner_changed(instance, sender, created, update_fields, *args, **kwargs):
91+
try:
92+
_log_group_attributes_changed(
93+
Operation.CREATED if created else Operation.UPDATED, "group_owner", "all"
94+
)
95+
except Exception:
96+
logger.error("failed to log group attributes after group_owner updated", exc_info=True)
97+
98+
99+
@receiver(
100+
post_delete, sender=GroupOwner, dispatch_uid="post_delete_log_group_owner_changed", weak=False
101+
)
102+
def post_delete_log_group_owner_changed(instance, sender, created, update_fields, *args, **kwargs):
103+
try:
104+
_log_group_attributes_changed(Operation.DELETED, "group_owner", "all")
105+
except Exception:
106+
logger.error("failed to log group attributes after group_owner delete", exc_info=True)

src/sentry/models/activity.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.conf import settings
77
from django.db import models
88
from django.db.models import F
9+
from django.db.models.signals import post_save
910
from django.utils import timezone
1011

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

133134
# HACK: support Group.num_comments
134135
if self.type == ActivityType.NOTE.value:
136+
from sentry.models import Group
137+
135138
self.group.update(num_comments=F("num_comments") + 1)
139+
post_save.send_robust(
140+
sender=Group, instance=self.group, created=True, update_fields=["num_comments"]
141+
)
136142

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

140146
# HACK: support Group.num_comments
141147
if self.type == ActivityType.NOTE.value:
148+
from sentry.models import Group
149+
142150
self.group.update(num_comments=F("num_comments") - 1)
151+
post_save.send_robust(
152+
sender=Group, instance=self.group, created=True, update_fields=["num_comments"]
153+
)
143154

144155
def send_notification(self):
145156
activity.send_activity_notifications.delay(self.id)

src/sentry/models/groupassignee.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
from typing import TYPE_CHECKING, Dict
45

56
from django.conf import settings
@@ -17,14 +18,16 @@
1718
from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
1819
from sentry.models.groupowner import GroupOwner
1920
from sentry.notifications.types import GroupSubscriptionReason
20-
from sentry.signals import issue_assigned
21+
from sentry.signals import issue_assigned, issue_unassigned
2122
from sentry.types.activity import ActivityType
2223
from sentry.utils import metrics
2324

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

29+
logger = logging.getLogger(__name__)
30+
2831

2932
class GroupAssigneeManager(BaseManager):
3033
def assign(
@@ -134,6 +137,10 @@ def deassign(self, group: Group, acting_user: User | RpcUser | None = None) -> N
134137
):
135138
sync_group_assignee_outbound(group, None, assign=False)
136139

140+
issue_unassigned.send_robust(
141+
project=group.project, group=group, user=acting_user, sender=self.__class__
142+
)
143+
137144

138145
@region_silo_only_model
139146
class GroupAssignee(Model):

src/sentry/signals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def send_robust(self, sender, **named) -> List[Tuple[Receiver, Union[Exception,
159159

160160
# issues
161161
issue_assigned = BetterSignal() # ["project", "group", "user"]
162+
issue_unassigned = BetterSignal() # ["project", "group", "user"]
162163
issue_deleted = BetterSignal() # ["group", "user", "delete_type"]
163164
# ["organization_id", "project", "group", "user", "resolution_type"]
164165
issue_resolved = BetterSignal()

src/sentry/tasks/post_process.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import sentry_sdk
99
from django.conf import settings
10+
from django.db.models.signals import post_save
1011
from django.utils import timezone
1112
from google.api_core.exceptions import ServiceUnavailable
1213

@@ -368,6 +369,12 @@ def handle_group_owners(project, group, issue_owners):
368369
)
369370
if new_group_owners:
370371
GroupOwner.objects.bulk_create(new_group_owners)
372+
for go in new_group_owners:
373+
post_save.send_robust(
374+
sender=GroupOwner,
375+
instance=go,
376+
created=True,
377+
)
371378

372379
except UnableToAcquireLock:
373380
pass

0 commit comments

Comments
 (0)