diff --git a/tools/rosgraph/src/rosgraph/roslogging.py b/tools/rosgraph/src/rosgraph/roslogging.py index e4cfafef9b..1785390219 100644 --- a/tools/rosgraph/src/rosgraph/roslogging.py +++ b/tools/rosgraph/src/rosgraph/roslogging.py @@ -39,12 +39,43 @@ import time import logging import logging.config +import inspect import rospkg from rospkg.environment import ROS_LOG_DIR class LoggingException(Exception): pass +class RospyLogger(logging.getLoggerClass()): + def findCaller(self): + """ + Find the stack frame of the caller so that we can note the source + file name, line number, and function name with class name if possible. + """ + file_name, lineno, func_name = super(RospyLogger, self).findCaller() + + f = inspect.currentframe() + if f is not None: + f = f.f_back + while hasattr(f, "f_code"): + # we search the right frame using the data already found by parent class + # following python logging findCaller() implementation logic + co = f.f_code + filename = os.path.normcase(co.co_filename) + if filename != file_name or f.f_lineno != lineno or co.co_name != func_name: + f = f.f_back + continue + # we found the correct frame, now extending func_name with class name + try: + class_name = f.f_locals['self'].__class__.__name__ + func_name = '%s.%s' % (class_name, func_name) + except KeyError: # if the function is unbound, there is no self. + pass + break + return file_name, lineno, func_name + +logging.setLoggerClass(RospyLogger) + def renew_latest_logdir(logfile_dir): log_dir = os.path.dirname(logfile_dir) latest_dir = os.path.join(log_dir, 'latest') diff --git a/tools/rosgraph/test/test_roslogging.py b/tools/rosgraph/test/test_roslogging.py new file mode 100644 index 0000000000..3601d467dd --- /dev/null +++ b/tools/rosgraph/test/test_roslogging.py @@ -0,0 +1,111 @@ +# Software License Agreement (BSD License) +# +# Copyright (c) 2016, Kentaro Wada. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of Willow Garage, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging +import os +from StringIO import StringIO +import sys + +from nose.tools import assert_regexp_matches +import rosgraph.roslogging + + +os.environ['ROSCONSOLE_FORMAT'] = ' '.join([ + '${severity}', + '${message}', + '${walltime}', + '${thread}', + '${logger}', + '${file}', + '${line}', + '${function}', + '${node}', + '${time}', +]) +rosgraph.roslogging.configure_logging('test_rosgraph', logging.INFO) +loginfo = logging.getLogger('rosout').info + +# Remap stdout for testing +f = StringIO() +sys.stdout = f + + +loginfo('on module') + + +def logging_on_function(): + loginfo('on function') + +logging_on_function() + + +class LoggingOnClass(object): + + def __init__(self): + loginfo('on method') + +LoggingOnClass() + + +def test_rosconsole__logging_format(): + this_file = os.path.abspath(__file__) + # this is necessary to avoid test fails because of .pyc cache file + base, ext = os.path.splitext(this_file) + if ext == '.pyc': + this_file = base + '.py' + + for i, loc in enumerate(['module', 'function', 'method']): + if loc == 'module': + function = '' + elif loc == 'function': + function = 'logging_on_function' + elif loc == 'method': + function = 'LoggingOnClass.__init__' + else: + raise ValueError + + log_out = ' '.join([ + 'INFO', + 'on ' + loc, + '[0-9]*\.[0-9]*', + '[0-9]*', + 'rosout', + this_file, + '[0-9]*', + function, + '/unnamed', + '[0-9]*\.[0-9]*', + ]) + assert_regexp_matches(f.getvalue().splitlines()[i], log_out) + + +sys.stdout = sys.__stdout__ diff --git a/tools/rosgraph/test/test_roslogging_user_logger.py b/tools/rosgraph/test/test_roslogging_user_logger.py new file mode 100644 index 0000000000..8d20575780 --- /dev/null +++ b/tools/rosgraph/test/test_roslogging_user_logger.py @@ -0,0 +1,118 @@ +# Software License Agreement (BSD License) +# +# Copyright (c) 2016, Kentaro Wada. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of Willow Garage, Inc. nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Test for rosgraph.roslogger with custom logger defined by user, +to ensure the custom logger won't be overwritten by RospyLogger defined +in rosgraph.roslogger. +""" + +import logging +import os +from StringIO import StringIO +import sys + +from nose.tools import assert_regexp_matches + +import rosgraph.roslogging + + +# set user defined custom logger +class UserCustomLogger(logging.Logger): + def findCaller(self): + """Returns static caller. + + This method is being overwritten in rosgraph.roslogging. + """ + return '', '', '' + + def _log(self, level, msg, args, exc_info=None, extra=None): + """Write log with ROS_IP. + + This method is not being overwritten in rosgraph.roslogging. + """ + ros_ip = os.environ.get('ROS_IP', '') + msg = '%s %s' % (ros_ip, msg) + logging.Logger._log(self, level, msg, args, exc_info, extra) + + +def setup_module(): + logging.setLoggerClass(UserCustomLogger) + + +def teardown_module(): + logging.setLoggerClass(rosgraph.roslogging.RospyLogger) + + +def test_roslogging_user_logger(): + os.environ['ROS_IP'] = '127.0.0.1' + os.environ['ROSCONSOLE_FORMAT'] = ' '.join([ + '${severity}', + '${message}', + '${walltime}', + '${thread}', + '${logger}', + '${file}', + '${line}', + '${function}', + '${node}', + '${time}', + ]) + rosgraph.roslogging.configure_logging('test_rosgraph', logging.INFO) + loginfo = logging.getLogger('rosout.custom_logger_test').info + + # Remap stdout for testing + f = StringIO() + sys.stdout = f + + # Logging + msg = 'Hello world.' + loginfo(msg) + + # Restore stdout + log_actual = f.getvalue().strip() + sys.stdout = sys.__stdout__ + + log_expected = ' '.join([ + 'INFO', + os.environ['ROS_IP'], + msg, + '[0-9]*\.[0-9]*', + '[0-9]*', + 'rosout.custom_logger_test', + '', + '', + '', + '/unnamed', + '[0-9]*\.[0-9]*', + ]) + assert_regexp_matches(log_actual, log_expected)