Skip to content

Commit 8b3ee9d

Browse files
committed
fix(opcua,#386): replace std::cerr traces with RCLCPP_DEBUG_STREAM (bburda review)
Per bburda review on PR #387: env-var-gated ``std::cerr`` (Copilot fix round) was the wrong observability primitive. It bypasses the gateway's log-level controls (``--log-level opcua.client:=debug``, ``RCUTILS_LOGGING_USE_STDOUT``, /rosout aggregation) and floods raw container stderr instead of integrating with the rest of the plugin's log_info / log_warn plumbing. All 18 trace sites switched to ``RCLCPP_DEBUG_STREAM`` against named loggers - ``opcua.client`` (trampoline, add_event_monitored_item, createEvent result, call_method status), ``opcua.poller`` (on_event, captured EventId hex, state-machine inputs, dispatching action), and ``opcua.plugin`` (acknowledge_fault / confirm_fault EventId hex). Three places kept a manual pre-gate via per-logger ``debug_enabled()`` helpers because the trace builds an ``std::ostringstream`` whose construction RCLCPP_DEBUG_STREAM does NOT short-circuit (the macro constructs a stringstream unconditionally and only the underlying RCUTILS log emission is level-gated). Pre-gating keeps the per-event formatting cost off the hot path at INFO. The ``Level`` enum class is compared via ``static_cast<int>`` because it has no defined ``operator<=``. Removed the ``opcua_trace_enabled()`` env-var helpers and their duplicated definitions across all three .cpp files; the env-var ``ROS2_MEDKIT_OPCUA_TRACE`` is replaced by the standard ROS log-level flow. Operators now toggle traces with: ros2 launch ... --log-level opcua.client:=debug ros2 launch ... --log-level opcua.poller:=debug ros2 launch ... --log-level opcua.plugin:=debug The ``[opcua_poller WARN] ConditionRefresh rejected`` fallback (when PollerConfig.log_warn is not wired) now goes through ``RCLCPP_WARN`` instead of raw stderr, again routing the message through /rosout. CMakeLists.txt: ``test_opcua_client`` gains ``rclcpp`` as a target dependency because ``opcua_client.cpp`` now includes ``rclcpp/logging.hpp``. The unit tests do not call ``rclcpp::init`` so this is link-only. Local verify: 27/27 test_opcua_client + 27/27 test_alarm_state_machine + 32/32 test_node_map.
1 parent a2a2faa commit 8b3ee9d

4 files changed

Lines changed: 95 additions & 86 deletions

File tree

src/ros2_medkit_plugins/ros2_medkit_opcua/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,13 @@ if(BUILD_TESTING)
194194
target_include_directories(test_opcua_client PRIVATE
195195
${CMAKE_CURRENT_SOURCE_DIR}/include
196196
)
197+
# rclcpp needed because opcua_client.cpp uses RCLCPP_DEBUG_STREAM for the
198+
# per-event diagnostics (bburda review on PR #387). The unit tests only
199+
# call public methods that never touch the trace path under test, so the
200+
# dep is link-only - no rclcpp::init / Node spin-up in the suite.
197201
medkit_target_dependencies(test_opcua_client
198202
ros2_medkit_gateway
203+
rclcpp
199204
)
200205
target_link_libraries(test_opcua_client
201206
open62541pp::open62541pp

src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_client.cpp

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,39 @@
1616

1717
#include <algorithm>
1818
#include <atomic>
19-
#include <cstdlib>
2019
#include <cstring>
2120
#include <deque>
22-
#include <iostream>
21+
#include <sstream>
2322
#include <thread>
2423
#include <type_traits>
2524
#include <unordered_map>
2625
#include <utility>
2726

2827
#include <open62541/client_subscriptions.h>
2928
#include <open62541/types.h>
29+
#include <rclcpp/logging.hpp>
3030

3131
namespace ros2_medkit_gateway {
3232

33-
// Env-var gate for verbose per-event / per-method-call diagnostics.
34-
// Set ``ROS2_MEDKIT_OPCUA_TRACE=1`` to enable trampoline / EventId /
35-
// call_method stderr logging. Off by default to keep production logs sane
36-
// (Copilot review on PR #387: per-event std::cerr would flood under high
37-
// alarm rates and bypass the gateway's normal logging path). Resolved once
38-
// at process start so toggling requires a restart.
39-
inline bool opcua_trace_enabled() {
40-
static const bool enabled = []() {
41-
const char * v = std::getenv("ROS2_MEDKIT_OPCUA_TRACE");
42-
return v != nullptr && v[0] != '\0' && std::string(v) != "0";
43-
}();
44-
return enabled;
33+
// Per-event / per-method-call traces use a named logger so they integrate
34+
// with the gateway's normal log level controls (e.g. RCUTILS_LOGGING_USE_STDOUT,
35+
// ros2 launch --log-level opcua.client:=debug). Off at INFO by default; an
36+
// operator turns them on without rebuilding (bburda review on PR #387).
37+
namespace {
38+
inline rclcpp::Logger opcua_client_logger() {
39+
static auto logger = rclcpp::get_logger("opcua.client");
40+
return logger;
41+
}
42+
43+
// Pre-gate for traces whose stream-build cost is non-trivial (e.g. per-arg
44+
// loops). RCLCPP_DEBUG_STREAM constructs the std::stringstream
45+
// unconditionally, so for hot paths we check the effective level first.
46+
// ``Level`` is an enum class - cast to int for the ordered comparison.
47+
inline bool client_debug_enabled() {
48+
return static_cast<int>(opcua_client_logger().get_effective_level()) <=
49+
static_cast<int>(rclcpp::Logger::Level::Debug);
4550
}
51+
} // namespace
4652

4753
// Forward declaration - defined after Impl so the trampoline can call back into
4854
// the client. Static linkage keeps the symbol private to this translation unit.
@@ -614,15 +620,11 @@ UA_EventFilter make_event_filter(const std::vector<OpcuaClient::EventFieldSpec>
614620
// Defined at namespace scope so its address is a stable function pointer.
615621
static void on_event_trampoline_c(UA_Client * /*client*/, UA_UInt32 sub_id, void * /*sub_ctx*/, UA_UInt32 mon_id,
616622
void * mon_ctx, size_t n_fields, UA_Variant * fields) {
617-
if (opcua_trace_enabled()) {
618-
std::cerr << "[opcua_client] TRAMPOLINE FIRED sub=" << sub_id << " mon=" << mon_id << " n_fields=" << n_fields
619-
<< std::endl;
620-
}
623+
RCLCPP_DEBUG_STREAM(opcua_client_logger(),
624+
"TRAMPOLINE FIRED sub=" << sub_id << " mon=" << mon_id << " n_fields=" << n_fields);
621625
auto * ctx = static_cast<EventCallbackContext *>(mon_ctx);
622626
if (ctx == nullptr || ctx->owner == nullptr) {
623-
if (opcua_trace_enabled()) {
624-
std::cerr << "[opcua_client] TRAMPOLINE: ctx null" << std::endl;
625-
}
627+
RCLCPP_DEBUG(opcua_client_logger(), "TRAMPOLINE: ctx null");
626628
return;
627629
}
628630
// Stale callback from a defunct subscription - ctx is still valid (we only
@@ -729,13 +731,13 @@ uint32_t OpcuaClient::add_event_monitored_item(uint32_t subscription_id, const o
729731
item.requestedParameters.queueSize = 100;
730732
UA_ExtensionObject_setValueNoDelete(&item.requestedParameters.filter, &filter, &UA_TYPES[UA_TYPES_EVENTFILTER]);
731733

732-
// Trace-level diagnostic gated by ROS2_MEDKIT_OPCUA_TRACE so integration
733-
// test failures can surface the exact NodeId / select-clause count we
734-
// hand to the server, without flooding production stderr.
735-
if (opcua_trace_enabled()) {
736-
std::cerr << "[opcua_client] add_event_monitored_item: subId=" << subscription_id
737-
<< " nodeId=" << source_node.toString() << " selectClauses=" << (select_specs.size() + 3) << std::endl;
738-
}
734+
// Debug-level diagnostic so integration-test failures can surface the
735+
// exact NodeId / select-clause count we hand to the server. Quiet at
736+
// INFO; ``ros2 launch --log-level opcua.client:=debug`` (or env var
737+
// RCUTILS_CONSOLE_OUTPUT_FORMAT) re-enables it.
738+
RCLCPP_DEBUG_STREAM(opcua_client_logger(),
739+
"add_event_monitored_item: subId=" << subscription_id << " nodeId=" << source_node.toString()
740+
<< " selectClauses=" << (select_specs.size() + 3));
739741

740742
UA_MonitoredItemCreateResult result =
741743
UA_Client_MonitoredItems_createEvent(impl_->client.handle(), subscription_id, UA_TIMESTAMPSTORETURN_BOTH, item,
@@ -752,10 +754,8 @@ uint32_t OpcuaClient::add_event_monitored_item(uint32_t subscription_id, const o
752754
UA_MonitoredItemCreateRequest_clear(&item);
753755
UA_EventFilter_clear(&filter);
754756

755-
if (opcua_trace_enabled()) {
756-
std::cerr << "[opcua_client] createEvent result: status=" << UA_StatusCode_name(result.statusCode)
757-
<< " miId=" << result.monitoredItemId << std::endl;
758-
}
757+
RCLCPP_DEBUG_STREAM(opcua_client_logger(), "createEvent result: status=" << UA_StatusCode_name(result.statusCode)
758+
<< " miId=" << result.monitoredItemId);
759759

760760
if (result.statusCode != UA_STATUSCODE_GOOD) {
761761
UA_MonitoredItemCreateResult_clear(&result);
@@ -864,13 +864,16 @@ OpcuaClient::call_method(const opcua::NodeId & object_id, const opcua::NodeId &
864864
for (size_t i = 0; i < arg_results.size(); ++i) {
865865
arg_codes.push_back(arg_results[i].get());
866866
}
867-
if (opcua_trace_enabled()) {
868-
std::cerr << "[opcua_client] call_method object=" << object_id.toString() << " method=" << method_id.toString()
869-
<< " statusCode=" << UA_StatusCode_name(code);
867+
if (client_debug_enabled()) {
868+
// RCLCPP_DEBUG_STREAM constructs its std::stringstream unconditionally,
869+
// so build the per-arg suffix only when DEBUG is actually active.
870+
std::ostringstream args_oss;
870871
for (size_t i = 0; i < arg_codes.size(); ++i) {
871-
std::cerr << " arg" << i << "=" << UA_StatusCode_name(arg_codes[i]);
872+
args_oss << " arg" << i << "=" << UA_StatusCode_name(arg_codes[i]);
872873
}
873-
std::cerr << std::endl;
874+
RCLCPP_DEBUG_STREAM(opcua_client_logger(),
875+
"call_method object=" << object_id.toString() << " method=" << method_id.toString()
876+
<< " statusCode=" << UA_StatusCode_name(code) << args_oss.str());
874877
}
875878

876879
auto classified = classify_call_result(code, arg_codes);

src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_plugin.cpp

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,23 @@
2727
#include <cmath>
2828
#include <cstdio>
2929
#include <cstdlib>
30-
#include <iostream>
30+
#include <sstream>
3131

3232
namespace ros2_medkit_gateway {
3333

3434
namespace {
3535

36-
// Env-var gate for verbose per-event / per-method-call diagnostics.
37-
// See opcua_client.cpp for rationale (Copilot review on PR #387). Duplicated
38-
// to keep traces local to their dispatch sites; promoting to a public header
39-
// would expose an internal trace knob.
40-
inline bool opcua_trace_enabled() {
41-
static const bool enabled = []() {
42-
const char * v = std::getenv("ROS2_MEDKIT_OPCUA_TRACE");
43-
return v != nullptr && v[0] != '\0' && std::string(v) != "0";
44-
}();
45-
return enabled;
36+
// Named logger so per-operation traces respect ROS log level filtering
37+
// (bburda review on PR #387). Quiet at INFO; ``--log-level
38+
// opcua.plugin:=debug`` re-enables for diagnostics.
39+
inline rclcpp::Logger opcua_plugin_logger() {
40+
static auto logger = rclcpp::get_logger("opcua.plugin");
41+
return logger;
42+
}
43+
44+
inline bool plugin_debug_enabled() {
45+
return static_cast<int>(opcua_plugin_logger().get_effective_level()) <=
46+
static_cast<int>(rclcpp::Logger::Level::Debug);
4647
}
4748

4849
/// Parse a JSON "value" field, coerce to the node's declared data_type, and
@@ -923,16 +924,17 @@ OpcuaPlugin::execute_operation(const std::string & entity_id, const std::string
923924
args.push_back(opcua::Variant::fromScalar(runtime->latest_event_id));
924925
args.push_back(opcua::Variant::fromScalar(opcua::LocalizedText("", comment)));
925926

926-
if (opcua_trace_enabled()) {
927+
if (plugin_debug_enabled()) {
928+
std::ostringstream hex_oss;
927929
const auto * bytes = runtime->latest_event_id.data();
928-
std::cerr << "[opcua_plugin] " << operation_name << " EventId len=" << runtime->latest_event_id.length()
929-
<< " hex=";
930930
for (size_t i = 0; i < std::min<size_t>(runtime->latest_event_id.length(), 16); ++i) {
931931
char buf[3];
932932
std::snprintf(buf, sizeof(buf), "%02x", static_cast<unsigned>(bytes[i]) & 0xffu);
933-
std::cerr << buf;
933+
hex_oss << buf;
934934
}
935-
std::cerr << " conditionId=" << runtime->condition_id.toString() << std::endl;
935+
RCLCPP_DEBUG_STREAM(opcua_plugin_logger(), operation_name << " EventId len=" << runtime->latest_event_id.length()
936+
<< " hex=" << hex_oss.str()
937+
<< " conditionId=" << runtime->condition_id.toString());
936938
}
937939

938940
auto result = client_->call_method(runtime->condition_id, method_id, args);

src/ros2_medkit_plugins/ros2_medkit_opcua/src/opcua_poller.cpp

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,25 @@
1717
#include <algorithm>
1818
#include <cmath>
1919
#include <cstdio>
20-
#include <cstdlib>
21-
#include <iostream>
2220
#include <optional>
21+
#include <sstream>
2322
#include <stdexcept>
2423
#include <utility>
2524

2625
#include <open62541/types.h>
26+
#include <rclcpp/logging.hpp>
2727

2828
namespace ros2_medkit_gateway {
2929

3030
namespace {
31-
// See opcua_client.cpp for the canonical helper. Duplicated here so the
32-
// poller's per-event traces stay together with the dispatch they describe;
33-
// promoting to a public header would expose an internal trace knob.
34-
inline bool opcua_trace_enabled() {
35-
static const bool enabled = []() {
36-
const char * v = std::getenv("ROS2_MEDKIT_OPCUA_TRACE");
37-
return v != nullptr && v[0] != '\0' && std::string(v) != "0";
38-
}();
39-
return enabled;
31+
inline rclcpp::Logger opcua_poller_logger() {
32+
static auto logger = rclcpp::get_logger("opcua.poller");
33+
return logger;
34+
}
35+
36+
inline bool poller_debug_enabled() {
37+
return static_cast<int>(opcua_poller_logger().get_effective_level()) <=
38+
static_cast<int>(rclcpp::Logger::Level::Debug);
4039
}
4140
} // namespace
4241

@@ -299,7 +298,10 @@ void OpcuaPoller::condition_refresh() {
299298
if (config_.log_warn) {
300299
config_.log_warn(msg);
301300
} else {
302-
std::cerr << "[opcua_poller WARN] " << msg << std::endl;
301+
// Fallback when the plugin did not wire log_warn through PollerConfig
302+
// (e.g., direct unit tests). RCLCPP_WARN goes to /rosout instead of
303+
// raw stderr so the warn integrates with normal log filtering.
304+
RCLCPP_WARN(opcua_poller_logger(), "%s", msg.c_str());
303305
}
304306
condition_refresh_warned_ = true;
305307
}
@@ -314,10 +316,9 @@ void OpcuaPoller::condition_refresh() {
314316
void OpcuaPoller::on_event(const AlarmEventConfig & cfg, const std::vector<opcua::Variant> & values,
315317
const opcua::NodeId & /*source_node*/, const opcua::NodeId & event_type,
316318
const opcua::NodeId & condition_id) {
317-
if (opcua_trace_enabled()) {
318-
std::cerr << "[opcua_poller] on_event fault=" << cfg.fault_code << " event_type=" << event_type.toString()
319-
<< " condition=" << condition_id.toString() << " values=" << values.size() << std::endl;
320-
}
319+
RCLCPP_DEBUG_STREAM(opcua_poller_logger(),
320+
"on_event fault=" << cfg.fault_code << " event_type=" << event_type.toString()
321+
<< " condition=" << condition_id.toString() << " values=" << values.size());
321322
// Detect ConditionRefresh bracketing per Part 9 §5.5.7. The flag is for
322323
// diagnostics only; the state machine itself does not need to know
323324
// because RefreshStart / RefreshEnd notifications carry no condition
@@ -402,28 +403,28 @@ void OpcuaPoller::on_event(const AlarmEventConfig & cfg, const std::vector<opcua
402403
// Track the latest EventId for spec-compliant Acknowledge calls.
403404
if (values[kFieldEventId].isType<opcua::ByteString>()) {
404405
it->second.latest_event_id = values[kFieldEventId].getScalarCopy<opcua::ByteString>();
405-
if (opcua_trace_enabled()) {
406-
std::cerr << "[opcua_poller] captured EventId len=" << it->second.latest_event_id.length() << " hex=";
406+
if (poller_debug_enabled()) {
407+
std::ostringstream hex_oss;
407408
const auto * bytes = it->second.latest_event_id.data();
408409
for (size_t i = 0; i < std::min<size_t>(it->second.latest_event_id.length(), 16); ++i) {
409410
char buf[3];
410411
std::snprintf(buf, sizeof(buf), "%02x", static_cast<unsigned>(bytes[i]) & 0xffu);
411-
std::cerr << buf;
412+
hex_oss << buf;
412413
}
413-
std::cerr << std::endl;
414+
RCLCPP_DEBUG_STREAM(opcua_poller_logger(),
415+
"captured EventId len=" << it->second.latest_event_id.length() << " hex=" << hex_oss.str());
414416
}
415-
} else if (opcua_trace_enabled()) {
416-
std::cerr << "[opcua_poller] EventId field not a ByteString" << std::endl;
417+
} else {
418+
RCLCPP_DEBUG(opcua_poller_logger(), "EventId field not a ByteString");
417419
}
418420

419421
auto outcome = AlarmStateMachine::compute(prev_status, input);
420-
if (opcua_trace_enabled()) {
421-
std::cerr << "[opcua_poller] state machine: enabled=" << input.enabled_state << " active=" << input.active_state
422-
<< " acked=" << input.acked_state << " confirmed=" << input.confirmed_state
423-
<< " shelved=" << input.shelved << " branch=" << input.branch_id_present
424-
<< " prev=" << static_cast<int>(prev_status) << " action=" << static_cast<int>(outcome.action)
425-
<< std::endl;
426-
}
422+
RCLCPP_DEBUG_STREAM(opcua_poller_logger(),
423+
"state machine: enabled="
424+
<< input.enabled_state << " active=" << input.active_state << " acked=" << input.acked_state
425+
<< " confirmed=" << input.confirmed_state << " shelved=" << input.shelved
426+
<< " branch=" << input.branch_id_present << " prev=" << static_cast<int>(prev_status)
427+
<< " action=" << static_cast<int>(outcome.action));
427428
it->second.last_status = outcome.next_status;
428429
runtime_snapshot = it->second;
429430

@@ -456,10 +457,8 @@ void OpcuaPoller::on_event(const AlarmEventConfig & cfg, const std::vector<opcua
456457
std::lock_guard cb_lock(event_alarm_callback_mutex_);
457458
cb_copy = event_alarm_callback_;
458459
}
459-
if (opcua_trace_enabled()) {
460-
std::cerr << "[opcua_poller] dispatching action=" << static_cast<int>(delivery.action)
461-
<< " cb_set=" << (cb_copy ? 1 : 0) << std::endl;
462-
}
460+
RCLCPP_DEBUG_STREAM(opcua_poller_logger(),
461+
"dispatching action=" << static_cast<int>(delivery.action) << " cb_set=" << (cb_copy ? 1 : 0));
463462
if (cb_copy) {
464463
cb_copy(delivery);
465464
}

0 commit comments

Comments
 (0)