Skip to content

Commit 7e32799

Browse files
Command to monitor and pretty print the rosout logs ros2#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: ros2#849
1 parent c43e064 commit 7e32799

12 files changed

Lines changed: 390 additions & 0 deletions

File tree

‎ros2rosout/README.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ros2rosout
2+
3+
This is the `ros2 rosout print` utility command which displaies the content of `/rosout` topic in a nicely formatted and colorized output.
4+
5+
## Usage
6+
7+
Run `ros2 rosout print` to get the live stream of the logs.
8+
9+
Run `ros2 rosout print -h/--help` to print all available command arguments.
10+
11+
12+
## Output format
13+
14+
The command outputs the log line with the following format:
15+
`[` _datetime_ `] [` _level_ `] [` _nodename_ `]: ` _log_ `:[` _file_ `:` _line_ `(` _function_ `)]`
16+
17+
File, line and function are all optional and are displays when the `-f/--function-detail` switch is provided.
18+
19+

‎ros2rosout/package.xml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>ros2rosout</name>
5+
<version>0.0.0</version>
6+
<description>A command line tool to print the rosout logs in a ROS 2 system</description>
7+
8+
<maintainer email="gautran@clearpath.ai">Guillaume Autran</maintainer>
9+
10+
<license>Apache License 2.0</license>
11+
12+
<author email="gautran@clearpath.ai">Guillaume Autran</author>
13+
14+
<exec_depend>ament_index_python</exec_depend>
15+
<exec_depend>rclpy</exec_depend>
16+
<exec_depend>ros2cli</exec_depend>
17+
<exec_depend>rcl_interfaces</exec_depend>
18+
19+
<test_depend>ament_copyright</test_depend>
20+
<test_depend>ament_flake8</test_depend>
21+
<test_depend>ament_pep257</test_depend>
22+
<test_depend>ament_xmllint</test_depend>
23+
<test_depend>launch</test_depend>
24+
<test_depend>launch_ros</test_depend>
25+
26+
<export>
27+
<build_type>ament_python</build_type>
28+
</export>
29+
</package>

‎ros2rosout/ros2rosout/__init__.py‎

Whitespace-only changes.

‎ros2rosout/ros2rosout/command/__init__.py‎

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2023 Clearpath Robotics Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ros2cli.command import add_subparsers_on_demand
16+
from ros2cli.command import CommandExtension
17+
18+
19+
class RosoutCommand(CommandExtension):
20+
"""Prints the '/rosout' log stream """
21+
22+
def add_arguments(self, parser, cli_name):
23+
self._subparser = parser
24+
# add arguments and sub-commands of verbs
25+
add_subparsers_on_demand(
26+
parser, cli_name, '_verb', 'ros2rosout.verb', required=False)
27+
28+
def main(self, *, parser, args):
29+
if not hasattr(args, '_verb'):
30+
# in case no verb was passed
31+
self._subparser.print_help()
32+
return 0
33+
34+
extension = getattr(args, '_verb')
35+
36+
# call the verb's main method
37+
return extension.main(args=args)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2023 Clearpath Robotics Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from ros2cli.plugin_system import PLUGIN_SYSTEM_VERSION
15+
from ros2cli.plugin_system import satisfies_version
16+
17+
18+
class VerbExtension:
19+
"""
20+
The extension point for 'print' verb extensions.
21+
22+
The following properties must be defined:
23+
* `NAME` (will be set to the entry point name)
24+
25+
The following methods must be defined:
26+
* `main`
27+
28+
The following methods can be defined:
29+
* `add_arguments`
30+
"""
31+
32+
NAME = None
33+
EXTENSION_POINT_VERSION = '0.1'
34+
35+
def __init__(self):
36+
super(VerbExtension, self).__init__()
37+
satisfies_version(PLUGIN_SYSTEM_VERSION, '^0.1')
38+
39+
def add_arguments(self, parser, cli_name):
40+
pass
41+
42+
def main(self, *, args):
43+
raise NotImplementedError()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Copyright 2023 Clearpath Robotics
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from datetime import datetime
15+
from rcl_interfaces.msg import Log
16+
from ros2cli.node.strategy import NodeStrategy
17+
from ros2cli.node.strategy import add_arguments
18+
from ros2rosout.verb import VerbExtension
19+
import rclpy
20+
import re
21+
22+
23+
class PrintVerb(VerbExtension):
24+
"""Outputs the '/rosout' content in a nicely formatted way"""
25+
26+
BLACK_TEXT = "\033[30;1m"
27+
BLUE_TEXT = "\033[34;1m"
28+
BOLD_TEXT = "\033[1m"
29+
CYAN_TEXT = "\033[36;1m"
30+
GREEN_TEXT = "\033[32;1m"
31+
MAGENTA_TEXT = "\033[35;1m"
32+
RED_TEXT = "\033[31;1m"
33+
WHITE_TEXT = "\033[37;1m"
34+
YELLOW_TEXT = "\033[33;1m"
35+
36+
COLOR_RESET = "\033[0m"
37+
38+
def add_arguments(self, parser, cli_name):
39+
add_arguments(parser)
40+
parser.add_argument(
41+
'-l', '--level', default=int.from_bytes(Log.INFO, 'big'), type=int,
42+
help='Print log statement with priority level greater than this value')
43+
parser.add_argument(
44+
'-n', '--node-regex', default=None,
45+
help='Only print log statements from node(s) matching the regular expression provided')
46+
parser.add_argument(
47+
'--no-color', action='store_true', default=False,
48+
help='Disables the use of ASCII colors for the output of the command')
49+
parser.add_argument(
50+
'-f', '--function-detail', action='store_true', default=False,
51+
help='Output function name, file, and line number')
52+
53+
def level_to_string(self, level):
54+
if type(level) is not bytes:
55+
level = level.to_bytes(1, 'big')
56+
57+
match level:
58+
case Log.DEBUG:
59+
return "DEBUG"
60+
case Log.INFO:
61+
return "INFO "
62+
case Log.WARN:
63+
return "WARN "
64+
case Log.ERROR:
65+
return "ERROR"
66+
case Log.FATAL:
67+
return "FATAL"
68+
case _:
69+
return "_____"
70+
71+
def stamp_to_string(self, stamp):
72+
dt = datetime.fromtimestamp(stamp.sec)
73+
s = dt.strftime('%Y-%m-%d %H:%M:%S')
74+
s += '.' + str(int(stamp.nanosec % 1000000000)).zfill(9)
75+
return f"{s}"
76+
77+
def add_color(self, txt, color=WHITE_TEXT):
78+
if self.args_.no_color:
79+
return txt
80+
81+
return f"{color}{txt}{self.COLOR_RESET}"
82+
83+
def get_color(self, level):
84+
if type(level) is not bytes:
85+
level = level.to_bytes(1, 'big')
86+
87+
match level:
88+
case Log.DEBUG:
89+
return self.GREEN_TEXT
90+
case Log.INFO:
91+
return self.WHITE_TEXT
92+
case Log.WARN:
93+
return self.YELLOW_TEXT
94+
case Log.ERROR:
95+
return self.RED_TEXT
96+
case Log.FATAL:
97+
return f"{self.RED_TEXT}{self.BOLD_TEXT}"
98+
case _:
99+
return self.BOLD_TEXT
100+
101+
def rosout_cb(self, msg):
102+
if msg.level < self.args_.level:
103+
return
104+
if self.args_.node_regex and not re.search(self.args_.node_regex, msg.name):
105+
return
106+
color = self.get_color(msg.level)
107+
lvl = self.add_color(self.level_to_string(msg.level), color)
108+
dt = self.add_color(self.stamp_to_string(msg.stamp), color)
109+
name = self.add_color(msg.name, color)
110+
mmsg = self.add_color(msg.msg, color)
111+
text = f"[{dt}] [{lvl}] [{name}]: {mmsg}"
112+
if self.args_.function_detail:
113+
file = self.add_color(msg.file, self.BLUE_TEXT)
114+
line = self.add_color(msg.line, self.BLUE_TEXT)
115+
function = self.add_color(msg.function, self.CYAN_TEXT)
116+
text += f" [{file}:{line}({function})]"
117+
print(f"{text}")
118+
119+
def main(self, *, args):
120+
self.args_ = args
121+
122+
with NodeStrategy(args) as node:
123+
self.rosout_ = node.node.create_subscription(Log, '/rosout', self.rosout_cb, 10)
124+
rclpy.spin(node)

‎ros2rosout/setup.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from setuptools import find_packages
2+
from setuptools import setup
3+
4+
package_name = 'ros2rosout'
5+
6+
setup(
7+
name=package_name,
8+
version='0.0.0',
9+
packages=find_packages(exclude=['test']),
10+
data_files=[
11+
('share/' + package_name, ['package.xml']),
12+
],
13+
install_requires=['ros2cli'],
14+
zip_safe=True,
15+
author='Guillaume Autran',
16+
author_email='gautran@clearpath.ai',
17+
maintainer='Guillaume Autran',
18+
maintainer_email='gautran@clearpath.ai',
19+
url='',
20+
download_url='',
21+
keywords=[],
22+
classifiers=[
23+
'Environment :: Console',
24+
'Intended Audience :: Developers',
25+
'License :: OSI Approved :: Apache Software License',
26+
'Programming Language :: Python',
27+
],
28+
description='A convenient command to display the /rosout logs for ROS 2 command line tools',
29+
long_description="""\
30+
The package provides a cli tool to print the `/rosout` logs in a ROS 2 system""",
31+
license='Apache License, Version 2.0',
32+
tests_require=['pytest'],
33+
entry_points={
34+
'ros2cli.command': [
35+
'rosout = ros2rosout.command.rosout:RosoutCommand',
36+
],
37+
'ros2cli.extension_point': [
38+
'ros2rosout.verb = ros2rosout.verb:VerbExtension',
39+
],
40+
'ros2rosout.verb': [
41+
'print = ros2rosout.verb.print:PrintVerb'
42+
]
43+
}
44+
)

‎ros2rosout/test/test_copyright.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2017 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ament_copyright.main import main
16+
import pytest
17+
18+
19+
@pytest.mark.copyright
20+
@pytest.mark.linter
21+
def test_copyright():
22+
rc = main(argv=['.', 'test'])
23+
assert rc == 0, 'Found errors'

‎ros2rosout/test/test_flake8.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2017 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ament_flake8.main import main_with_errors
16+
import pytest
17+
18+
19+
@pytest.mark.flake8
20+
@pytest.mark.linter
21+
def test_flake8():
22+
rc, errors = main_with_errors(argv=[])
23+
assert rc == 0, \
24+
'Found %d code style errors / warnings:\n' % len(errors) + \
25+
'\n'.join(errors)

0 commit comments

Comments
 (0)