Skip to content

Commit b7a4469

Browse files
author
Alex MacLean
committed
Adds {severity_with_color} as a log format option.
Signed-off-by: Alex MacLean <macleale@amazon.com>
1 parent 306fcfa commit b7a4469

File tree

5 files changed

+211
-9
lines changed

5 files changed

+211
-9
lines changed

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ if(BUILD_TESTING)
150150
TIMEOUT 10
151151
)
152152

153+
add_executable(test_logging_severity_with_color test/test_logging_severity_with_color.cpp)
154+
target_link_libraries(test_logging_severity_with_color ${PROJECT_NAME})
155+
add_launch_test(
156+
"test/test_logging_severity_with_color.py"
157+
TARGET test_logging_severity_with_color
158+
WORKING_DIRECTORY "$<TARGET_FILE_DIR:test_logging_severity_with_color>"
159+
TIMEOUT 10
160+
)
161+
153162
add_launch_test(
154163
"test/test_logging_output_format.py"
155164
TARGET test_logging_output_format

src/logging.c

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,12 @@ static const char * expand_file_name(
381381
return logging_output->buffer;
382382
}
383383

384+
// Forward declare expand_serverity_with_color to associate it with tokens.
385+
static const char * expand_severity_with_color(
386+
const logging_input_t * logging_input,
387+
rcutils_char_array_t * logging_output,
388+
size_t start_offset, size_t end_offset);
389+
384390
typedef struct token_map_entry_s
385391
{
386392
const char * token;
@@ -389,6 +395,7 @@ typedef struct token_map_entry_s
389395

390396
static const token_map_entry_t tokens[] = {
391397
{.token = "severity", .handler = expand_severity},
398+
{.token = "severity_with_color", .handler = expand_severity_with_color},
392399
{.token = "name", .handler = expand_name},
393400
{.token = "message", .handler = expand_message},
394401
{.token = "function_name", .handler = expand_function_name},
@@ -1407,13 +1414,24 @@ rcutils_ret_t rcutils_logging_format_message(
14071414
# define SET_STANDARD_COLOR_IN_STREAM(is_colorized, status)
14081415
#endif
14091416

1417+
static bool rcutils_logging_is_colorized()
1418+
{
1419+
if (g_colorized_output == RCUTILS_COLORIZED_OUTPUT_FORCE_ENABLE) {
1420+
return true;
1421+
}
1422+
if (g_colorized_output == RCUTILS_COLORIZED_OUTPUT_FORCE_DISABLE) {
1423+
return false;
1424+
}
1425+
return IS_STREAM_A_TTY(g_output_stream);
1426+
}
1427+
14101428
void rcutils_logging_console_output_handler(
14111429
const rcutils_log_location_t * location,
14121430
int severity, const char * name, rcutils_time_point_value_t timestamp,
14131431
const char * format, va_list * args)
14141432
{
14151433
rcutils_ret_t status = RCUTILS_RET_OK;
1416-
bool is_colorized = false;
1434+
bool is_colorized = rcutils_logging_is_colorized();
14171435

14181436
if (!g_rcutils_logging_initialized) {
14191437
RCUTILS_SAFE_FWRITE_TO_STDERR(
@@ -1434,14 +1452,6 @@ void rcutils_logging_console_output_handler(
14341452
return;
14351453
}
14361454

1437-
if (g_colorized_output == RCUTILS_COLORIZED_OUTPUT_FORCE_ENABLE) {
1438-
is_colorized = true;
1439-
} else if (g_colorized_output == RCUTILS_COLORIZED_OUTPUT_FORCE_DISABLE) {
1440-
is_colorized = false;
1441-
} else {
1442-
is_colorized = IS_STREAM_A_TTY(g_output_stream);
1443-
}
1444-
14451455
char msg_buf[1024] = "";
14461456
rcutils_char_array_t msg_array = {
14471457
.buffer = msg_buf,
@@ -1501,3 +1511,51 @@ void rcutils_logging_console_output_handler(
15011511
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to fini array.\n");
15021512
}
15031513
}
1514+
1515+
static const char * expand_severity_with_color(
1516+
const logging_input_t * logging_input,
1517+
rcutils_char_array_t * logging_output,
1518+
size_t start_offset, size_t end_offset)
1519+
{
1520+
(void)start_offset;
1521+
(void)end_offset;
1522+
1523+
if (rcutils_logging_is_colorized()) {
1524+
// The entire message is colorized, so we don't need to colorize the severity.
1525+
return expand_severity(logging_input, logging_output, start_offset, end_offset);
1526+
}
1527+
1528+
rcutils_ret_t status = RCUTILS_RET_OK;
1529+
1530+
SET_OUTPUT_COLOR_WITH_SEVERITY(status, logging_input->severity, *logging_output)
1531+
if (RCUTILS_RET_OK != status) {
1532+
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to set color on severity.\n");
1533+
return NULL;
1534+
}
1535+
1536+
const char * severity_string = g_rcutils_log_severity_names[logging_input->severity];
1537+
if (rcutils_char_array_strcat(logging_output, severity_string) != RCUTILS_RET_OK) {
1538+
RCUTILS_SAFE_FWRITE_TO_STDERR(rcutils_get_error_string().str);
1539+
rcutils_reset_error();
1540+
RCUTILS_SAFE_FWRITE_TO_STDERR("\n");
1541+
return NULL;
1542+
}
1543+
1544+
// If the severity is 4 characters long, add another space to line it up with the 5 character severities.
1545+
if (strlen(severity_string) == 4) {
1546+
if (rcutils_char_array_strcat(logging_output, " ") != RCUTILS_RET_OK) {
1547+
RCUTILS_SAFE_FWRITE_TO_STDERR(rcutils_get_error_string().str);
1548+
rcutils_reset_error();
1549+
RCUTILS_SAFE_FWRITE_TO_STDERR("\n");
1550+
return NULL;
1551+
}
1552+
}
1553+
1554+
SET_STANDARD_COLOR_IN_BUFFER(true, status, *logging_output)
1555+
if (RCUTILS_RET_OK != status) {
1556+
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to reset color after severity.\n");
1557+
return NULL;
1558+
}
1559+
1560+
return logging_output->buffer;
1561+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 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+
#include <iostream>
16+
17+
#include "rcutils/error_handling.h"
18+
#include "rcutils/logging.h"
19+
#include "rcutils/types/rcutils_ret.h"
20+
21+
int main(int, char **)
22+
{
23+
rcutils_ret_t ret = rcutils_logging_initialize();
24+
if (ret != RCUTILS_RET_OK) {
25+
fprintf(stderr, "error initializing logging: %s\n", rcutils_get_error_string().str);
26+
return -1;
27+
}
28+
29+
rcutils_ret_t status = rcutils_logging_set_logger_level("name", RCUTILS_LOG_SEVERITY_DEBUG);
30+
if (status != RCUTILS_RET_OK) {
31+
fprintf(stderr, "error setting logger level: %s\n", rcutils_get_error_string().str);
32+
return -1;
33+
}
34+
35+
// Log at all 5 severities to check the colorized severity in the log output.
36+
rcutils_log_location_t location = {"func", "file", 42u};
37+
rcutils_log(&location, RCUTILS_LOG_SEVERITY_DEBUG, "name", "Debug message");
38+
rcutils_log(&location, RCUTILS_LOG_SEVERITY_INFO, "name", "Info message");
39+
rcutils_log(&location, RCUTILS_LOG_SEVERITY_WARN, "name", "Warn message");
40+
rcutils_log(&location, RCUTILS_LOG_SEVERITY_ERROR, "name", "Error message");
41+
rcutils_log(&location, RCUTILS_LOG_SEVERITY_FATAL, "name", "Fatal message");
42+
43+
std::cout.flush();
44+
45+
return 0;
46+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
import os
16+
import unittest
17+
18+
from launch import LaunchDescription
19+
from launch.actions import ExecuteProcess
20+
from launch.actions import SetEnvironmentVariable
21+
22+
import launch_testing
23+
import launch_testing.actions
24+
import launch_testing.asserts
25+
import launch_testing.markers
26+
27+
28+
@launch_testing.markers.keep_alive
29+
def generate_test_description():
30+
test_process_name='test_logging_severity_with_color'
31+
launch_description = LaunchDescription()
32+
# Set the output format to a "verbose" format that is expected by the executable output
33+
launch_description.add_action(
34+
SetEnvironmentVariable(
35+
name='RCUTILS_CONSOLE_OUTPUT_FORMAT',
36+
value='[{severity_with_color}] [{name}]: {message} '
37+
'({function_name}() at {file_name}:{line_number})'
38+
)
39+
)
40+
launch_description.add_action(
41+
SetEnvironmentVariable(
42+
name='RCUTILS_COLORIZED_OUTPUT',
43+
value='0'
44+
)
45+
)
46+
executable = os.path.join(os.getcwd(), test_process_name)
47+
if os.name == 'nt':
48+
executable += '.exe'
49+
process_name = test_process_name
50+
launch_description.add_action(
51+
ExecuteProcess(cmd=[executable], name=process_name, output='screen')
52+
)
53+
54+
launch_description.add_action(
55+
launch_testing.actions.ReadyToTest()
56+
)
57+
return launch_description, {'process_name': process_name}
58+
59+
60+
class TestLoggingSeverityWithColor(unittest.TestCase):
61+
62+
def test_wait_for_shutdown(self, proc_info, proc_output, process_name):
63+
"""Wait for the process to complete so that the log messages will be available to inspect."""
64+
proc_info.assertWaitForShutdown(process=process_name, timeout=10)
65+
66+
67+
@launch_testing.post_shutdown_test()
68+
class TestLoggingSeverityWithColorAfterShutdown(unittest.TestCase):
69+
70+
def test_logging_output(self, proc_output, process_name):
71+
"""Test executable output against expectation."""
72+
launch_testing.asserts.assertInStderr(
73+
proc_output,
74+
expected_output=launch_testing.tools.expected_output_from_file(
75+
path=os.path.join(os.path.dirname(__file__), process_name),
76+
encoding='unicode_escape'
77+
),
78+
process=process_name,
79+
strip_ansi_escape_sequences=False
80+
)
81+
82+
def test_processes_exit_codes(self, proc_info):
83+
"""Test that all executables finished cleanly."""
84+
launch_testing.asserts.assertExitCodes(proc_info)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[\033[32mDEBUG\033[0m] [name]: Debug message (func() at file:42)
2+
[\033[0mINFO \033[0m] [name]: Info message (func() at file:42)
3+
[\033[33mWARN \033[0m] [name]: Warn message (func() at file:42)
4+
[\033[31mERROR\033[0m] [name]: Error message (func() at file:42)
5+
[\033[31mFATAL\033[0m] [name]: Fatal message (func() at file:42)

0 commit comments

Comments
 (0)