Log request_id for each api call

Add the function to log 'x-openstack-request-id' or
'x-compute-request-id' in each API call.
If the caller (e.g. heat) uses oslo.log,
the caller's request id in oslo.context and
the callee's request id can be output in the same
log message (same line).

Change-Id: I29312ce278ecfae41a688a0ddf76c24cfa0eaf6b
Implements: blueprint log-request-id
This commit is contained in:
Takashi NATSUME 2016-05-30 14:54:25 +09:00
parent e8354f1098
commit d16c3692e6
6 changed files with 86 additions and 18 deletions

View File

@ -73,6 +73,18 @@ class _ClientConnectionPool(object):
return self._adapters[url] return self._adapters[url]
def _log_request_id(logger, resp, service_name):
request_id = (resp.headers.get('x-openstack-request-id') or
resp.headers.get('x-compute-request-id'))
if request_id:
logger.debug('%(method)s call to %(service_name)s for %(url)s '
'used request id %(response_request_id)s',
{'method': resp.request.method,
'service_name': service_name,
'url': resp.url,
'response_request_id': request_id})
class SessionClient(adapter.LegacyJsonAdapter): class SessionClient(adapter.LegacyJsonAdapter):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -93,6 +105,11 @@ class SessionClient(adapter.LegacyJsonAdapter):
method, method,
raise_exc=False, raise_exc=False,
**kwargs) **kwargs)
# if service name is None then use service_type for logging
service = self.service_name or self.service_type
_log_request_id(self.logger, resp, service)
# TODO(andreykurilin): uncomment this line, when we will be able to # TODO(andreykurilin): uncomment this line, when we will be able to
# check only nova-related calls # check only nova-related calls
# api_versions.check_headers(resp, self.api_version) # api_versions.check_headers(resp, self.api_version)
@ -142,7 +159,8 @@ class HTTPClient(object):
http_log_debug=False, auth_system='keystone', http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_token=None, auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None, cacert=None, tenant_id=None, user_id=None,
connection_pool=False, api_version=None): connection_pool=False, api_version=None,
logger=None):
self.user = user self.user = user
self.user_id = user_id self.user_id = user_id
self.password = password self.password = password
@ -203,9 +221,10 @@ class HTTPClient(object):
self.auth_plugin = auth_plugin self.auth_plugin = auth_plugin
self._session = None self._session = None
self._current_url = None self._current_url = None
self._logger = logging.getLogger(__name__) self._logger = logger or logging.getLogger(__name__)
if self.http_log_debug and not self._logger.handlers: if (self.http_log_debug and logger is None and
not self._logger.handlers):
# Logging level is already set on the root logger # Logging level is already set on the root logger
ch = logging.StreamHandler() ch = logging.StreamHandler()
self._logger.addHandler(ch) self._logger.addHandler(ch)
@ -320,6 +339,10 @@ class HTTPClient(object):
'headers': resp.headers, 'headers': resp.headers,
'text': json.dumps(body)}) 'text': json.dumps(body)})
# if service name is None then use service_type for logging
service = self.service_name or self.service_type
_log_request_id(self._logger, resp, service)
def open_session(self): def open_session(self):
if not self._connection_pool: if not self._connection_pool:
self._session = requests.Session() self._session = requests.Session()
@ -706,6 +729,7 @@ def _construct_http_client(username=None, password=None, project_id=None,
else: else:
# FIXME(jamielennox): username and password are now optional. Need # FIXME(jamielennox): username and password are now optional. Need
# to test that they were provided in this mode. # to test that they were provided in this mode.
logger = kwargs.get('logger')
return HTTPClient(username, return HTTPClient(username,
password, password,
user_id=user_id, user_id=user_id,
@ -730,7 +754,8 @@ def _construct_http_client(username=None, password=None, project_id=None,
http_log_debug=http_log_debug, http_log_debug=http_log_debug,
cacert=cacert, cacert=cacert,
connection_pool=connection_pool, connection_pool=connection_pool,
api_version=api_version) api_version=api_version,
logger=logger)
def discover_extensions(version, only_contrib=False): def discover_extensions(version, only_contrib=False):

View File

@ -380,6 +380,9 @@ class NovaClientArgumentParser(argparse.ArgumentParser):
class OpenStackComputeShell(object): class OpenStackComputeShell(object):
times = [] times = []
def __init__(self):
self.client_logger = None
def _append_global_identity_args(self, parser, argv): def _append_global_identity_args(self, parser, argv):
# Register the CLI arguments that have moved to the session object. # Register the CLI arguments that have moved to the session object.
loading.register_session_argparse_arguments(parser) loading.register_session_argparse_arguments(parser)
@ -698,6 +701,11 @@ class OpenStackComputeShell(object):
format=streamformat) format=streamformat)
logging.getLogger('iso8601').setLevel(logging.WARNING) logging.getLogger('iso8601').setLevel(logging.WARNING)
self.client_logger = logging.getLogger(client.__name__)
ch = logging.StreamHandler()
self.client_logger.setLevel(logging.DEBUG)
self.client_logger.addHandler(ch)
def main(self, argv): def main(self, argv):
# Parse args once to find version and debug settings # Parse args once to find version and debug settings
parser = self.get_base_parser(argv) parser = self.get_base_parser(argv)
@ -892,7 +900,8 @@ class OpenStackComputeShell(object):
timings=args.timings, bypass_url=bypass_url, timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=args.debug, os_cache=os_cache, http_log_debug=args.debug,
cacert=cacert, timeout=timeout, cacert=cacert, timeout=timeout,
session=keystone_session, auth=keystone_auth) session=keystone_session, auth=keystone_auth,
logger=self.client_logger)
if not skip_auth: if not skip_auth:
if not api_version.is_latest(): if not api_version.is_latest():

View File

@ -443,7 +443,8 @@ class ClientTest(utils.TestCase):
class SessionClientTest(utils.TestCase): class SessionClientTest(utils.TestCase):
@mock.patch.object(adapter.LegacyJsonAdapter, 'request') @mock.patch.object(adapter.LegacyJsonAdapter, 'request')
def test_timings(self, m_request): @mock.patch.object(novaclient.client, '_log_request_id')
def test_timings(self, mock_log_request_id, m_request):
m_request.return_value = (mock.MagicMock(status_code=200), None) m_request.return_value = (mock.MagicMock(status_code=200), None)
client = novaclient.client.SessionClient(session=mock.MagicMock()) client = novaclient.client.SessionClient(session=mock.MagicMock())
@ -456,6 +457,17 @@ class SessionClientTest(utils.TestCase):
self.assertEqual(1, len(client.times)) self.assertEqual(1, len(client.times))
self.assertEqual('GET http://no.where', client.times[0][0]) self.assertEqual('GET http://no.where', client.times[0][0])
@mock.patch.object(adapter.LegacyJsonAdapter, 'request')
@mock.patch.object(novaclient.client, '_log_request_id')
def test_log_request_id(self, mock_log_request_id, mock_request):
response = mock.MagicMock(status_code=200)
mock_request.return_value = (response, None)
client = novaclient.client.SessionClient(session=mock.MagicMock(),
service_name='compute')
client.request("http://no.where", 'GET')
mock_log_request_id.assert_called_once_with(client.logger, response,
'compute')
class DiscoverExtensionTest(utils.TestCase): class DiscoverExtensionTest(utils.TestCase):

View File

@ -71,15 +71,16 @@ retry_after_non_supporting_mock_request = mock.Mock(
return_value=retry_after_non_supporting_response) return_value=retry_after_non_supporting_response)
def get_client(): def get_client(**kwargs):
cl = client.HTTPClient("username", "password", cl = client.HTTPClient("username", "password",
"project_id", "project_id",
utils.AUTH_URL_V2) utils.AUTH_URL_V2,
**kwargs)
return cl return cl
def get_authed_client(): def get_authed_client(**kwargs):
cl = get_client() cl = get_client(**kwargs)
cl.management_url = "http://example.com" cl.management_url = "http://example.com"
cl.auth_token = "token" cl.auth_token = "token"
cl.get_service_url = mock.Mock(return_value="http://example.com") cl.get_service_url = mock.Mock(return_value="http://example.com")
@ -164,14 +165,16 @@ class ClientTest(utils.TestCase):
test_refused_call() test_refused_call()
def test_client_logger(self): @mock.patch.object(requests, "request", mock_request)
cl1 = client.HTTPClient("username", "password", "project_id", @mock.patch.object(client, '_log_request_id')
"auth_test", http_log_debug=True) @mock.patch.object(client.HTTPClient, 'http_log_req')
self.assertEqual(1, len(cl1._logger.handlers)) def test_client_logger(self, mock_http_log_req, mock_log_request_id):
cl = get_authed_client(service_name='compute', http_log_debug=True)
self.assertIsNotNone(cl._logger)
cl2 = client.HTTPClient("username", "password", "project_id", cl.post("/hi", body=[1, 2, 3])
"auth_test", http_log_debug=True) mock_log_request_id.assert_called_once_with(cl._logger, fake_response,
self.assertEqual(1, len(cl2._logger.handlers)) 'compute')
@mock.patch.object(requests, 'request', unknown_error_mock_request) @mock.patch.object(requests, 'request', unknown_error_mock_request)
def test_unknown_server_error(self): def test_unknown_server_error(self):

View File

@ -13,6 +13,8 @@
# 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 logging
from novaclient import api_versions from novaclient import api_versions
from novaclient import client from novaclient import client
from novaclient.i18n import _LW from novaclient.i18n import _LW
@ -67,7 +69,7 @@ class Client(object):
auth_system='keystone', auth_plugin=None, auth_token=None, auth_system='keystone', auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None, cacert=None, tenant_id=None, user_id=None,
connection_pool=False, session=None, auth=None, connection_pool=False, session=None, auth=None,
api_version=None, direct_use=True, **kwargs): api_version=None, direct_use=True, logger=None, **kwargs):
"""Initialization of Client object. """Initialization of Client object.
:param str username: Username :param str username: Username
@ -99,6 +101,8 @@ class Client(object):
:param str session: Session :param str session: Session
:param str auth: Auth :param str auth: Auth
:param api_version: Compute API version :param api_version: Compute API version
:param direct_use: Direct use
:param logger: Logger
:type api_version: novaclient.api_versions.APIVersion :type api_version: novaclient.api_versions.APIVersion
""" """
if direct_use: if direct_use:
@ -174,6 +178,9 @@ class Client(object):
setattr(self, extension.name, setattr(self, extension.name,
extension.manager_class(self)) extension.manager_class(self))
if not logger:
logger = logging.getLogger(__name__)
self.client = client._construct_http_client( self.client = client._construct_http_client(
username=username, username=username,
password=password, password=password,
@ -202,6 +209,7 @@ class Client(object):
session=session, session=session,
auth=auth, auth=auth,
api_version=api_version, api_version=api_version,
logger=logger,
**kwargs) **kwargs)
@client._original_only @client._original_only

View File

@ -0,0 +1,11 @@
---
prelude: >
- Log 'x-openstack-request-id' or 'x-compute-request-id'
in each API call. If the caller (e.g. heat) uses oslo.log,
the caller's request id in oslo.context and the callee's
request id can be output in the same log message (same line).
features:
- Log 'x-openstack-request-id' or 'x-compute-request-id'
in each API call. If the caller (e.g. heat) uses oslo.log,
the caller's request id in oslo.context and the callee's
request id can be output in the same log message (same line).