Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command to monitor and pretty print the rosout logs #849 #850

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ros2rosout/README.md
Original file line number Diff line number Diff line change
@@ -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.


29 changes: 29 additions & 0 deletions ros2rosout/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>ros2rosout</name>
<version>0.0.0</version>
<description>A command line tool to print the rosout logs in a ROS 2 system</description>

<maintainer email="[email protected]">Guillaume Autran</maintainer>

<license>Apache License 2.0</license>

<author email="[email protected]">Guillaume Autran</author>

<exec_depend>ament_index_python</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>ros2cli</exec_depend>
<exec_depend>rcl_interfaces</exec_depend>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>ament_xmllint</test_depend>
<test_depend>launch</test_depend>
<test_depend>launch_ros</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
Empty file.
Empty file.
37 changes: 37 additions & 0 deletions ros2rosout/ros2rosout/command/rosout.py
Original file line number Diff line number Diff line change
@@ -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)
43 changes: 43 additions & 0 deletions ros2rosout/ros2rosout/verb/__init__.py
Original file line number Diff line number Diff line change
@@ -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()
124 changes: 124 additions & 0 deletions ros2rosout/ros2rosout/verb/print.py
Original file line number Diff line number Diff line change
@@ -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)
44 changes: 44 additions & 0 deletions ros2rosout/setup.py
Original file line number Diff line number Diff line change
@@ -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='[email protected]',
maintainer='Guillaume Autran',
maintainer_email='[email protected]',
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'
]
}
)
23 changes: 23 additions & 0 deletions ros2rosout/test/test_copyright.py
Original file line number Diff line number Diff line change
@@ -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'
25 changes: 25 additions & 0 deletions ros2rosout/test/test_flake8.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions ros2rosout/test/test_pep257.py
Original file line number Diff line number Diff line change
@@ -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'
23 changes: 23 additions & 0 deletions ros2rosout/test/test_xmllint.py
Original file line number Diff line number Diff line change
@@ -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'