Add function/sql results to trace info

This patch adds function/sql results to trace info
New arg: hide_result (Boolean True/False)
- True (default): Hide function/sql result by default
- False: Show result in trace info

With database: add_tracing(sqlalchemy, engine, name, hide_result=True)

Demo: https://tovin07.github.io/murano/environment-show-with-results.html

Change-Id: I317dfa04e0109d46c1a5ca1e0a3523cfd8470d78
This commit is contained in:
Tovin Seven 2017-03-23 10:38:45 +07:00
parent 88c1e8b9d3
commit 7813c4a711
4 changed files with 124 additions and 17 deletions

View File

@ -87,7 +87,8 @@ def stop(info=None):
profiler.stop(info=info) profiler.stop(info=info)
def trace(name, info=None, hide_args=False, allow_multiple_trace=True): def trace(name, info=None, hide_args=False, hide_result=True,
allow_multiple_trace=True):
"""Trace decorator for functions. """Trace decorator for functions.
Very useful if you would like to add trace point on existing function: Very useful if you would like to add trace point on existing function:
@ -102,6 +103,9 @@ def trace(name, info=None, hide_args=False, allow_multiple_trace=True):
:param hide_args: Don't push to trace info args and kwargs. Quite useful :param hide_args: Don't push to trace info args and kwargs. Quite useful
if you have some info in args that you wont to share, if you have some info in args that you wont to share,
e.g. passwords. e.g. passwords.
:param hide_result: Boolean value to hide/show function result in trace.
True - hide function result (default).
False - show function result in trace.
:param allow_multiple_trace: If the wrapped function has already been :param allow_multiple_trace: If the wrapped function has already been
traced either allow the new trace to occur traced either allow the new trace to occur
or raise a value error denoting that multiple or raise a value error denoting that multiple
@ -133,24 +137,43 @@ def trace(name, info=None, hide_args=False, allow_multiple_trace=True):
@functools.wraps(f) @functools.wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
if "name" not in info["function"]: # NOTE(tovin07): Workaround for this issue
# F823 local variable 'info'
# (defined in enclosing scope on line xxx)
# referenced before assignment
info_ = info
if "name" not in info_["function"]:
# Get this once (as it should **not** be changing in # Get this once (as it should **not** be changing in
# subsequent calls). # subsequent calls).
info["function"]["name"] = reflection.get_callable_name(f) info_["function"]["name"] = reflection.get_callable_name(f)
if not hide_args: if not hide_args:
info["function"]["args"] = str(args) info_["function"]["args"] = str(args)
info["function"]["kwargs"] = str(kwargs) info_["function"]["kwargs"] = str(kwargs)
with Trace(name, info=info): stop_info = None
return f(*args, **kwargs) try:
start(name, info=info_)
result = f(*args, **kwargs)
except Exception as ex:
stop_info = {"etype": reflection.get_class_name(ex)}
raise
else:
if not hide_result:
stop_info = {"function": {"result": repr(result)}}
return result
finally:
if stop_info:
stop(info=stop_info)
else:
stop()
return wrapper return wrapper
return decorator return decorator
def trace_cls(name, info=None, hide_args=False, def trace_cls(name, info=None, hide_args=False, hide_result=True,
trace_private=False, allow_multiple_trace=True, trace_private=False, allow_multiple_trace=True,
trace_class_methods=False, trace_static_methods=False): trace_class_methods=False, trace_static_methods=False):
"""Trace decorator for instances of class . """Trace decorator for instances of class .
@ -173,6 +196,9 @@ def trace_cls(name, info=None, hide_args=False,
:param hide_args: Don't push to trace info args and kwargs. Quite useful :param hide_args: Don't push to trace info args and kwargs. Quite useful
if you have some info in args that you wont to share, if you have some info in args that you wont to share,
e.g. passwords. e.g. passwords.
:param hide_result: Boolean value to hide/show function result in trace.
True - hide function result (default).
False - show function result in trace.
:param trace_private: Trace methods that starts with "_". It wont trace :param trace_private: Trace methods that starts with "_". It wont trace
methods that starts "__" even if it is turned on. methods that starts "__" even if it is turned on.
:param trace_static_methods: Trace staticmethods. This may be prone to :param trace_static_methods: Trace staticmethods. This may be prone to
@ -226,7 +252,8 @@ def trace_cls(name, info=None, hide_args=False,
# halfway trace this class). # halfway trace this class).
_ensure_no_multiple_traced(traceable_attrs) _ensure_no_multiple_traced(traceable_attrs)
for i, (attr_name, attr) in enumerate(traceable_attrs): for i, (attr_name, attr) in enumerate(traceable_attrs):
wrapped_method = trace(name, info=info, hide_args=hide_args)(attr) wrapped_method = trace(name, info=info, hide_args=hide_args,
hide_result=hide_result)(attr)
wrapper = traceable_wrappers[i] wrapper = traceable_wrappers[i]
if wrapper is not None: if wrapper is not None:
wrapped_method = wrapper(wrapped_method) wrapped_method = wrapper(wrapped_method)
@ -246,6 +273,7 @@ class TracedMeta(type):
>>> __trace_args__ = {'name': 'rpc', >>> __trace_args__ = {'name': 'rpc',
>>> 'info': None, >>> 'info': None,
>>> 'hide_args': False, >>> 'hide_args': False,
>>> 'hide_result': True,
>>> 'trace_private': False} >>> 'trace_private': False}
>>> >>>
>>> def my_method(self, some_args): >>> def my_method(self, some_args):

View File

@ -34,14 +34,16 @@ def enable():
_DISABLED = False _DISABLED = False
def add_tracing(sqlalchemy, engine, name): def add_tracing(sqlalchemy, engine, name, hide_result=True):
"""Add tracing to all sqlalchemy calls.""" """Add tracing to all sqlalchemy calls."""
if not _DISABLED: if not _DISABLED:
sqlalchemy.event.listen(engine, "before_cursor_execute", sqlalchemy.event.listen(engine, "before_cursor_execute",
_before_cursor_execute(name)) _before_cursor_execute(name))
sqlalchemy.event.listen(engine, "after_cursor_execute", sqlalchemy.event.listen(
_after_cursor_execute()) engine, "after_cursor_execute",
_after_cursor_execute(hide_result=hide_result)
)
@contextlib.contextmanager @contextlib.contextmanager
@ -66,10 +68,24 @@ def _before_cursor_execute(name):
return handler return handler
def _after_cursor_execute(): def _after_cursor_execute(hide_result=True):
"""Add listener that will send trace info after query is executed.""" """Add listener that will send trace info after query is executed.
:param hide_result: Boolean value to hide or show SQL result in trace.
True - hide SQL result (default).
False - show SQL result in trace.
"""
def handler(conn, cursor, statement, params, context, executemany): def handler(conn, cursor, statement, params, context, executemany):
profiler.stop() if not hide_result:
# Add SQL result to trace info in *-stop phase
info = {
"db": {
"result": str(cursor._rows)
}
}
profiler.stop(info=info)
else:
profiler.stop()
return handler return handler

View File

@ -185,6 +185,11 @@ def test_fn_exc():
raise ValueError() raise ValueError()
@profiler.trace("hide_result", hide_result=False)
def trace_with_result_func(a, i=10):
return (a, i)
class TraceDecoratorTestCase(test.TestCase): class TraceDecoratorTestCase(test.TestCase):
@mock.patch("osprofiler.profiler.stop") @mock.patch("osprofiler.profiler.stop")
@ -242,6 +247,27 @@ class TraceDecoratorTestCase(test.TestCase):
mock_start.assert_called_once_with("foo", info=expected_info) mock_start.assert_called_once_with("foo", info=expected_info)
mock_stop.assert_called_once_with(info=expected_stop_info) mock_stop.assert_called_once_with(info=expected_stop_info)
@mock.patch("osprofiler.profiler.stop")
@mock.patch("osprofiler.profiler.start")
def test_with_result(self, mock_start, mock_stop):
self.assertEqual((1, 2), trace_with_result_func(1, i=2))
start_info = {
"function": {
"name": "osprofiler.tests.unit.test_profiler"
".trace_with_result_func",
"args": str((1,)),
"kwargs": str({"i": 2})
}
}
stop_info = {
"function": {
"result": str((1, 2))
}
}
mock_start.assert_called_once_with("hide_result", info=start_info)
mock_stop.assert_called_once_with(info=stop_info)
class FakeTracedCls(object): class FakeTracedCls(object):

View File

@ -37,6 +37,19 @@ class SqlalchemyTracingTestCase(test.TestCase):
handler(mock.MagicMock(), 1, 2, 3, 4, 5) handler(mock.MagicMock(), 1, 2, 3, 4, 5)
mock_profiler.stop.assert_called_once_with() mock_profiler.stop.assert_called_once_with()
@mock.patch("osprofiler.sqlalchemy.profiler")
def test_after_execute_with_sql_result(self, mock_profiler):
handler = sqlalchemy._after_cursor_execute(hide_result=False)
cursor = mock.MagicMock()
cursor._rows = (1,)
handler(1, cursor, 2, 3, 4, 5)
info = {
"db": {
"result": str(cursor._rows)
}
}
mock_profiler.stop.assert_called_once_with(info=info)
@mock.patch("osprofiler.sqlalchemy._before_cursor_execute") @mock.patch("osprofiler.sqlalchemy._before_cursor_execute")
@mock.patch("osprofiler.sqlalchemy._after_cursor_execute") @mock.patch("osprofiler.sqlalchemy._after_cursor_execute")
def test_add_tracing(self, mock_after_exc, mock_before_exc): def test_add_tracing(self, mock_after_exc, mock_before_exc):
@ -49,7 +62,8 @@ class SqlalchemyTracingTestCase(test.TestCase):
sqlalchemy.add_tracing(sa, engine, "sql") sqlalchemy.add_tracing(sa, engine, "sql")
mock_before_exc.assert_called_once_with("sql") mock_before_exc.assert_called_once_with("sql")
mock_after_exc.assert_called_once_with() # Default set hide_result=True
mock_after_exc.assert_called_once_with(hide_result=True)
expected_calls = [ expected_calls = [
mock.call(engine, "before_cursor_execute", "before"), mock.call(engine, "before_cursor_execute", "before"),
mock.call(engine, "after_cursor_execute", "after") mock.call(engine, "after_cursor_execute", "after")
@ -78,7 +92,8 @@ class SqlalchemyTracingTestCase(test.TestCase):
pass pass
mock_before_exc.assert_called_once_with("db") mock_before_exc.assert_called_once_with("db")
mock_after_exc.assert_called_once_with() # Default set hide_result=True
mock_after_exc.assert_called_once_with(hide_result=True)
expected_calls = [ expected_calls = [
mock.call(sess.bind, "before_cursor_execute", "before"), mock.call(sess.bind, "before_cursor_execute", "before"),
mock.call(sess.bind, "after_cursor_execute", "after") mock.call(sess.bind, "after_cursor_execute", "after")
@ -86,6 +101,28 @@ class SqlalchemyTracingTestCase(test.TestCase):
self.assertEqual(sa.event.listen.call_args_list, expected_calls) self.assertEqual(sa.event.listen.call_args_list, expected_calls)
@mock.patch("osprofiler.sqlalchemy._before_cursor_execute")
@mock.patch("osprofiler.sqlalchemy._after_cursor_execute")
@mock.patch("osprofiler.profiler")
def test_with_sql_result(self, mock_profiler,
mock_after_exc, mock_before_exc):
sa = mock.MagicMock()
engine = mock.MagicMock()
mock_before_exc.return_value = "before"
mock_after_exc.return_value = "after"
sqlalchemy.add_tracing(sa, engine, "sql", hide_result=False)
mock_before_exc.assert_called_once_with("sql")
# Default set hide_result=True
mock_after_exc.assert_called_once_with(hide_result=False)
expected_calls = [
mock.call(engine, "before_cursor_execute", "before"),
mock.call(engine, "after_cursor_execute", "after")
]
self.assertEqual(sa.event.listen.call_args_list, expected_calls)
@mock.patch("osprofiler.sqlalchemy._before_cursor_execute") @mock.patch("osprofiler.sqlalchemy._before_cursor_execute")
@mock.patch("osprofiler.sqlalchemy._after_cursor_execute") @mock.patch("osprofiler.sqlalchemy._after_cursor_execute")
def test_disable_and_enable(self, mock_after_exc, mock_before_exc): def test_disable_and_enable(self, mock_after_exc, mock_before_exc):