Skip to content

Commit

Permalink
Add a view deriver to actually time views themselves (#5043)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstufft committed Nov 12, 2018
1 parent 1b8f5f9 commit 8fd1576
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 2 deletions.
11 changes: 10 additions & 1 deletion tests/unit/metrics/test_init.py
Expand Up @@ -12,7 +12,7 @@

import pretend

from pyramid import events
from pyramid import events, viewderivers
from pyramid_retry import IBeforeRetry

from warehouse.metrics import (
Expand All @@ -21,6 +21,7 @@
DataDogMetrics,
IMetricsService,
event_handlers,
views,
)


Expand All @@ -30,6 +31,7 @@ def test_include_defaults_to_null():
maybe_dotted=lambda i: i,
register_service_factory=pretend.call_recorder(lambda factory, iface: None),
add_subscriber=pretend.call_recorder(lambda handler, event: None),
add_view_deriver=pretend.call_recorder(lambda deriver, under: None),
)
includeme(config)

Expand All @@ -44,6 +46,9 @@ def test_include_defaults_to_null():
pretend.call(event_handlers.on_new_response, events.NewResponse),
pretend.call(event_handlers.on_before_retry, IBeforeRetry),
]
assert config.add_view_deriver.calls == [
pretend.call(views.timing_view, under=viewderivers.INGRESS)
]


def test_include_sets_class():
Expand All @@ -56,6 +61,7 @@ def test_include_sets_class():
],
register_service_factory=pretend.call_recorder(lambda factory, iface: None),
add_subscriber=pretend.call_recorder(lambda handler, event: None),
add_view_deriver=pretend.call_recorder(lambda deriver, under: None),
)
includeme(config)

Expand All @@ -70,3 +76,6 @@ def test_include_sets_class():
pretend.call(event_handlers.on_new_response, events.NewResponse),
pretend.call(event_handlers.on_before_retry, IBeforeRetry),
]
assert config.add_view_deriver.calls == [
pretend.call(views.timing_view, under=viewderivers.INGRESS)
]
90 changes: 90 additions & 0 deletions tests/unit/metrics/test_views.py
@@ -0,0 +1,90 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
import pretend

from warehouse.metrics import views


class TestTimingView:
@pytest.mark.parametrize("route", [None, "foo"])
def test_unknown_view(self, pyramid_services, metrics, route):
response = pretend.stub()
view = pretend.call_recorder(lambda request, context: response)
view_info = pretend.stub(original_view=pretend.stub())

derived = views.timing_view(view, view_info)

request = pretend.stub(
matched_route=pretend.stub(name=route) if route else None,
find_service=pyramid_services.find_service,
)
context = pretend.stub()

route_tag = "route:null" if route is None else f"route:{route}"

assert derived(context, request) is response
assert view.calls == [pretend.call(context, request)]
assert metrics.timed.calls == [
pretend.call("pyramid.view.duration", tags=[route_tag, "view:unknown"])
]

@pytest.mark.parametrize("route", [None, "foo"])
def test_qualname_view(self, pyramid_services, metrics, route):
response = pretend.stub()
view = pretend.call_recorder(lambda request, context: response)
view_info = pretend.stub(
original_view=pretend.stub(
__module__="foo", __qualname__="bar", __name__="other"
)
)

derived = views.timing_view(view, view_info)

request = pretend.stub(
matched_route=pretend.stub(name=route) if route else None,
find_service=pyramid_services.find_service,
)
context = pretend.stub()

route_tag = "route:null" if route is None else f"route:{route}"

assert derived(context, request) is response
assert view.calls == [pretend.call(context, request)]
assert metrics.timed.calls == [
pretend.call("pyramid.view.duration", tags=[route_tag, "view:foo.bar"])
]

@pytest.mark.parametrize("route", [None, "foo"])
def test_name_view(self, pyramid_services, metrics, route):
response = pretend.stub()
view = pretend.call_recorder(lambda request, context: response)
view_info = pretend.stub(
original_view=pretend.stub(__module__="foo", __name__="other")
)

derived = views.timing_view(view, view_info)

request = pretend.stub(
matched_route=pretend.stub(name=route) if route else None,
find_service=pyramid_services.find_service,
)
context = pretend.stub()

route_tag = "route:null" if route is None else f"route:{route}"

assert derived(context, request) is response
assert view.calls == [pretend.call(context, request)]
assert metrics.timed.calls == [
pretend.call("pyramid.view.duration", tags=[route_tag, "view:foo.other"])
]
6 changes: 5 additions & 1 deletion warehouse/metrics/__init__.py
Expand Up @@ -10,12 +10,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from pyramid import events
from pyramid import events, viewderivers
from pyramid_retry import IBeforeRetry

from warehouse.metrics import event_handlers
from warehouse.metrics.interfaces import IMetricsService
from warehouse.metrics.services import NullMetrics, DataDogMetrics
from warehouse.metrics.views import timing_view


__all__ = ["IMetricsService", "NullMetrics", "DataDogMetrics", "includeme"]
Expand All @@ -35,3 +36,6 @@ def includeme(config):
config.add_subscriber(event_handlers.on_before_render, events.BeforeRender)
config.add_subscriber(event_handlers.on_new_response, events.NewResponse)
config.add_subscriber(event_handlers.on_before_retry, IBeforeRetry)

# Register our view deriver that ensures we get our view timed.
config.add_view_deriver(timing_view, under=viewderivers.INGRESS)
36 changes: 36 additions & 0 deletions warehouse/metrics/views.py
@@ -0,0 +1,36 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from warehouse.metrics.interfaces import IMetricsService


def timing_view(view, info):
def wrapper_view(context, request):
metrics = request.find_service(IMetricsService, context=None)

route_tag = "route:null"
if request.matched_route:
route_tag = f"route:{request.matched_route.name}"

original_view = info.original_view
if hasattr(original_view, "__qualname__"):
view_name = f"{original_view.__module__}.{original_view.__qualname__}"
elif hasattr(original_view, "__name__"):
view_name = f"{original_view.__module__}.{original_view.__name__}"
else:
view_name = "unknown"
view_tag = f"view:{view_name}"

with metrics.timed("pyramid.view.duration", tags=[route_tag, view_tag]):
return view(context, request)

return wrapper_view

0 comments on commit 8fd1576

Please sign in to comment.