From 7e32799200f9755ad60f3f8ee10bc335efb17910 Mon Sep 17 00:00:00 2001 From: Guillaume Autran Date: Wed, 9 Aug 2023 17:06:28 -0400 Subject: [PATCH] Command to monitor and pretty print the rosout logs #849 Creation of a rosout print command that generates pretty prints of the content of the `/rosout` topic. The commands also allows for the filtering of nodes based on a regular expression applied to the node name itself. Output color can be disabled (enabled by default) and the function information detail can also be displayed. issue: #849 --- ros2rosout/README.md | 19 ++++ ros2rosout/package.xml | 29 +++++ ros2rosout/ros2rosout/__init__.py | 0 ros2rosout/ros2rosout/command/__init__.py | 0 ros2rosout/ros2rosout/command/rosout.py | 37 +++++++ ros2rosout/ros2rosout/verb/__init__.py | 43 ++++++++ ros2rosout/ros2rosout/verb/print.py | 124 ++++++++++++++++++++++ ros2rosout/setup.py | 44 ++++++++ ros2rosout/test/test_copyright.py | 23 ++++ ros2rosout/test/test_flake8.py | 25 +++++ ros2rosout/test/test_pep257.py | 23 ++++ ros2rosout/test/test_xmllint.py | 23 ++++ 12 files changed, 390 insertions(+) create mode 100644 ros2rosout/README.md create mode 100644 ros2rosout/package.xml create mode 100644 ros2rosout/ros2rosout/__init__.py create mode 100644 ros2rosout/ros2rosout/command/__init__.py create mode 100644 ros2rosout/ros2rosout/command/rosout.py create mode 100644 ros2rosout/ros2rosout/verb/__init__.py create mode 100644 ros2rosout/ros2rosout/verb/print.py create mode 100644 ros2rosout/setup.py create mode 100644 ros2rosout/test/test_copyright.py create mode 100644 ros2rosout/test/test_flake8.py create mode 100644 ros2rosout/test/test_pep257.py create mode 100644 ros2rosout/test/test_xmllint.py diff --git a/ros2rosout/README.md b/ros2rosout/README.md new file mode 100644 index 000000000..3593f820e --- /dev/null +++ b/ros2rosout/README.md @@ -0,0 +1,19 @@ +# ros2rosout + +This is the `ros2 rosout print` utility command which displaies the content of `/rosout` topic in a nicely formatted and colorized output. + +## Usage + +Run `ros2 rosout print` to get the live stream of the logs. + +Run `ros2 rosout print -h/--help` to print all available command arguments. + + +## Output format + +The command outputs the log line with the following format: +`[` _datetime_ `] [` _level_ `] [` _nodename_ `]: ` _log_ `:[` _file_ `:` _line_ `(` _function_ `)]` + +File, line and function are all optional and are displays when the `-f/--function-detail` switch is provided. + + diff --git a/ros2rosout/package.xml b/ros2rosout/package.xml new file mode 100644 index 000000000..a799cbbc9 --- /dev/null +++ b/ros2rosout/package.xml @@ -0,0 +1,29 @@ + + + + ros2rosout + 0.0.0 + A command line tool to print the rosout logs in a ROS 2 system + + Guillaume Autran + + Apache License 2.0 + + Guillaume Autran + + ament_index_python + rclpy + ros2cli + rcl_interfaces + + ament_copyright + ament_flake8 + ament_pep257 + ament_xmllint + launch + launch_ros + + + ament_python + + diff --git a/ros2rosout/ros2rosout/__init__.py b/ros2rosout/ros2rosout/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ros2rosout/ros2rosout/command/__init__.py b/ros2rosout/ros2rosout/command/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ros2rosout/ros2rosout/command/rosout.py b/ros2rosout/ros2rosout/command/rosout.py new file mode 100644 index 000000000..1adb88337 --- /dev/null +++ b/ros2rosout/ros2rosout/command/rosout.py @@ -0,0 +1,37 @@ +# Copyright 2023 Clearpath Robotics Inc. +# +# 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. + +from ros2cli.command import add_subparsers_on_demand +from ros2cli.command import CommandExtension + + +class RosoutCommand(CommandExtension): + """Prints the '/rosout' log stream """ + + def add_arguments(self, parser, cli_name): + self._subparser = parser + # add arguments and sub-commands of verbs + add_subparsers_on_demand( + parser, cli_name, '_verb', 'ros2rosout.verb', required=False) + + def main(self, *, parser, args): + if not hasattr(args, '_verb'): + # in case no verb was passed + self._subparser.print_help() + return 0 + + extension = getattr(args, '_verb') + + # call the verb's main method + return extension.main(args=args) diff --git a/ros2rosout/ros2rosout/verb/__init__.py b/ros2rosout/ros2rosout/verb/__init__.py new file mode 100644 index 000000000..7f8820d9e --- /dev/null +++ b/ros2rosout/ros2rosout/verb/__init__.py @@ -0,0 +1,43 @@ +# Copyright 2023 Clearpath Robotics Inc. +# +# 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. +from ros2cli.plugin_system import PLUGIN_SYSTEM_VERSION +from ros2cli.plugin_system import satisfies_version + + +class VerbExtension: + """ + The extension point for 'print' verb extensions. + + The following properties must be defined: + * `NAME` (will be set to the entry point name) + + The following methods must be defined: + * `main` + + The following methods can be defined: + * `add_arguments` + """ + + NAME = None + EXTENSION_POINT_VERSION = '0.1' + + def __init__(self): + super(VerbExtension, self).__init__() + satisfies_version(PLUGIN_SYSTEM_VERSION, '^0.1') + + def add_arguments(self, parser, cli_name): + pass + + def main(self, *, args): + raise NotImplementedError() diff --git a/ros2rosout/ros2rosout/verb/print.py b/ros2rosout/ros2rosout/verb/print.py new file mode 100644 index 000000000..2d3f32fad --- /dev/null +++ b/ros2rosout/ros2rosout/verb/print.py @@ -0,0 +1,124 @@ +# Copyright 2023 Clearpath Robotics +# +# 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. +from datetime import datetime +from rcl_interfaces.msg import Log +from ros2cli.node.strategy import NodeStrategy +from ros2cli.node.strategy import add_arguments +from ros2rosout.verb import VerbExtension +import rclpy +import re + + +class PrintVerb(VerbExtension): + """Outputs the '/rosout' content in a nicely formatted way""" + + BLACK_TEXT = "\033[30;1m" + BLUE_TEXT = "\033[34;1m" + BOLD_TEXT = "\033[1m" + CYAN_TEXT = "\033[36;1m" + GREEN_TEXT = "\033[32;1m" + MAGENTA_TEXT = "\033[35;1m" + RED_TEXT = "\033[31;1m" + WHITE_TEXT = "\033[37;1m" + YELLOW_TEXT = "\033[33;1m" + + COLOR_RESET = "\033[0m" + + def add_arguments(self, parser, cli_name): + add_arguments(parser) + parser.add_argument( + '-l', '--level', default=int.from_bytes(Log.INFO, 'big'), type=int, + help='Print log statement with priority level greater than this value') + parser.add_argument( + '-n', '--node-regex', default=None, + help='Only print log statements from node(s) matching the regular expression provided') + parser.add_argument( + '--no-color', action='store_true', default=False, + help='Disables the use of ASCII colors for the output of the command') + parser.add_argument( + '-f', '--function-detail', action='store_true', default=False, + help='Output function name, file, and line number') + + def level_to_string(self, level): + if type(level) is not bytes: + level = level.to_bytes(1, 'big') + + match level: + case Log.DEBUG: + return "DEBUG" + case Log.INFO: + return "INFO " + case Log.WARN: + return "WARN " + case Log.ERROR: + return "ERROR" + case Log.FATAL: + return "FATAL" + case _: + return "_____" + + def stamp_to_string(self, stamp): + dt = datetime.fromtimestamp(stamp.sec) + s = dt.strftime('%Y-%m-%d %H:%M:%S') + s += '.' + str(int(stamp.nanosec % 1000000000)).zfill(9) + return f"{s}" + + def add_color(self, txt, color=WHITE_TEXT): + if self.args_.no_color: + return txt + + return f"{color}{txt}{self.COLOR_RESET}" + + def get_color(self, level): + if type(level) is not bytes: + level = level.to_bytes(1, 'big') + + match level: + case Log.DEBUG: + return self.GREEN_TEXT + case Log.INFO: + return self.WHITE_TEXT + case Log.WARN: + return self.YELLOW_TEXT + case Log.ERROR: + return self.RED_TEXT + case Log.FATAL: + return f"{self.RED_TEXT}{self.BOLD_TEXT}" + case _: + return self.BOLD_TEXT + + def rosout_cb(self, msg): + if msg.level < self.args_.level: + return + if self.args_.node_regex and not re.search(self.args_.node_regex, msg.name): + return + color = self.get_color(msg.level) + lvl = self.add_color(self.level_to_string(msg.level), color) + dt = self.add_color(self.stamp_to_string(msg.stamp), color) + name = self.add_color(msg.name, color) + mmsg = self.add_color(msg.msg, color) + text = f"[{dt}] [{lvl}] [{name}]: {mmsg}" + if self.args_.function_detail: + file = self.add_color(msg.file, self.BLUE_TEXT) + line = self.add_color(msg.line, self.BLUE_TEXT) + function = self.add_color(msg.function, self.CYAN_TEXT) + text += f" [{file}:{line}({function})]" + print(f"{text}") + + def main(self, *, args): + self.args_ = args + + with NodeStrategy(args) as node: + self.rosout_ = node.node.create_subscription(Log, '/rosout', self.rosout_cb, 10) + rclpy.spin(node) diff --git a/ros2rosout/setup.py b/ros2rosout/setup.py new file mode 100644 index 000000000..2bf67dbd4 --- /dev/null +++ b/ros2rosout/setup.py @@ -0,0 +1,44 @@ +from setuptools import find_packages +from setuptools import setup + +package_name = 'ros2rosout' + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/' + package_name, ['package.xml']), + ], + install_requires=['ros2cli'], + zip_safe=True, + author='Guillaume Autran', + author_email='gautran@clearpath.ai', + maintainer='Guillaume Autran', + maintainer_email='gautran@clearpath.ai', + url='', + download_url='', + keywords=[], + classifiers=[ + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + ], + description='A convenient command to display the /rosout logs for ROS 2 command line tools', + long_description="""\ + The package provides a cli tool to print the `/rosout` logs in a ROS 2 system""", + license='Apache License, Version 2.0', + tests_require=['pytest'], + entry_points={ + 'ros2cli.command': [ + 'rosout = ros2rosout.command.rosout:RosoutCommand', + ], + 'ros2cli.extension_point': [ + 'ros2rosout.verb = ros2rosout.verb:VerbExtension', + ], + 'ros2rosout.verb': [ + 'print = ros2rosout.verb.print:PrintVerb' + ] + } +) diff --git a/ros2rosout/test/test_copyright.py b/ros2rosout/test/test_copyright.py new file mode 100644 index 000000000..cf0fae31f --- /dev/null +++ b/ros2rosout/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/ros2rosout/test/test_flake8.py b/ros2rosout/test/test_flake8.py new file mode 100644 index 000000000..27ee1078f --- /dev/null +++ b/ros2rosout/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/ros2rosout/test/test_pep257.py b/ros2rosout/test/test_pep257.py new file mode 100644 index 000000000..0e38a6c60 --- /dev/null +++ b/ros2rosout/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' diff --git a/ros2rosout/test/test_xmllint.py b/ros2rosout/test/test_xmllint.py new file mode 100644 index 000000000..f46285e71 --- /dev/null +++ b/ros2rosout/test/test_xmllint.py @@ -0,0 +1,23 @@ +# Copyright 2019 Open Source Robotics Foundation, Inc. +# +# 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. + +from ament_xmllint.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.xmllint +def test_xmllint(): + rc = main(argv=[]) + assert rc == 0, 'Found errors'