Trace data class for enchanced Solum logging

* Adds easy structured logging capability
* Uses thread local storage to hold trace data needed in future logging
* Hooks into Oslo Log's context adapter to look via to_dict()
* Utilizes Oslo Config to format the trace logging output
* Includes ability to delete old trace data automatically
* Will accept a RequestContext as input

Partially implements blueprint logging
https://blueprints.launchpad.net/solum/+spec/logging

Change-Id: I9522c5eb1e2c56b1d48e444025d47d45bf0e264d
This commit is contained in:
Paul Montgomery 2014-02-07 19:34:24 +00:00
parent ca61f2844c
commit f4a02a714b
3 changed files with 203 additions and 0 deletions

View File

@ -0,0 +1,19 @@
# Copyright 2014 - Rackspace
#
# 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 threading
# Make a project global TLS trace storage repository
TLS = threading.local()

106
solum/common/trace_data.py Normal file
View File

@ -0,0 +1,106 @@
# Copyright 2014 - Rackspace
#
# 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 six
# The list below holds the keys corresponding to Oslo RequestContext keys
# which are acceptable for an authenticated user to view. Everything else
# will be considered support-only data. This list may be too restrictive and
# can be relaxed more as needed. This is only used if import_context() is
# called.
_TRACE_USER_KEYS = [six.u("user"), six.u("tenant")]
class TraceData(object):
"""This class holds trace data needed for logging.
This class also provides a way to indicate the confidentiality of stored
data. This is useful for properly logging support or operator-only data
in such a way that the backend log/notification filters have a single key
to filter on. It also provides a centralized location to add secure
storage/cleanup algorithms.
"""
def __init__(self):
"""Initialize class storage."""
self.request_id = six.u("<not set>")
self._auto_clear = False
self._user_data = {}
self._support_data = {}
def import_context(self, context):
"""Accept an Oslo RequestContext and fill in data automatically."""
context_dict = context.to_dict()
for key, val in six.iteritems(context_dict):
if key == "request_id":
self.request_id = val
elif key in _TRACE_USER_KEYS:
self._user_data[key] = val
else:
self._support_data[key] = val
def user_info(self, **kwargs):
"""Add data to user-visible storage."""
self._user_data.update(kwargs)
def support_info(self, **kwargs):
"""Add confidential data to support/operator-visible only storage."""
self._support_data.update(kwargs)
def clear(self):
"""Clear data regardless of confidential status.
Note: The lightweight memory clearing below is certainly not what we
want in the end. It is just an initial/simple/portable way to do this
for now. It is only somewhat useful before garbage collection.
"""
self.request_id = six.u("<not set>")
self._user_data = {}
for key in six.iterkeys(self._support_data):
self._support_data[key] = "0" * len(self._support_data[key])
self._support_data = {}
def to_dict(self):
"""Generate a dictionary for Oslo Log (requires 'request_id').
Because this class initializes request_id to <not set> the log
operation will succeed even if a UUID was not stored.
"""
if self._auto_clear is not True:
return ({"request_id": self.request_id,
"user_trace": self._user_data,
"support_trace": self._support_data})
user_data = self._user_data.copy()
support_data = self._support_data.copy()
self.clear()
return ({"request_id": self.request_id,
"user_trace": user_data,
"support_trace": support_data})
@property
def auto_clear(self):
"""_auto_clear getter."""
return self._auto_clear
@auto_clear.setter
def auto_clear(self, value):
"""_auto_clear setter to enable assertion checks.
When set to True, this class will automatically delete all trace data
after the next Oslo log call (or a to_dict() call). If the Oslo log
call does not log any data (log levels preventing it for example) then
the data is still deleted so care must be taken when using this.
"""
assert isinstance(value, bool)
self._auto_clear = value

View File

@ -0,0 +1,78 @@
# Copyright 2014 - Rackspace
#
# 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 solum
from solum.common import trace_data
from solum.openstack.common import context
from solum.tests import base
solum.TLS.trace = trace_data.TraceData()
# Just putting highly recognizable values in context
CONTEXT = context.RequestContext(
'_auth_token_', '_user_', '_tenant_', '_domain_', '_user_domain_',
'_project_domain_', '_is_admin_', '_read_only_', '_show_deleted_',
'_request_id_', '_instance_uuid_')
class TestTraceData(base.BaseTestCase):
"""Tests the TraceData class."""
def test_auto_clear(self):
"""auto_clear success and then a failure case."""
solum.TLS.trace.auto_clear = True
solum.TLS.trace.auto_clear = False
try:
solum.TLS.trace.auto_clear = 'fail'
except AssertionError:
pass
else:
self.assertTrue(False)
def test_import_context(self):
"""Test importing Oslo RequestContext."""
solum.TLS.trace.clear()
solum.TLS.trace.import_context(CONTEXT)
self.assertEqual(
solum.TLS.trace._user_data,
{'user': '_user_', 'tenant': '_tenant_'})
self.assertEqual(
solum.TLS.trace._support_data, (
{
'instance_uuid': '_instance_uuid_',
'read_only': '_read_only_',
'domain': '_domain_',
'show_deleted': '_show_deleted_',
'user_identity':
'_user_ _tenant_ _domain_ _user_domain_ _project_domain_',
'project_domain': '_project_domain_',
'auth_token': '_auth_token_',
'is_admin': '_is_admin_',
'user_domain': '_user_domain_'
}))
def test_info_commands(self):
"""Test trace setting functions."""
solum.TLS.trace.clear()
solum.TLS.trace.request_id = '98765'
solum.TLS.trace.user_info(ip_addr="1.2.3.4", user_id=12345)
solum.TLS.trace.support_info(confidential_data={"a": "b", "c": "d"})
self.assertEqual(
solum.TLS.trace._user_data,
{'ip_addr': '1.2.3.4', 'user_id': 12345})
self.assertEqual(
solum.TLS.trace._support_data,
{'confidential_data': {'a': 'b', 'c': 'd'}})
self.assertEqual(
solum.TLS.trace.request_id,
'98765')