Browse Source

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
Monty Taylor 11 months ago
parent
commit
244780fba8
No account linked to committer's email address

+ 15
- 0
keystoneauth1/loading/session.py View File

@@ -118,12 +118,19 @@ class Session(base.BaseLoader):
118 118
             metavar='<seconds>',
119 119
             help='Set request timeout (in seconds).')
120 120
 
121
+        session_group.add_argument(
122
+            '--collect-timing',
123
+            default=False,
124
+            action='store_true',
125
+            help='Collect per-API call timing information.')
126
+
121 127
     def load_from_argparse_arguments(self, namespace, **kwargs):
122 128
         kwargs.setdefault('insecure', namespace.insecure)
123 129
         kwargs.setdefault('cacert', namespace.os_cacert)
124 130
         kwargs.setdefault('cert', namespace.os_cert)
125 131
         kwargs.setdefault('key', namespace.os_key)
126 132
         kwargs.setdefault('timeout', namespace.timeout)
133
+        kwargs.setdefault('collect_timing', namespace.collect_timing)
127 134
 
128 135
         return self.load_from_options(**kwargs)
129 136
 
@@ -139,6 +146,7 @@ class Session(base.BaseLoader):
139 146
             :keyfile: The key for the client certificate.
140 147
             :insecure: Whether to ignore SSL verification.
141 148
             :timeout: The max time to wait for HTTP connections.
149
+            :collect-timing: Whether to collect API timing information.
142 150
 
143 151
         :param dict deprecated_opts: Deprecated options that should be included
144 152
              in the definition of new options. This should be a dict from the
@@ -175,6 +183,11 @@ class Session(base.BaseLoader):
175 183
                 cfg.IntOpt('timeout',
176 184
                            deprecated_opts=deprecated_opts.get('timeout'),
177 185
                            help='Timeout value for http requests'),
186
+                cfg.BoolOpt('collect-timing',
187
+                            deprecated_opts=deprecated_opts.get(
188
+                                'collect-timing'),
189
+                            default=False,
190
+                            help='Collect per-API call timing information.'),
178 191
                 ]
179 192
 
180 193
     def register_conf_options(self, conf, group, deprecated_opts=None):
@@ -186,6 +199,7 @@ class Session(base.BaseLoader):
186 199
             :keyfile: The key for the client certificate.
187 200
             :insecure: Whether to ignore SSL verification.
188 201
             :timeout: The max time to wait for HTTP connections.
202
+            :collect-timing: Whether to collect API timing information.
189 203
 
190 204
         :param oslo_config.Cfg conf: config object to register with.
191 205
         :param string group: The ini group to register options in.
@@ -227,6 +241,7 @@ class Session(base.BaseLoader):
227 241
         kwargs.setdefault('cert', c.certfile)
228 242
         kwargs.setdefault('key', c.keyfile)
229 243
         kwargs.setdefault('timeout', c.timeout)
244
+        kwargs.setdefault('collect_timing', c.collect_timing)
230 245
 
231 246
         return self.load_from_options(**kwargs)
232 247
 

+ 50
- 1
keystoneauth1/session.py View File

@@ -183,6 +183,24 @@ def _determine_user_agent():
183 183
     return name
184 184
 
185 185
 
186
+class RequestTiming(object):
187
+    """Contains timing information for an HTTP interaction."""
188
+
189
+    #: HTTP method used for the call (GET, POST, etc)
190
+    method = None
191
+
192
+    #: URL against which the call was made
193
+    url = None
194
+
195
+    #: Elapsed time information
196
+    elapsed = None  # type: datetime.timedelta
197
+
198
+    def __init__(self, method, url, elapsed):
199
+        self.method = method
200
+        self.url = url
201
+        self.elapsed = elapsed
202
+
203
+
186 204
 class Session(object):
187 205
     """Maintains client communication state and common functionality.
188 206
 
@@ -244,6 +262,9 @@ class Session(object):
244 262
                                  None which means automatically manage)
245 263
     :param bool split_loggers: Split the logging of requests across multiple
246 264
                                loggers instead of just one. Defaults to False.
265
+    :param bool collect_timing: Whether or not to collect per-method timing
266
+                                information for each API call. (optional,
267
+                                defaults to False)
247 268
     """
248 269
 
249 270
     user_agent = None
@@ -256,7 +277,8 @@ class Session(object):
256 277
                  cert=None, timeout=None, user_agent=None,
257 278
                  redirect=_DEFAULT_REDIRECT_LIMIT, additional_headers=None,
258 279
                  app_name=None, app_version=None, additional_user_agent=None,
259
-                 discovery_cache=None, split_loggers=None):
280
+                 discovery_cache=None, split_loggers=None,
281
+                 collect_timing=False):
260 282
 
261 283
         self.auth = auth
262 284
         self.session = _construct_session(session)
@@ -276,6 +298,8 @@ class Session(object):
276 298
         # NOTE(mordred) split_loggers kwarg default is None rather than False
277 299
         # so we can distinguish between the value being set or not.
278 300
         self._split_loggers = split_loggers
301
+        self._collect_timing = collect_timing
302
+        self._api_times = []
279 303
 
280 304
         if timeout is not None:
281 305
             self.timeout = float(timeout)
@@ -808,6 +832,19 @@ class Session(object):
808 832
                          resp.status_code)
809 833
             raise exceptions.from_response(resp, method, url)
810 834
 
835
+        if self._collect_timing:
836
+            for h in resp.history:
837
+                self._api_times.append(RequestTiming(
838
+                    method=h.request.method,
839
+                    url=h.request.url,
840
+                    elapsed=h.elapsed,
841
+                ))
842
+            self._api_times.append(RequestTiming(
843
+                method=resp.request.method,
844
+                url=resp.request.url,
845
+                elapsed=resp.elapsed,
846
+            ))
847
+
811 848
         return resp
812 849
 
813 850
     def _send_request(self, url, method, redirect, log, logger, split_loggers,
@@ -1172,6 +1209,18 @@ class Session(object):
1172 1209
         auth = self._auth_required(auth, 'get project_id')
1173 1210
         return auth.get_project_id(self)
1174 1211
 
1212
+    def get_timings(self):
1213
+        """Return collected API timing information.
1214
+
1215
+        :returns: List of `RequestTiming` objects.
1216
+        """
1217
+        return self._api_times
1218
+
1219
+    def reset_timings(self):
1220
+        """Clear API timing information."""
1221
+        self._api_times = []
1222
+
1223
+
1175 1224
 REQUESTS_VERSION = tuple(int(v) for v in requests.__version__.split('.'))
1176 1225
 
1177 1226
 

+ 8
- 1
keystoneauth1/tests/unit/loading/test_session.py View File

@@ -70,7 +70,14 @@ class ConfLoadingTests(utils.TestCase):
70 70
         def new_deprecated():
71 71
             return cfg.DeprecatedOpt(uuid.uuid4().hex, group=uuid.uuid4().hex)
72 72
 
73
-        opt_names = ['cafile', 'certfile', 'keyfile', 'insecure', 'timeout']
73
+        opt_names = [
74
+            'cafile',
75
+            'certfile',
76
+            'keyfile',
77
+            'insecure',
78
+            'timeout',
79
+            'collect-timing',
80
+        ]
74 81
         depr = dict([(n, [new_deprecated()]) for n in opt_names])
75 82
         opts = loading.get_session_conf_options(deprecated_opts=depr)
76 83
 

+ 21
- 0
keystoneauth1/tests/unit/test_session.py View File

@@ -10,6 +10,7 @@
10 10
 # License for the specific language governing permissions and limitations
11 11
 # under the License.
12 12
 
13
+import datetime
13 14
 import itertools
14 15
 import json
15 16
 import logging
@@ -1010,6 +1011,26 @@ class SessionAuthTests(utils.TestCase):
1010 1011
                 key=response_key, val=response_val),
1011 1012
             body_output)
1012 1013
 
1014
+    def test_collect_timing(self):
1015
+        auth = AuthPlugin()
1016
+        sess = client_session.Session(auth=auth, collect_timing=True)
1017
+        response = {uuid.uuid4().hex: uuid.uuid4().hex}
1018
+
1019
+        self.stub_url('GET',
1020
+                      json=response,
1021
+                      headers={'Content-Type': 'application/json'})
1022
+
1023
+        resp = sess.get(self.TEST_URL)
1024
+
1025
+        self.assertEqual(response, resp.json())
1026
+        timings = sess.get_timings()
1027
+        self.assertEqual(timings[0].method, 'GET')
1028
+        self.assertEqual(timings[0].url, self.TEST_URL)
1029
+        self.assertIsInstance(timings[0].elapsed, datetime.timedelta)
1030
+        sess.reset_timings()
1031
+        timings = sess.get_timings()
1032
+        self.assertEqual(len(timings), 0)
1033
+
1013 1034
 
1014 1035
 class AdapterTest(utils.TestCase):
1015 1036
 

+ 8
- 0
releasenotes/notes/collect-timing-85f007f0d86c8b26.yaml View File

@@ -0,0 +1,8 @@
1
+---
2
+features:
3
+  - |
4
+    Added ``collect_timing`` option to ``keystoneauth1.session.Session``.
5
+    The option, which is off by default, causes the ``Session`` to collect
6
+    API timing information for every call it makes. Methods ``get_timings``
7
+    and ``reset_timings`` have been added to allow getting and clearing the
8
+    data.

Loading…
Cancel
Save