Skip to content

Commit 2d7f8ff

Browse files
committed
feat(log_bridge): add /rosout to faults bridge
New ros2_medkit_log_bridge: subscribes /rosout and promotes WARN+/ERROR/ FATAL log entries to FaultManager faults, attributed to the originating node FQN so each fault associates with the runtime-discovered entity. Drop-in, no publisher code changes. Refs #420
1 parent 90b881f commit 2d7f8ff

10 files changed

Lines changed: 786 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2+
Changelog for package ros2_medkit_log_bridge
3+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4+
5+
Forthcoming
6+
-----------
7+
* Initial release: promote ``/rosout`` log entries (WARN/ERROR/FATAL) to
8+
FaultManager faults, attributed to the originating node via a per-source
9+
FaultReporter, with auto-generated stable fault codes.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2026 mfaferek93, bburda
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+
cmake_minimum_required(VERSION 3.8)
16+
project(ros2_medkit_log_bridge)
17+
18+
set(CMAKE_CXX_STANDARD 17)
19+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
20+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
21+
22+
find_package(ros2_medkit_cmake REQUIRED)
23+
include(ROS2MedkitCcache)
24+
include(ROS2MedkitSanitizers)
25+
include(ROS2MedkitLinting)
26+
include(ROS2MedkitWarnings)
27+
28+
option(ENABLE_COVERAGE "Enable code coverage reporting" OFF)
29+
if(ENABLE_COVERAGE)
30+
message(STATUS "Code coverage enabled")
31+
add_compile_options(--coverage -O0 -g)
32+
add_link_options(--coverage)
33+
endif()
34+
35+
find_package(ament_cmake REQUIRED)
36+
37+
include(ROS2MedkitCompat)
38+
39+
find_package(rclcpp REQUIRED)
40+
find_package(rcl_interfaces REQUIRED)
41+
find_package(ros2_medkit_msgs REQUIRED)
42+
find_package(ros2_medkit_fault_reporter REQUIRED)
43+
44+
# Library target (for testing)
45+
add_library(log_bridge_lib SHARED
46+
src/log_bridge_node.cpp
47+
)
48+
49+
target_include_directories(log_bridge_lib PUBLIC
50+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
51+
$<INSTALL_INTERFACE:include>
52+
)
53+
54+
medkit_target_dependencies(log_bridge_lib
55+
rclcpp
56+
rcl_interfaces
57+
ros2_medkit_msgs
58+
ros2_medkit_fault_reporter
59+
)
60+
61+
# Executable
62+
add_executable(log_bridge_node src/main.cpp)
63+
target_link_libraries(log_bridge_node log_bridge_lib)
64+
medkit_target_dependencies(log_bridge_node rclcpp)
65+
66+
install(TARGETS log_bridge_node
67+
DESTINATION lib/${PROJECT_NAME}
68+
)
69+
70+
install(TARGETS log_bridge_lib
71+
EXPORT export_${PROJECT_NAME}
72+
ARCHIVE DESTINATION lib
73+
LIBRARY DESTINATION lib
74+
RUNTIME DESTINATION bin
75+
)
76+
77+
install(DIRECTORY include/
78+
DESTINATION include
79+
)
80+
81+
install(DIRECTORY launch config
82+
DESTINATION share/${PROJECT_NAME}
83+
)
84+
85+
ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
86+
ament_export_dependencies(rclcpp rcl_interfaces ros2_medkit_msgs ros2_medkit_fault_reporter)
87+
88+
if(BUILD_TESTING)
89+
find_package(ament_lint_auto REQUIRED)
90+
find_package(ament_cmake_gtest REQUIRED)
91+
92+
set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format")
93+
list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint ament_cmake_clang_tidy)
94+
ament_lint_auto_find_test_dependencies()
95+
96+
ros2_medkit_clang_tidy()
97+
98+
include(ROS2MedkitTestDomain)
99+
medkit_init_test_domains(START 90 END 99)
100+
101+
ament_add_gtest(test_log_bridge test/test_log_bridge.cpp)
102+
target_link_libraries(test_log_bridge log_bridge_lib)
103+
medkit_target_dependencies(test_log_bridge rclcpp rcl_interfaces ros2_medkit_msgs)
104+
medkit_set_test_domain(test_log_bridge)
105+
106+
if(ENABLE_COVERAGE)
107+
target_compile_options(test_log_bridge PRIVATE --coverage -O0 -g)
108+
target_link_options(test_log_bridge PRIVATE --coverage)
109+
endif()
110+
111+
ros2_medkit_relax_vendor_warnings()
112+
endif()
113+
114+
ament_package()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# ros2_medkit_log_bridge
2+
3+
Drop-in bridge that promotes ROS 2 `/rosout` log entries to structured medkit
4+
faults, attributing each fault to the node that logged it. No changes to the
5+
user's nodes are required.
6+
7+
It is a compatibility adapter, the same category as
8+
`ros2_medkit_diagnostic_bridge`. Native `ros2_medkit_fault_reporter`
9+
instrumentation stays the canonical path for code you control; this bridge is
10+
the fallback for nodes that only log.
11+
12+
## What it does
13+
14+
Subscribes to `/rosout` (`rcl_interfaces/msg/Log`) and forwards entries at or
15+
above a severity floor to the FaultManager:
16+
17+
| Log level | medkit severity |
18+
|-----------|-----------------|
19+
| DEBUG (10) / INFO (20) | dropped |
20+
| WARN (30) | `SEVERITY_WARN` |
21+
| ERROR (40) | `SEVERITY_ERROR` |
22+
| FATAL (50) | `SEVERITY_CRITICAL` |
23+
24+
- `source_id` of each fault is the originating node (the `Log.name` field), via
25+
a per-node `FaultReporter`, so faults attribute correctly and each node gets
26+
its own local debounce.
27+
- `fault_code` is auto-generated as `<PREFIX>_<NODE>_<HASH>`, where the hash is
28+
taken over a normalized message template (numbers / hex / paths stripped) so
29+
the same logical message maps to the same code across occurrences.
30+
31+
## WARN as PREFAILED
32+
33+
The bridge forwards WARN immediately. Whether a WARN shows as `PREFAILED`
34+
(suspected, kept out of the confirmed-fault list) or `CONFIRMED` is decided by
35+
the FaultManager's `confirmation_threshold`, not the bridge. For the
36+
visible-but-quiet behaviour, launch the FaultManager with
37+
`confirmation_threshold:=-2` or lower (or an entity threshold for `LOG_*`
38+
codes). With the shipped default (`-1`), every WARN confirms on first
39+
occurrence.
40+
41+
## Hard limitations (by construction)
42+
43+
- Only sees logs that reach `/rosout` via rclcpp from a still-alive node.
44+
Console-only loggers (e.g. some Micro XRCE-DDS / non-rclcpp loggers) are
45+
invisible.
46+
- A node that crashes hard may not flush its final log to `/rosout`, so the
47+
terminating ERROR can be missed. Process-death detection belongs to a
48+
separate liveliness bridge, not here.
49+
50+
## Run it
51+
52+
```bash
53+
# next to an existing stack + the medkit gateway/fault_manager
54+
ros2 launch ros2_medkit_log_bridge log_bridge.launch.py
55+
```
56+
57+
## Configuration (`config/log_bridge.yaml`)
58+
59+
| Param | Default | Meaning |
60+
|-------|---------|---------|
61+
| `rosout_topic` | `/rosout` | log topic to subscribe |
62+
| `severity_floor` | `30` (WARN) | minimum level promoted; raise to `40` on chatty / constrained targets |
63+
| `code_prefix` | `LOG` | prefix for generated fault codes |
64+
| `exclude_nodes` | `[]` | node-name substrings to skip |
65+
| `include_only_nodes` | `[]` | if set, only promote these nodes |
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
log_bridge:
2+
ros__parameters:
3+
# Topic carrying aggregated node logs.
4+
rosout_topic: "/rosout"
5+
# Minimum rcl_interfaces/msg/Log level promoted to a fault.
6+
# 10=DEBUG 20=INFO 30=WARN 40=ERROR 50=FATAL. Default WARN.
7+
# Raise to 40 (ERROR) on chatty / resource-constrained targets.
8+
severity_floor: 30
9+
# Prefix for auto-generated fault codes (<PREFIX>_<NODE>_<HASH>).
10+
code_prefix: "LOG"
11+
# Originating-node name substrings to skip (e.g. noisy debug nodes).
12+
exclude_nodes: []
13+
# If non-empty, ONLY promote logs from nodes matching these substrings.
14+
include_only_nodes: []
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2026 mfaferek93, bburda
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+
#pragma once
16+
17+
#include <map>
18+
#include <memory>
19+
#include <mutex>
20+
#include <string>
21+
#include <vector>
22+
23+
#include "rclcpp/rclcpp.hpp"
24+
#include "rcl_interfaces/msg/log.hpp"
25+
#include "ros2_medkit_fault_reporter/fault_reporter.hpp"
26+
27+
namespace ros2_medkit_log_bridge {
28+
29+
/// Bridge node that promotes ROS 2 /rosout log entries to FaultManager faults.
30+
///
31+
/// Subscribes to /rosout (rcl_interfaces/msg/Log) and forwards entries at or
32+
/// above a configurable severity floor to the FaultManager, attributing each
33+
/// fault to the originating node (the Log.name field) via a per-source
34+
/// FaultReporter. Drop-in compat adapter, same category as
35+
/// ros2_medkit_diagnostic_bridge: native FaultReporter instrumentation stays
36+
/// the canonical path; this bridge is the fallback for nodes that only log.
37+
/// Level mapping and the WARN-as-PREFAILED caveat are documented in README.md.
38+
///
39+
/// Hard limitation by construction: only sees rclcpp logs that reach /rosout
40+
/// from a still-alive node. Console-only loggers and crash-before-flush are out
41+
/// of reach.
42+
class LogBridgeNode : public rclcpp::Node {
43+
public:
44+
explicit LogBridgeNode(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
45+
46+
/// Map an rcl_interfaces/msg/Log level to a Fault severity.
47+
/// Returns false when the level is below the floor / not promotable.
48+
static bool map_level_to_severity(uint8_t log_level, uint8_t severity_floor, uint8_t * severity_out);
49+
50+
/// Auto-generate a stable fault code from the originating node name and the
51+
/// log message. Numbers/hex/paths in the message are normalized away so the
52+
/// same logical message maps to the same code across occurrences.
53+
/// Format: <PREFIX>_<NODE>_<HASH>, clamped to medkit's [A-Z0-9_] / 64-char rule.
54+
std::string generate_fault_code(const std::string & node_name, const std::string & message) const;
55+
56+
/// Normalize a log message into a stable template (lowercased, digit/hex/path
57+
/// runs stripped, whitespace collapsed). Exposed for unit testing.
58+
static std::string normalize_message(const std::string & message);
59+
60+
/// Whether a given originating node should be promoted, honouring the
61+
/// include/exclude lists. Exposed for unit testing.
62+
bool node_is_eligible(const std::string & node_name) const;
63+
64+
/// Map an rcl_interfaces/msg/Log.name (a logger name, e.g. "bt_navigator" or
65+
/// "controller_manager.resource_manager") to the originating node's
66+
/// fully-qualified name ("/bt_navigator", "/controller_manager"). The gateway
67+
/// discovers runtime entities by node FQN, so the fault's source_id must use
68+
/// the same form for faults (and their snapshots / rosbag) to associate with
69+
/// the entity in the SOVD tree. Exposed for unit testing.
70+
static std::string node_source_id(const std::string & log_name);
71+
72+
private:
73+
void log_callback(const rcl_interfaces::msg::Log::ConstSharedPtr & msg);
74+
75+
/// Fetch (or lazily create) the per-source FaultReporter for an originating
76+
/// node, so the fault's source_id is the node that logged, not the bridge.
77+
ros2_medkit_fault_reporter::FaultReporter * reporter_for(const std::string & node_name);
78+
79+
void load_parameters();
80+
81+
static std::string to_upper_snake(const std::string & in, size_t max_len);
82+
83+
rclcpp::Subscription<rcl_interfaces::msg::Log>::SharedPtr log_sub_;
84+
85+
// One FaultReporter per originating node (correct source_id + own LocalFilter).
86+
std::map<std::string, std::unique_ptr<ros2_medkit_fault_reporter::FaultReporter>> reporters_;
87+
std::mutex reporters_mutex_;
88+
89+
// Configuration
90+
std::string rosout_topic_;
91+
uint8_t severity_floor_;
92+
std::string code_prefix_;
93+
std::vector<std::string> exclude_nodes_;
94+
std::vector<std::string> include_only_nodes_;
95+
std::string own_node_name_;
96+
};
97+
98+
} // namespace ros2_medkit_log_bridge
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2026 mfaferek93, bburda
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+
17+
from ament_index_python.packages import get_package_share_directory
18+
from launch import LaunchDescription
19+
from launch_ros.actions import Node
20+
21+
22+
def generate_launch_description():
23+
config_file = os.path.join(
24+
get_package_share_directory('ros2_medkit_log_bridge'),
25+
'config',
26+
'log_bridge.yaml'
27+
)
28+
29+
return LaunchDescription([
30+
Node(
31+
package='ros2_medkit_log_bridge',
32+
executable='log_bridge_node',
33+
name='log_bridge',
34+
output='screen',
35+
parameters=[config_file],
36+
),
37+
])
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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>ros2_medkit_log_bridge</name>
5+
<version>0.5.0</version>
6+
<description>Bridge node promoting ROS2 /rosout log entries to FaultManager faults</description>
7+
8+
<maintainer email="michal.faferek@selfpatch.ai">mfaferek93</maintainer>
9+
<license>Apache-2.0</license>
10+
11+
<buildtool_depend>ament_cmake</buildtool_depend>
12+
<buildtool_depend>ros2_medkit_cmake</buildtool_depend>
13+
14+
<depend>rclcpp</depend>
15+
<depend>rcl_interfaces</depend>
16+
<depend>ros2_medkit_msgs</depend>
17+
<depend>ros2_medkit_fault_reporter</depend>
18+
19+
<test_depend>ament_lint_auto</test_depend>
20+
<test_depend>ament_lint_common</test_depend>
21+
<test_depend>ament_cmake_clang_format</test_depend>
22+
<test_depend>ament_cmake_clang_tidy</test_depend>
23+
<test_depend>ament_cmake_gtest</test_depend>
24+
<test_depend>launch_testing_ament_cmake</test_depend>
25+
<test_depend>launch_testing_ros</test_depend>
26+
<test_depend>ros2_medkit_fault_manager</test_depend>
27+
28+
<export>
29+
<build_type>ament_cmake</build_type>
30+
</export>
31+
</package>

0 commit comments

Comments
 (0)