Apply a heuristic for product name if a user_agent is not provided

Apply a heuristic when the caller of keystoneauth Session does not
specify a user_agent. The approach is simple:
 - use the name of the calling program aka sys.argv[0]
 - if that name is in the ignore list, look at the call stack to
   determine who imported this module. Again, honoring a ignore list.
 - return None if all else fails

This ensures that all of the OpenStack services are logged properly in
Keystone's http access logs.

Change-Id: I942fbac97ad830639cf4c45f6e8fb253838f54e5
This commit is contained in:
Sean Perry 2016-03-03 15:23:39 -08:00
parent 29aaac32e1
commit 2a8133c8ab
2 changed files with 86 additions and 5 deletions

View File

@ -15,8 +15,10 @@ import functools
import hashlib
import json
import logging
import os
import platform
import socket
import sys
import time
import uuid
@ -93,6 +95,72 @@ class _StringFormatter(object):
return value
def _determine_calling_package():
"""Walk the call frames trying to identify what is using this module."""
# Create a lookup table mapping file name to module name. The ``inspect``
# module does this but is far less efficient. Same story with the
# frame walking below. One could use ``inspect.stack()`` but it
# has far more overhead.
mod_lookup = dict((m.__file__, n) for n, m in sys.modules.items()
if hasattr(m, '__file__'))
# NOTE(shaleh): these are not useful because they hide the real
# user of the code. debtcollector did not import keystoneauth but
# it will show up in the call stack. Similarly we do not want to
# report ourselves or keystone client as the user agent. The real
# user is the code importing them.
ignored = ('debtcollector', 'keystoneauth1', 'keystoneclient')
i = 0
while True:
i += 1
try:
# NOTE(shaleh): this is safe in CPython but could break in
# other implementations of Python. Yes, the `inspect`
# module could be used instead. But it does a lot more
# work so it has worse performance.
f = sys._getframe(i)
try:
name = mod_lookup[f.f_code.co_filename]
# finds the full name module.foo.bar but all we need
# is the module name.
name, _, _ = name.partition('.')
if name not in ignored:
return name
except KeyError:
pass # builtin or the like
except ValueError:
# hit the bottom of the frame stack
break
return None
def _determine_user_agent():
"""Attempt to programatically generate a user agent string.
First, look at the name of the process. Return this unless it is in
the `ignored` list. Otherwise, look at the function call stack and
try to find the name of the code that invoked this module.
"""
# NOTE(shaleh): mod_wsgi is not any more useful than just
# reporting "keystoneauth". Ignore it and perform the package name
# heuristic.
ignored = ('mod_wsgi', )
try:
name = sys.argv[0]
except IndexError:
# sys.argv is empty, usually the Python interpreter prevents this.
return None
name = os.path.basename(name)
if name in ignored:
name = _determine_calling_package()
return name
class Session(object):
"""Maintains client communication state and common functionality.
@ -156,12 +224,16 @@ class Session(object):
if timeout is not None:
self.timeout = float(timeout)
# don't override the class variable if none provided
# Per RFC 7231 Section 5.5.3, identifiers in a user-agent should be
# ordered by decreasing significance. If a user sets their product
# that value will be used. Otherwise we attempt to derive a useful
# product value. The value will be prepended it to the KSA version,
# requests version, and then the Python version.
if user_agent is None:
user_agent = _determine_user_agent()
if user_agent is not None:
# Per RFC 7231 Section 5.5.3, identifiers in a user-agent
# should be ordered by decreasing significance.
# If a user sets their product, we prepend it to the KSA
# version, requests version, and then the Python version.
self.user_agent = "%s %s" % (user_agent, DEFAULT_USER_AGENT)
self._json = _JSONEncoder()

View File

@ -90,6 +90,15 @@ class SessionTests(utils.TestCase):
self.assertRequestBodyIs(json={'hello': 'world'})
def test_user_agent(self):
session = client_session.Session()
self.stub_url('GET', text='response')
resp = session.get(self.TEST_URL)
self.assertTrue(resp.ok)
self.assertRequestHeaderEqual(
'User-Agent',
'%s %s' % ("run.py", client_session.DEFAULT_USER_AGENT))
custom_agent = 'custom-agent/1.0'
session = client_session.Session(user_agent=custom_agent)
self.stub_url('GET', text='response')