From 67ed7a1e0085df232c11b3705bc0c2acac9d5845 Mon Sep 17 00:00:00 2001
From: Andreas Loeffler <73336148+andreas-loeffler@users.noreply.github.com>
Date: Sun, 15 Feb 2026 06:32:23 +0800
Subject: [PATCH] add functionality to enable color logging for ros2 launch
files
Signed-off-by: Andreas Loeffler <73336148+andreas-loeffler@users.noreply.github.com>
---
launch/launch/logging/__init__.py | 33 +++++++-
launch/package.xml | 1 +
launch/test/launch/test_logging.py | 123 +++++++++++++++++++++++++++++
3 files changed, 154 insertions(+), 3 deletions(-)
diff --git a/launch/launch/logging/__init__.py b/launch/launch/logging/__init__.py
index bc058a161..1505e15f7 100644
--- a/launch/launch/logging/__init__.py
+++ b/launch/launch/logging/__init__.py
@@ -25,6 +25,8 @@
from typing import (Any, Dict, List, Literal, Optional, Protocol, Set, Tuple,
Union)
+import colorlog
+
from typing_extensions import TypeAlias
from . import handlers
@@ -126,6 +128,27 @@ def reset(self) -> None:
self.set_screen_format('default')
self.set_log_format('default')
+ def _logging_colors_enabled(self) -> bool:
+ """
+ Determine if colored output should be used for console logging.
+
+ Colors are enabled when:
+ - RCUTILS_COLORIZED_OUTPUT=1 is explicitly set, OR
+ - RCUTILS_COLORIZED_OUTPUT is unset AND stdout is a TTY
+
+ Colors are disabled when:
+ - RCUTILS_COLORIZED_OUTPUT=0 is explicitly set
+
+ :return: True if colors should be used, False otherwise
+ """
+ colorized_output = os.environ.get('RCUTILS_COLORIZED_OUTPUT')
+ if colorized_output == '0':
+ return False
+ elif colorized_output == '1':
+ return True
+ else:
+ return sys.stdout.isatty()
+
@property
def level(self) -> int:
return logging.root.getEffectiveLevel()
@@ -262,9 +285,13 @@ def set_screen_format(self, screen_format: str, *,
)
if screen_style is None:
screen_style = '{'
- self.screen_formatter = logging.Formatter(
- screen_format, style=screen_style
- )
+ if self._logging_colors_enabled():
+ self.screen_formatter = colorlog.ColoredFormatter(
+ '{log_color}' + screen_format,
+ style=screen_style
+ )
+ else:
+ self.screen_formatter = logging.Formatter(screen_format, style=screen_style)
if self.screen_handler is not None:
self.screen_handler.setFormatter(self.screen_formatter)
else:
diff --git a/launch/package.xml b/launch/package.xml
index 0c8ee9211..0dc8fcf72 100644
--- a/launch/package.xml
+++ b/launch/package.xml
@@ -25,6 +25,7 @@
python3-osrf-pycommon
python3-yaml
python3-typing-extensions
+ python3-colorlog
ament_copyright
ament_flake8
diff --git a/launch/test/launch/test_logging.py b/launch/test/launch/test_logging.py
index 62ecebae9..75c8b1a1e 100644
--- a/launch/test/launch/test_logging.py
+++ b/launch/test/launch/test_logging.py
@@ -25,6 +25,24 @@
import pytest
+def contains_ansicode(str_to_check: str) -> bool:
+ """
+ Check if given string contains an ANSI code sequence.
+
+ :param str_to_check: Given string to check for ANSI codes.
+ :return: True if the given string contains any ANSI code sequence,
+ False otherwise.
+ """
+ ansi_regex = re.compile(r"""
+ \x1b # literal ESC
+ \[ # literal [
+ [;\d]* # zero or more digits or semicolons
+ [A-Za-z] # a letter
+ """, re.VERBOSE)
+
+ return bool(ansi_regex.search(str_to_check))
+
+
@pytest.fixture
def log_dir(tmpdir_factory):
"""Test fixture that generates a temporary directory for log files."""
@@ -97,6 +115,111 @@ def test_output_loggers_bad_configuration(log_dir):
]
+def test_colorized_output_with_tty(capsys, monkeypatch, mock_clean_env):
+ """
+ Test color output with TTY.
+
+ Colors should be enabled when stdout is set to a TTY and
+ RCUTILS_COLORIZED_OUTPUT is unset.
+ """
+ monkeypatch.setattr('sys.stdout.isatty', lambda: True)
+ monkeypatch.delenv('RCUTILS_COLORIZED_OUTPUT', raising=False)
+
+ launch.logging.reset()
+ logger = launch.logging.get_logger()
+ logger.setLevel(logging.INFO)
+ logger.info('Test message with TTY')
+
+ capture = capsys.readouterr()
+ assert contains_ansicode(capture.out), \
+ 'Expected ANSI color codes in output with TTY'
+
+
+def test_colorized_output_disabled_by_env(capsys, monkeypatch, mock_clean_env):
+ """
+ Test color output disabled by env var.
+
+ Colors should be disabled when stdout is a TTY but
+ RCUTILS_COLORIZED_OUTPUT=0.
+ """
+ monkeypatch.setattr('sys.stdout.isatty', lambda: True)
+ monkeypatch.setenv('RCUTILS_COLORIZED_OUTPUT', '0')
+
+ launch.logging.reset()
+ logger = launch.logging.get_logger()
+ logger.setLevel(logging.INFO)
+ logger.info('Test message with colors disabled')
+
+ capture = capsys.readouterr()
+ assert not contains_ansicode(capture.out), \
+ 'Expected no ANSI color codes when disabled by env var'
+
+
+def test_colorized_output_forced_by_env(capsys, monkeypatch, mock_clean_env):
+ """
+ Test color output forced by env var in non TTY.
+
+ Colors should be forced on when RCUTILS_COLORIZED_OUTPUT=1
+ even without TTY.
+ """
+ monkeypatch.setattr('sys.stdout.isatty', lambda: False)
+ monkeypatch.setenv('RCUTILS_COLORIZED_OUTPUT', '1')
+
+ launch.logging.reset()
+ logger = launch.logging.get_logger()
+ logger.setLevel(logging.INFO)
+ logger.info('Test message with forced colors')
+
+ capture = capsys.readouterr()
+ assert contains_ansicode(capture.out), \
+ 'Expected ANSI color codes when forced by env var'
+
+
+def test_file_logs_not_colorized(log_dir, monkeypatch, mock_clean_env):
+ """
+ Test that file logs are not colorized.
+
+ File logs should never contain ANSI color codes even if
+ RCUTILS_COLORIZED_OUTPUT=1.
+ """
+ monkeypatch.setenv('RCUTILS_COLORIZED_OUTPUT', '1')
+
+ launch.logging.reset()
+ launch.logging.launch_config.log_dir = log_dir
+ logger = launch.logging.get_logger()
+ logger.setLevel(logging.INFO)
+ logger.info('Test message for file logging')
+
+ log_file = pathlib.Path(log_dir) / 'launch.log'
+ assert log_file.exists(), 'Log file should exist'
+ file_contents = log_file.read_text()
+
+ assert not contains_ansicode(file_contents), \
+ 'File logs should never contain ANSI color codes'
+ assert 'Test message for file logging' in file_contents, \
+ 'Message should be in log file'
+
+
+def test_colorized_output_no_tty(capsys, monkeypatch, mock_clean_env):
+ """
+ Test color output without a TTY and no env var.
+
+ Colors should be disabled when stdout is not a TTY and
+ RCUTILS_COLORIZED_OUTPUT is unset.
+ """
+ monkeypatch.setattr('sys.stdout.isatty', lambda: False)
+ monkeypatch.delenv('RCUTILS_COLORIZED_OUTPUT', raising=False)
+
+ launch.logging.reset()
+ logger = launch.logging.get_logger()
+ logger.setLevel(logging.INFO)
+ logger.info('Test message without TTY')
+
+ capture = capsys.readouterr()
+ assert not contains_ansicode(capture.out), \
+ 'Expected no ANSI color codes without TTY'
+
+
@pytest.mark.parametrize('config,checks,main_log_file_name', params)
def test_output_loggers_configuration(
capsys, log_dir, config, checks, main_log_file_name, mock_clean_env