Skip to content

Commit fc1f937

Browse files
committed
feat(api): per-endpoint metric tags
This allows endpoints to add tags to the resulting `view.response` metric that gets recorded in middleware. The immediate use for this is to tag all Integration Platform endpoints with a corresponding tag so that we can split them out downstream.
1 parent 5eb4bfd commit fc1f937

File tree

8 files changed

+110
-10
lines changed

8 files changed

+110
-10
lines changed

src/sentry/api/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ def load_json_body(self, request):
127127
except JSONDecodeError:
128128
return
129129

130+
def add_metric_tags(self, *args, **kwargs):
131+
if not hasattr(self.request, '_metric_tags'):
132+
self.request._metric_tags = {}
133+
134+
self.request._metric_tags.update(**kwargs)
135+
130136
def initialize_request(self, request, *args, **kwargs):
131137
rv = super(Endpoint, self).initialize_request(request, *args, **kwargs)
132138
# If our request is being made via our internal API client, we need to
@@ -151,6 +157,10 @@ def dispatch(self, request, *args, **kwargs):
151157
self.request = request
152158
self.headers = self.default_response_headers # deprecate?
153159

160+
# Tags that will ultimately flow into the metrics backend at the end of
161+
# the request (happens via middleware/stats.py).
162+
request._metric_tags = {}
163+
154164
if settings.SENTRY_API_RESPONSE_DELAY:
155165
time.sleep(settings.SENTRY_API_RESPONSE_DELAY / 1000.0)
156166

src/sentry/api/bases/sentryapps.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ def ensure_scoped_permission(request, allowed_scopes):
3333
return any(request.access.has_scope(s) for s in set(allowed_scopes))
3434

3535

36+
def add_integration_platform_metric_tag(func):
37+
def decorator(self, *args, **kwargs):
38+
self.add_metric_tags(integration_platform=True)
39+
return func(self, *args, **kwargs)
40+
return decorator
41+
42+
3643
class SentryAppsPermission(SentryPermission):
3744
scope_map = {
3845
'GET': (), # Public endpoint.
@@ -58,7 +65,13 @@ def has_object_permission(self, request, view, organization):
5865
)
5966

6067

61-
class SentryAppsBaseEndpoint(Endpoint):
68+
class IntegrationPlatformEndpoint(Endpoint):
69+
def dispatch(self, request, *args, **kwargs):
70+
self.add_metric_tags(integration_platform=True)
71+
return super(IntegrationPlatformEndpoint, self).dispatch(request, *args, **kwargs)
72+
73+
74+
class SentryAppsBaseEndpoint(IntegrationPlatformEndpoint):
6275
permission_classes = (SentryAppsPermission, )
6376

6477
def convert_args(self, request, *args, **kwargs):
@@ -131,7 +144,7 @@ def _scopes_for_sentry_app(self, sentry_app):
131144
return self.unpublished_scope_map
132145

133146

134-
class SentryAppBaseEndpoint(Endpoint):
147+
class SentryAppBaseEndpoint(IntegrationPlatformEndpoint):
135148
permission_classes = (SentryAppPermission, )
136149

137150
def convert_args(self, request, sentry_app_slug, *args, **kwargs):
@@ -175,7 +188,7 @@ def has_object_permission(self, request, view, organization):
175188
)
176189

177190

178-
class SentryAppInstallationsBaseEndpoint(Endpoint):
191+
class SentryAppInstallationsBaseEndpoint(IntegrationPlatformEndpoint):
179192
permission_classes = (SentryAppInstallationsPermission, )
180193

181194
def convert_args(self, request, organization_slug, *args, **kwargs):
@@ -226,7 +239,7 @@ def has_object_permission(self, request, view, installation):
226239
)
227240

228241

229-
class SentryAppInstallationBaseEndpoint(Endpoint):
242+
class SentryAppInstallationBaseEndpoint(IntegrationPlatformEndpoint):
230243
permission_classes = (SentryAppInstallationPermission, )
231244

232245
def convert_args(self, request, uuid, *args, **kwargs):

src/sentry/api/endpoints/organization_sentry_apps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import absolute_import
22

3-
from sentry.api.bases import OrganizationEndpoint
3+
from sentry.api.bases import OrganizationEndpoint, add_integration_platform_metric_tag
44
from sentry.api.paginator import OffsetPaginator
55
from sentry.api.serializers import serialize
66
from sentry.models import SentryApp
77

88

99
class OrganizationSentryAppsEndpoint(OrganizationEndpoint):
10+
@add_integration_platform_metric_tag
1011
def get(self, request, organization):
1112
queryset = SentryApp.objects.filter(
1213
owner=organization,

src/sentry/api/endpoints/sentry_app_components.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from rest_framework.response import Response
44

5-
from sentry.api.bases import OrganizationEndpoint, SentryAppBaseEndpoint
5+
from sentry.api.bases import (
6+
OrganizationEndpoint, SentryAppBaseEndpoint, add_integration_platform_metric_tag,
7+
)
68
from sentry.api.paginator import OffsetPaginator
79
from sentry.api.serializers import serialize
810
from sentry.coreapi import APIError
@@ -21,6 +23,7 @@ def get(self, request, sentry_app):
2123

2224

2325
class OrganizationSentryAppComponentsEndpoint(OrganizationEndpoint):
26+
@add_integration_platform_metric_tag
2427
def get(self, request, organization):
2528
try:
2629
project = Project.objects.get(

src/sentry/middleware/stats.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class RequestTimingMiddleware(object):
2727
)
2828

2929
def process_view(self, request, view_func, view_args, view_kwargs):
30+
request._metric_tags = {}
31+
3032
if request.method not in self.allowed_methods:
3133
return
3234

@@ -56,13 +58,16 @@ def _record_time(self, request, status_code):
5658
if not hasattr(request, '_view_path'):
5759
return
5860

61+
tags = request._metric_tags if hasattr(request, '_metric_tags') else {}
62+
tags.update({
63+
'method': request.method,
64+
'status_code': status_code,
65+
})
66+
5967
metrics.incr(
6068
'view.response',
6169
instance=request._view_path,
62-
tags={
63-
'method': request.method,
64-
'status_code': status_code,
65-
},
70+
tags=tags,
6671
skip_internal=False,
6772
)
6873

src/sentry/testutils/helpers/faux.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ def _kwargs_to_s(self, **kwargs):
174174
return ', '.join(u'{}={!r}'.format(k, v) for k, v in six.iteritems(kwargs))
175175

176176

177+
class Mock(object):
178+
def __init__(self, *args, **kwargs):
179+
for k, v in six.iteritems(kwargs):
180+
setattr(self, k, v)
181+
182+
177183
def faux(mock, call=None):
178184
if call is not None:
179185
return Faux(mock.mock_calls[call])

tests/sentry/api/test_base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ def test_basic_cors(self):
4141

4242
assert response['Access-Control-Allow-Origin'] == 'http://example.com'
4343

44+
def test_add_metric_tags(self):
45+
request = HttpRequest()
46+
endpoint = DummyEndpoint()
47+
endpoint.request = request
48+
49+
endpoint.add_metric_tags(foo='bar')
50+
51+
assert endpoint.request._metric_tags['foo'] == 'bar'
52+
4453

4554
class EndpointJSONBodyTest(APITestCase):
4655
def setUp(self):

tests/sentry/middleware/test_stats.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from __future__ import absolute_import
2+
3+
from django.test import RequestFactory
4+
from exam import fixture
5+
from mock import patch
6+
7+
from sentry.middleware.stats import RequestTimingMiddleware
8+
from sentry.testutils import TestCase
9+
from sentry.testutils.helpers.faux import Mock
10+
11+
12+
class RequestTimingMiddlewareTest(TestCase):
13+
middleware = fixture(RequestTimingMiddleware)
14+
factory = fixture(RequestFactory)
15+
16+
@patch('sentry.utils.metrics.incr')
17+
def test_records_default_api_metrics(self, incr):
18+
request = self.factory.get('/')
19+
request._view_path = '/'
20+
response = Mock(status_code=200)
21+
22+
self.middleware.process_response(request, response)
23+
24+
incr.assert_called_with(
25+
'view.response',
26+
instance=request._view_path,
27+
tags={
28+
'method': 'GET',
29+
'status_code': 200,
30+
},
31+
skip_internal=False,
32+
)
33+
34+
@patch('sentry.utils.metrics.incr')
35+
def test_records_endpoint_specific_metrics(self, incr):
36+
request = self.factory.get('/')
37+
request._view_path = '/'
38+
request._metric_tags = {'a': 'b'}
39+
40+
response = Mock(status_code=200)
41+
42+
self.middleware.process_response(request, response)
43+
44+
incr.assert_called_with(
45+
'view.response',
46+
instance=request._view_path,
47+
tags={
48+
'method': 'GET',
49+
'status_code': 200,
50+
'a': 'b',
51+
},
52+
skip_internal=False,
53+
)

0 commit comments

Comments
 (0)