Skip to content

Commit f66fde3

Browse files
Dav1ddec298lee
authored andcommitted
feat(metrics): Add abuse quotas for all namespaces and scopes (#68686)
- Renames the existing metric bucket abuse quotas to a match a more general naming schema, which allows all metric abuse quotas to be named consistently. Currently we only have an abuse quota defined for testing purposes on S4S, which is fine to break. - Adds abuse quotas for all (missing) namespaces and for the project scope. - Fully qualifies the namespace in the abuse quota to prevent (accidental) collisions between e.g. `sessions` and `spans`. - Uses `mb` for `metric bucket` to keep the namespace open for a future `metric volume` etc. category.
1 parent 0c58b00 commit f66fde3

File tree

3 files changed

+91
-148
lines changed

3 files changed

+91
-148
lines changed

src/sentry/options/defaults.py

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
FLAG_REQUIRED,
1818
FLAG_SCALAR,
1919
)
20+
from sentry.quotas.base import build_metric_abuse_quotas
2021
from sentry.utils.types import Any, Bool, Dict, Float, Int, Sequence, String
2122

2223
# Cache
@@ -1170,40 +1171,13 @@
11701171
)
11711172

11721173

1173-
register(
1174-
"global-abuse-quota.metric-bucket-limit",
1175-
type=Int,
1176-
default=0,
1177-
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1178-
)
1179-
1180-
register(
1181-
"global-abuse-quota.sessions-metric-bucket-limit",
1182-
type=Int,
1183-
default=0,
1184-
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1185-
)
1186-
1187-
register(
1188-
"global-abuse-quota.transactions-metric-bucket-limit",
1189-
type=Int,
1190-
default=0,
1191-
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1192-
)
1193-
1194-
register(
1195-
"global-abuse-quota.spans-metric-bucket-limit",
1196-
type=Int,
1197-
default=0,
1198-
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1199-
)
1200-
1201-
register(
1202-
"global-abuse-quota.custom-metric-bucket-limit",
1203-
type=Int,
1204-
default=0,
1205-
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1206-
)
1174+
for mabq in build_metric_abuse_quotas():
1175+
register(
1176+
mabq.option,
1177+
type=Int,
1178+
default=0,
1179+
flags=FLAG_PRIORITIZE_DISK | FLAG_AUTOMATOR_MODIFIABLE,
1180+
)
12071181

12081182
# END ABUSE QUOTAS
12091183

src/sentry/quotas/base.py

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from sentry import features, options
1111
from sentry.constants import DataCategory
12+
from sentry.sentry_metrics.use_case_id_registry import USE_CASE_ID_CARDINALITY_LIMIT_QUOTA_OPTIONS
1213
from sentry.utils.json import prune_empty_keys
1314
from sentry.utils.services import Service
1415

@@ -28,6 +29,9 @@ def api_name(self):
2829
return self.name.lower()
2930

3031

32+
AbuseQuotaScope = Literal[QuotaScope.ORGANIZATION, QuotaScope.PROJECT, QuotaScope.GLOBAL]
33+
34+
3135
@dataclass
3236
class AbuseQuota:
3337
# Quota Id.
@@ -37,7 +41,7 @@ class AbuseQuota:
3741
# Quota categories.
3842
categories: list[DataCategory]
3943
# Quota Scope.
40-
scope: Literal[QuotaScope.ORGANIZATION, QuotaScope.PROJECT, QuotaScope.GLOBAL]
44+
scope: AbuseQuotaScope
4145
# The optional namespace that the quota belongs to.
4246
namespace: str | None = None
4347
# Old org option name still used for compatibility reasons,
@@ -48,6 +52,39 @@ class AbuseQuota:
4852
compat_option_sentry: str | None = None
4953

5054

55+
def build_metric_abuse_quotas() -> list[AbuseQuota]:
56+
quotas = list()
57+
58+
scopes: list[tuple[AbuseQuotaScope, str]] = [
59+
(QuotaScope.PROJECT, "p"),
60+
(QuotaScope.ORGANIZATION, "o"),
61+
(QuotaScope.GLOBAL, "g"),
62+
]
63+
64+
for (scope, prefix) in scopes:
65+
quotas.append(
66+
AbuseQuota(
67+
id=f"{prefix}amb",
68+
option=f"metric-abuse-quota.{scope.api_name()}",
69+
categories=[DataCategory.METRIC_BUCKET],
70+
scope=scope,
71+
)
72+
)
73+
74+
for use_case in USE_CASE_ID_CARDINALITY_LIMIT_QUOTA_OPTIONS:
75+
quotas.append(
76+
AbuseQuota(
77+
id=f"{prefix}amb_{use_case.value}",
78+
option=f"metric-abuse-quota.{scope.api_name()}.{use_case.value}",
79+
categories=[DataCategory.METRIC_BUCKET],
80+
scope=scope,
81+
namespace=use_case.value,
82+
)
83+
)
84+
85+
return quotas
86+
87+
5188
class QuotaConfig:
5289
"""
5390
Abstract configuration for a quota.
@@ -413,55 +450,10 @@ def get_abuse_quotas(self, org):
413450
categories=[DataCategory.SESSION],
414451
scope=QuotaScope.PROJECT,
415452
),
416-
AbuseQuota(
417-
id="oam",
418-
option="organization-abuse-quota.metric-bucket-limit",
419-
categories=[DataCategory.METRIC_BUCKET],
420-
scope=QuotaScope.ORGANIZATION,
421-
),
422-
AbuseQuota(
423-
id="oacm",
424-
option="organization-abuse-quota.custom-metric-bucket-limit",
425-
categories=[DataCategory.METRIC_BUCKET],
426-
scope=QuotaScope.ORGANIZATION,
427-
namespace="custom",
428-
),
429-
AbuseQuota(
430-
id="gam",
431-
option="global-abuse-quota.metric-bucket-limit",
432-
categories=[DataCategory.METRIC_BUCKET],
433-
scope=QuotaScope.GLOBAL,
434-
),
435-
AbuseQuota(
436-
id="gams",
437-
option="global-abuse-quota.sessions-metric-bucket-limit",
438-
categories=[DataCategory.METRIC_BUCKET],
439-
scope=QuotaScope.GLOBAL,
440-
namespace="sessions",
441-
),
442-
AbuseQuota(
443-
id="gamt",
444-
option="global-abuse-quota.transactions-metric-bucket-limit",
445-
categories=[DataCategory.METRIC_BUCKET],
446-
scope=QuotaScope.GLOBAL,
447-
namespace="transactions",
448-
),
449-
AbuseQuota(
450-
id="gamp",
451-
option="global-abuse-quota.spans-metric-bucket-limit",
452-
categories=[DataCategory.METRIC_BUCKET],
453-
scope=QuotaScope.GLOBAL,
454-
namespace="spans",
455-
),
456-
AbuseQuota(
457-
id="gamc",
458-
option="global-abuse-quota.custom-metric-bucket-limit",
459-
categories=[DataCategory.METRIC_BUCKET],
460-
scope=QuotaScope.GLOBAL,
461-
namespace="custom",
462-
),
463453
]
464454

455+
abuse_quotas.extend(build_metric_abuse_quotas())
456+
465457
# XXX: These reason codes are hardcoded in getsentry:
466458
# as `RateLimitReasonLabel.PROJECT_ABUSE_LIMIT` and `RateLimitReasonLabel.ORG_ABUSE_LIMIT`.
467459
# Don't change it here. If it's changed in getsentry, it needs to be synced here.

tests/sentry/quotas/test_redis.py

Lines changed: 43 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
import pytest
66

77
from sentry.constants import DataCategory
8-
from sentry.quotas.base import QuotaConfig, QuotaScope
8+
from sentry.quotas.base import QuotaConfig, QuotaScope, build_metric_abuse_quotas
99
from sentry.quotas.redis import RedisQuota, is_rate_limited
10+
from sentry.sentry_metrics.use_case_id_registry import (
11+
USE_CASE_ID_CARDINALITY_LIMIT_QUOTA_OPTIONS,
12+
UseCaseID,
13+
)
1014
from sentry.testutils.cases import TestCase
1115
from sentry.utils.redis import clusters
1216

@@ -118,11 +122,12 @@ def test_abuse_quotas(self):
118122
self.organization.update_option("project-abuse-quota.session-limit", 602)
119123
self.organization.update_option("organization-abuse-quota.metric-bucket-limit", 603)
120124
self.organization.update_option("organization-abuse-quota.custom-metric-bucket-limit", 604)
121-
self.organization.update_option("global-abuse-quota.metric-bucket-limit", 605)
122-
self.organization.update_option("global-abuse-quota.sessions-metric-bucket-limit", 606)
123-
self.organization.update_option("global-abuse-quota.transactions-metric-bucket-limit", 607)
124-
self.organization.update_option("global-abuse-quota.spans-metric-bucket-limit", 608)
125-
self.organization.update_option("global-abuse-quota.custom-metric-bucket-limit", 609)
125+
126+
metric_abuse_limit_by_id = dict()
127+
for i, mabq in enumerate(build_metric_abuse_quotas()):
128+
self.organization.update_option(mabq.option, 700 + i)
129+
metric_abuse_limit_by_id[mabq.id] = 700 + i
130+
126131
with self.feature("organizations:transaction-metrics-extraction"):
127132
quotas = self.quota.get_quotas(self.project)
128133

@@ -150,66 +155,38 @@ def test_abuse_quotas(self):
150155
assert quotas[3].window == 10
151156
assert quotas[3].reason_code == "project_abuse_limit"
152157

153-
assert quotas[4].id == "oam"
154-
assert quotas[4].scope == QuotaScope.ORGANIZATION
155-
assert quotas[4].scope_id is None
156-
assert quotas[4].categories == {DataCategory.METRIC_BUCKET}
157-
assert quotas[4].limit == 6030
158-
assert quotas[4].window == 10
159-
assert quotas[4].reason_code == "org_abuse_limit"
160-
161-
assert quotas[5].id == "oacm"
162-
assert quotas[5].scope == QuotaScope.ORGANIZATION
163-
assert quotas[5].scope_id is None
164-
assert quotas[5].categories == {DataCategory.METRIC_BUCKET}
165-
assert quotas[5].limit == 6040
166-
assert quotas[5].window == 10
167-
assert quotas[5].reason_code == "org_abuse_limit"
168-
169-
assert quotas[6].id == "gam"
170-
assert quotas[6].scope == QuotaScope.GLOBAL
171-
assert quotas[6].scope_id is None
172-
assert quotas[6].categories == {DataCategory.METRIC_BUCKET}
173-
assert quotas[6].limit == 6050
174-
assert quotas[6].window == 10
175-
assert quotas[6].reason_code == "global_abuse_limit"
176-
assert quotas[6].namespace is None
177-
178-
assert quotas[7].id == "gams"
179-
assert quotas[7].scope == QuotaScope.GLOBAL
180-
assert quotas[7].scope_id is None
181-
assert quotas[7].categories == {DataCategory.METRIC_BUCKET}
182-
assert quotas[7].limit == 6060
183-
assert quotas[7].window == 10
184-
assert quotas[7].reason_code == "global_abuse_limit"
185-
assert quotas[7].namespace == "sessions"
186-
187-
assert quotas[8].id == "gamt"
188-
assert quotas[8].scope == QuotaScope.GLOBAL
189-
assert quotas[8].scope_id is None
190-
assert quotas[8].categories == {DataCategory.METRIC_BUCKET}
191-
assert quotas[8].limit == 6070
192-
assert quotas[8].window == 10
193-
assert quotas[8].reason_code == "global_abuse_limit"
194-
assert quotas[8].namespace == "transactions"
195-
196-
assert quotas[9].id == "gamp"
197-
assert quotas[9].scope == QuotaScope.GLOBAL
198-
assert quotas[9].scope_id is None
199-
assert quotas[9].categories == {DataCategory.METRIC_BUCKET}
200-
assert quotas[9].limit == 6080
201-
assert quotas[9].window == 10
202-
assert quotas[9].reason_code == "global_abuse_limit"
203-
assert quotas[9].namespace == "spans"
204-
205-
assert quotas[10].id == "gamc"
206-
assert quotas[10].scope == QuotaScope.GLOBAL
207-
assert quotas[10].scope_id is None
208-
assert quotas[10].categories == {DataCategory.METRIC_BUCKET}
209-
assert quotas[10].limit == 6090
210-
assert quotas[10].window == 10
211-
assert quotas[10].reason_code == "global_abuse_limit"
212-
assert quotas[10].namespace == "custom"
158+
expected_quotas: dict[tuple[QuotaScope, UseCaseID | None], str] = dict()
159+
for scope, prefix in [
160+
(QuotaScope.PROJECT, "p"),
161+
(QuotaScope.ORGANIZATION, "o"),
162+
(QuotaScope.GLOBAL, "g"),
163+
]:
164+
expected_quotas[(scope, None)] = f"{prefix}amb"
165+
for use_case in USE_CASE_ID_CARDINALITY_LIMIT_QUOTA_OPTIONS:
166+
expected_quotas[(scope, use_case)] = f"{prefix}amb_{use_case.value}"
167+
168+
for ((expected_scope, expected_use_case), id) in expected_quotas.items():
169+
quota = next(x for x in quotas if x.id == id)
170+
assert quota is not None
171+
172+
assert quota.id == id
173+
assert quota.scope == expected_scope
174+
assert quota.scope_id is None
175+
assert quota.categories == {DataCategory.METRIC_BUCKET}
176+
assert quota.limit == metric_abuse_limit_by_id[id] * 10
177+
if expected_use_case is None:
178+
assert quota.namespace is None
179+
else:
180+
assert quota.namespace == expected_use_case.value
181+
assert quota.window == 10
182+
if expected_scope == QuotaScope.GLOBAL:
183+
assert quota.reason_code == "global_abuse_limit"
184+
elif expected_scope == QuotaScope.ORGANIZATION:
185+
assert quota.reason_code == "org_abuse_limit"
186+
elif expected_scope == QuotaScope.PROJECT:
187+
assert quota.reason_code == "project_abuse_limit"
188+
else:
189+
assert False, "invalid quota scope"
213190

214191
# Let's set the global option for error limits.
215192
# Since we already have an org override for it, it shouldn't change anything.

0 commit comments

Comments
 (0)