Collect timing information for API calls
python-openstackclient does this in a wrapper class around Session, and openstacksdk does something similar that could be removed if support were directly in keystoneauth. Add this so that we can remove the custom wrapper/manipulation in openstackclient and openstacksdk. Change-Id: Icf00c66f57d20d2cef724c233160d3b1e0d52102
This commit is contained in:
parent
0bebdaf0f9
commit
244780fba8
@ -118,12 +118,19 @@ class Session(base.BaseLoader):
|
|||||||
metavar='<seconds>',
|
metavar='<seconds>',
|
||||||
help='Set request timeout (in seconds).')
|
help='Set request timeout (in seconds).')
|
||||||
|
|
||||||
|
session_group.add_argument(
|
||||||
|
'--collect-timing',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Collect per-API call timing information.')
|
||||||
|
|
||||||
def load_from_argparse_arguments(self, namespace, **kwargs):
|
def load_from_argparse_arguments(self, namespace, **kwargs):
|
||||||
kwargs.setdefault('insecure', namespace.insecure)
|
kwargs.setdefault('insecure', namespace.insecure)
|
||||||
kwargs.setdefault('cacert', namespace.os_cacert)
|
kwargs.setdefault('cacert', namespace.os_cacert)
|
||||||
kwargs.setdefault('cert', namespace.os_cert)
|
kwargs.setdefault('cert', namespace.os_cert)
|
||||||
kwargs.setdefault('key', namespace.os_key)
|
kwargs.setdefault('key', namespace.os_key)
|
||||||
kwargs.setdefault('timeout', namespace.timeout)
|
kwargs.setdefault('timeout', namespace.timeout)
|
||||||
|
kwargs.setdefault('collect_timing', namespace.collect_timing)
|
||||||
|
|
||||||
return self.load_from_options(**kwargs)
|
return self.load_from_options(**kwargs)
|
||||||
|
|
||||||
@ -139,6 +146,7 @@ class Session(base.BaseLoader):
|
|||||||
:keyfile: The key for the client certificate.
|
:keyfile: The key for the client certificate.
|
||||||
:insecure: Whether to ignore SSL verification.
|
:insecure: Whether to ignore SSL verification.
|
||||||
:timeout: The max time to wait for HTTP connections.
|
:timeout: The max time to wait for HTTP connections.
|
||||||
|
:collect-timing: Whether to collect API timing information.
|
||||||
|
|
||||||
:param dict deprecated_opts: Deprecated options that should be included
|
:param dict deprecated_opts: Deprecated options that should be included
|
||||||
in the definition of new options. This should be a dict from the
|
in the definition of new options. This should be a dict from the
|
||||||
@ -175,6 +183,11 @@ class Session(base.BaseLoader):
|
|||||||
cfg.IntOpt('timeout',
|
cfg.IntOpt('timeout',
|
||||||
deprecated_opts=deprecated_opts.get('timeout'),
|
deprecated_opts=deprecated_opts.get('timeout'),
|
||||||
help='Timeout value for http requests'),
|
help='Timeout value for http requests'),
|
||||||
|
cfg.BoolOpt('collect-timing',
|
||||||
|
deprecated_opts=deprecated_opts.get(
|
||||||
|
'collect-timing'),
|
||||||
|
default=False,
|
||||||
|
help='Collect per-API call timing information.'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def register_conf_options(self, conf, group, deprecated_opts=None):
|
def register_conf_options(self, conf, group, deprecated_opts=None):
|
||||||
@ -186,6 +199,7 @@ class Session(base.BaseLoader):
|
|||||||
:keyfile: The key for the client certificate.
|
:keyfile: The key for the client certificate.
|
||||||
:insecure: Whether to ignore SSL verification.
|
:insecure: Whether to ignore SSL verification.
|
||||||
:timeout: The max time to wait for HTTP connections.
|
:timeout: The max time to wait for HTTP connections.
|
||||||
|
:collect-timing: Whether to collect API timing information.
|
||||||
|
|
||||||
:param oslo_config.Cfg conf: config object to register with.
|
:param oslo_config.Cfg conf: config object to register with.
|
||||||
:param string group: The ini group to register options in.
|
:param string group: The ini group to register options in.
|
||||||
@ -227,6 +241,7 @@ class Session(base.BaseLoader):
|
|||||||
kwargs.setdefault('cert', c.certfile)
|
kwargs.setdefault('cert', c.certfile)
|
||||||
kwargs.setdefault('key', c.keyfile)
|
kwargs.setdefault('key', c.keyfile)
|
||||||
kwargs.setdefault('timeout', c.timeout)
|
kwargs.setdefault('timeout', c.timeout)
|
||||||
|
kwargs.setdefault('collect_timing', c.collect_timing)
|
||||||
|
|
||||||
return self.load_from_options(**kwargs)
|
return self.load_from_options(**kwargs)
|
||||||
|
|
||||||
|
@ -183,6 +183,24 @@ def _determine_user_agent():
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTiming(object):
|
||||||
|
"""Contains timing information for an HTTP interaction."""
|
||||||
|
|
||||||
|
#: HTTP method used for the call (GET, POST, etc)
|
||||||
|
method = None
|
||||||
|
|
||||||
|
#: URL against which the call was made
|
||||||
|
url = None
|
||||||
|
|
||||||
|
#: Elapsed time information
|
||||||
|
elapsed = None # type: datetime.timedelta
|
||||||
|
|
||||||
|
def __init__(self, method, url, elapsed):
|
||||||
|
self.method = method
|
||||||
|
self.url = url
|
||||||
|
self.elapsed = elapsed
|
||||||
|
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
"""Maintains client communication state and common functionality.
|
"""Maintains client communication state and common functionality.
|
||||||
|
|
||||||
@ -244,6 +262,9 @@ class Session(object):
|
|||||||
None which means automatically manage)
|
None which means automatically manage)
|
||||||
:param bool split_loggers: Split the logging of requests across multiple
|
:param bool split_loggers: Split the logging of requests across multiple
|
||||||
loggers instead of just one. Defaults to False.
|
loggers instead of just one. Defaults to False.
|
||||||
|
:param bool collect_timing: Whether or not to collect per-method timing
|
||||||
|
information for each API call. (optional,
|
||||||
|
defaults to False)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_agent = None
|
user_agent = None
|
||||||
@ -256,7 +277,8 @@ class Session(object):
|
|||||||
cert=None, timeout=None, user_agent=None,
|
cert=None, timeout=None, user_agent=None,
|
||||||
redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
|
redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
|
||||||
app_name=None, app_version=None, additional_user_agent=None,
|
app_name=None, app_version=None, additional_user_agent=None,
|
||||||
discovery_cache=None, split_loggers=None):
|
discovery_cache=None, split_loggers=None,
|
||||||
|
collect_timing=False):
|
||||||
|
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.session = _construct_session(session)
|
self.session = _construct_session(session)
|
||||||
@ -276,6 +298,8 @@ class Session(object):
|
|||||||
# NOTE(mordred) split_loggers kwarg default is None rather than False
|
# NOTE(mordred) split_loggers kwarg default is None rather than False
|
||||||
# so we can distinguish between the value being set or not.
|
# so we can distinguish between the value being set or not.
|
||||||
self._split_loggers = split_loggers
|
self._split_loggers = split_loggers
|
||||||
|
self._collect_timing = collect_timing
|
||||||
|
self._api_times = []
|
||||||
|
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
self.timeout = float(timeout)
|
self.timeout = float(timeout)
|
||||||
@ -808,6 +832,19 @@ class Session(object):
|
|||||||
resp.status_code)
|
resp.status_code)
|
||||||
raise exceptions.from_response(resp, method, url)
|
raise exceptions.from_response(resp, method, url)
|
||||||
|
|
||||||
|
if self._collect_timing:
|
||||||
|
for h in resp.history:
|
||||||
|
self._api_times.append(RequestTiming(
|
||||||
|
method=h.request.method,
|
||||||
|
url=h.request.url,
|
||||||
|
elapsed=h.elapsed,
|
||||||
|
))
|
||||||
|
self._api_times.append(RequestTiming(
|
||||||
|
method=resp.request.method,
|
||||||
|
url=resp.request.url,
|
||||||
|
elapsed=resp.elapsed,
|
||||||
|
))
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def _send_request(self, url, method, redirect, log, logger, split_loggers,
|
def _send_request(self, url, method, redirect, log, logger, split_loggers,
|
||||||
@ -1172,6 +1209,18 @@ class Session(object):
|
|||||||
auth = self._auth_required(auth, 'get project_id')
|
auth = self._auth_required(auth, 'get project_id')
|
||||||
return auth.get_project_id(self)
|
return auth.get_project_id(self)
|
||||||
|
|
||||||
|
def get_timings(self):
|
||||||
|
"""Return collected API timing information.
|
||||||
|
|
||||||
|
:returns: List of `RequestTiming` objects.
|
||||||
|
"""
|
||||||
|
return self._api_times
|
||||||
|
|
||||||
|
def reset_timings(self):
|
||||||
|
"""Clear API timing information."""
|
||||||
|
self._api_times = []
|
||||||
|
|
||||||
|
|
||||||
REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.'))
|
REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.'))
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +70,14 @@ class ConfLoadingTests(utils.TestCase):
|
|||||||
def new_deprecated():
|
def new_deprecated():
|
||||||
return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex)
|
return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex)
|
||||||
|
|
||||||
opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout']
|
opt_names = [
|
||||||
|
'cafile',
|
||||||
|
'certfile',
|
||||||
|
'keyfile',
|
||||||
|
'insecure',
|
||||||
|
'timeout',
|
||||||
|
'collect-timing',
|
||||||
|
]
|
||||||
depr = dict([(n, [new_deprecated()]) for n in opt_names])
|
depr = dict([(n, [new_deprecated()]) for n in opt_names])
|
||||||
opts = loading.get_session_conf_options(deprecated_opts=depr)
|
opts = loading.get_session_conf_options(deprecated_opts=depr)
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -1010,6 +1011,26 @@ class SessionAuthTests(utils.TestCase):
|
|||||||
key=response_key, val=response_val),
|
key=response_key, val=response_val),
|
||||||
body_output)
|
body_output)
|
||||||
|
|
||||||
|
def test_collect_timing(self):
|
||||||
|
auth = AuthPlugin()
|
||||||
|
sess = client_session.Session(auth=auth, collect_timing=True)
|
||||||
|
response = {uuid.uuid4().hex: uuid.uuid4().hex}
|
||||||
|
|
||||||
|
self.stub_url('GET',
|
||||||
|
json=response,
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
resp = sess.get(self.TEST_URL)
|
||||||
|
|
||||||
|
self.assertEqual(response, resp.json())
|
||||||
|
timings = sess.get_timings()
|
||||||
|
self.assertEqual(timings[0].method, 'GET')
|
||||||
|
self.assertEqual(timings[0].url, self.TEST_URL)
|
||||||
|
self.assertIsInstance(timings[0].elapsed, datetime.timedelta)
|
||||||
|
sess.reset_timings()
|
||||||
|
timings = sess.get_timings()
|
||||||
|
self.assertEqual(len(timings), 0)
|
||||||
|
|
||||||
|
|
||||||
class AdapterTest(utils.TestCase):
|
class AdapterTest(utils.TestCase):
|
||||||
|
|
||||||
|
8
releasenotes/notes/collect-timing-85f007f0d86c8b26.yaml
Normal file
8
releasenotes/notes/collect-timing-85f007f0d86c8b26.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added ``collect_timing`` option to ``keystoneauth1.session.Session``.
|
||||||
|
The option, which is off by default, causes the ``Session`` to collect
|
||||||
|
API timing information for every call it makes. Methods ``get_timings``
|
||||||
|
and ``reset_timings`` have been added to allow getting and clearing the
|
||||||
|
data.
|
Loading…
Reference in New Issue
Block a user