Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ add_library(gateway_ros2 STATIC
src/ros2/transports/ros2_topic_subscription_transport.cpp
src/ros2/transports/ros2_topic_transport.cpp
src/ros2/status/ros2_lifecycle_state_reader.cpp
src/ros2/ros2_log_sink.cpp
src/ros2/trigger_topic_subscriber.cpp
src/default_script_provider.cpp
src/discovery/discovery_manager.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
#include <memory>
#include <nlohmann/json.hpp>
#include <optional>
#include <rclcpp/logger.hpp>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <vector>

#include "ros2_medkit_gateway/core/aggregation/classification.hpp"
#include "ros2_medkit_gateway/core/aggregation/peer_client.hpp"
#include "ros2_medkit_gateway/core/logging.hpp"
#include "ros2_medkit_gateway/dto/aggregation.hpp"

namespace ros2_medkit_gateway {
Expand Down Expand Up @@ -174,7 +174,7 @@ class AggregationManager {
* @param config Aggregation configuration
* @param logger Optional logger for TLS warnings (pass nullptr to suppress)
*/
explicit AggregationManager(const AggregationConfig & config, rclcpp::Logger * logger = nullptr);
explicit AggregationManager(const AggregationConfig & config, const log::Logger * logger = nullptr);

/**
* @brief Cancel every in-flight HTTP call to every peer.
Expand Down Expand Up @@ -258,7 +258,7 @@ class AggregationManager {
MergedPeerResult
fetch_and_merge_peer_entities(const std::vector<Area> & local_areas, const std::vector<Component> & local_components,
const std::vector<App> & local_apps, const std::vector<Function> & local_functions,
size_t max_entities_per_peer = 10000, rclcpp::Logger * logger = nullptr);
size_t max_entities_per_peer = 10000, const log::Logger * logger = nullptr);

/**
* @brief Update the routing table (entity_id -> peer_name)
Expand Down Expand Up @@ -405,7 +405,7 @@ class AggregationManager {

private:
AggregationConfig config_;
rclcpp::Logger logger_;
log::Logger logger_;
size_t static_peer_count_{0}; ///< Number of statically configured peers (not subject to max_discovered_peers)
/// Whether a peer whose picture could not be refreshed can still be reached.
///
Expand Down
113 changes: 113 additions & 0 deletions src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/logging.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2026 bburda
//
// 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.

#pragma once

#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>

namespace ros2_medkit_gateway::log {

/// Severity values match the rcutils severities.
enum class Level { kDebug = 10, kInfo = 20, kWarn = 30, kError = 40, kFatal = 50 };

/// Call site of a log line.
struct Location {
const char * file;
const char * function;
int line;
};

/// Destination of all log lines. Must be thread-safe.
class Sink {
public:
Sink() = default;
Sink(const Sink &) = delete;
Sink & operator=(const Sink &) = delete;
Sink(Sink &&) = delete;
Sink & operator=(Sink &&) = delete;
virtual ~Sink() = default;

virtual bool enabled(const std::string & logger, Level level) const = 0;
virtual void write(const std::string & logger, Level level, std::string_view message, const Location & location) = 0;
};

/// Replaces the active sink. Call it once at startup, before other threads log.
/// A null sink restores the default sink, which writes "[LEVEL] [name]: message"
/// to stderr for INFO and above. Every installed sink stays alive until the process
/// exits, because a thread that is logging may still use the sink it read before the call.
void set_sink(std::shared_ptr<Sink> sink);

/// A named log source. Cheap to copy.
class Logger {
public:
explicit Logger(std::string name);
const std::string & name() const {
return name_;
}
bool enabled(Level level) const;

private:
std::string name_;
};

namespace detail {
/// Process-wide sink slot. Default visibility, so a plugin that links this code
/// binds to the executable's slot.
struct SinkSlot {
SinkSlot();
SinkSlot(const SinkSlot &) = delete;
SinkSlot & operator=(const SinkSlot &) = delete;
SinkSlot(SinkSlot &&) = delete;
SinkSlot & operator=(SinkSlot &&) = delete;
~SinkSlot() = default;

/// Makes @p sink the active sink and keeps it alive for the process.
void install(std::shared_ptr<Sink> sink);

/// The sink the log calls use. Never null.
std::atomic<Sink *> active{nullptr};

private:
std::mutex mutex_;
std::vector<std::shared_ptr<Sink>> installed_;
};

__attribute__((visibility("default"))) SinkSlot & sink_slot();

void emit(const Logger & logger, Level level, const Location & location, const char * fmt, ...)
__attribute__((format(printf, 4, 5)));
} // namespace detail

} // namespace ros2_medkit_gateway::log

// The level is checked before the arguments are evaluated or formatted.
#define MEDKIT_LOG_AT(logger, level, ...) \
do { \
const auto & medkit_log_ref_ = (logger); \
if (medkit_log_ref_.enabled(level)) { \
::ros2_medkit_gateway::log::detail::emit( \
medkit_log_ref_, level, ::ros2_medkit_gateway::log::Location{__FILE__, __func__, __LINE__}, __VA_ARGS__); \
} \
} while (0)

#define MEDKIT_LOG_DEBUG(logger, ...) MEDKIT_LOG_AT(logger, ::ros2_medkit_gateway::log::Level::kDebug, __VA_ARGS__)
#define MEDKIT_LOG_INFO(logger, ...) MEDKIT_LOG_AT(logger, ::ros2_medkit_gateway::log::Level::kInfo, __VA_ARGS__)
#define MEDKIT_LOG_WARN(logger, ...) MEDKIT_LOG_AT(logger, ::ros2_medkit_gateway::log::Level::kWarn, __VA_ARGS__)
#define MEDKIT_LOG_ERROR(logger, ...) MEDKIT_LOG_AT(logger, ::ros2_medkit_gateway::log::Level::kError, __VA_ARGS__)
#define MEDKIT_LOG_FATAL(logger, ...) MEDKIT_LOG_AT(logger, ::ros2_medkit_gateway::log::Level::kFatal, __VA_ARGS__)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
#include <iomanip>
#include <memory>
#include <nlohmann/json.hpp>
#include <rclcpp/rclcpp.hpp>
#include <set>
#include <sstream>
#include <string>
Expand All @@ -32,6 +31,7 @@
#include "ros2_medkit_gateway/core/config.hpp"
#include "ros2_medkit_gateway/core/http/error_codes.hpp"
#include "ros2_medkit_gateway/core/http/http_utils.hpp"
#include "ros2_medkit_gateway/core/logging.hpp"
#include "ros2_medkit_gateway/core/models/entity_capabilities.hpp"
#include "ros2_medkit_gateway/core/models/entity_types.hpp"
#include "ros2_medkit_gateway/core/models/error_info.hpp"
Expand Down Expand Up @@ -354,8 +354,9 @@ class HandlerContext {
/**
* @brief Get logger for handlers
*/
static rclcpp::Logger logger() {
return rclcpp::get_logger("rest_server");
static const log::Logger & logger() {
static const log::Logger instance("rest_server");
return instance;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2026 bburda
//
// 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.

#pragma once

#include <string>
#include <string_view>

#include "ros2_medkit_gateway/core/logging.hpp"

namespace ros2_medkit_gateway {

/// Forwards log lines to rcutils under the same logger name, so per-name level
/// control (`--log-level <name>:=debug`) and `/rosout` behave as with rclcpp.
class Ros2LogSink : public log::Sink {
public:
bool enabled(const std::string & logger, log::Level level) const override;
void write(const std::string & logger, log::Level level, std::string_view message,
const log::Location & location) override;
};

} // namespace ros2_medkit_gateway
49 changes: 25 additions & 24 deletions src/ros2_medkit_gateway/src/aggregation/aggregation_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

#include "ros2_medkit_gateway/aggregation/aggregation_manager.hpp"
#include "ros2_medkit_gateway/core/logging.hpp"

#include <algorithm>
#include <future>
Expand All @@ -24,8 +25,6 @@
#include <arpa/inet.h>
#include <netdb.h>

#include <rclcpp/logging.hpp>

#include "ros2_medkit_gateway/core/aggregation/entity_merger.hpp"
#include "ros2_medkit_gateway/core/http/error_codes.hpp"

Expand Down Expand Up @@ -189,8 +188,8 @@ bool is_valid_peer_url(const std::string & url) {

} // namespace

AggregationManager::AggregationManager(const AggregationConfig & config, rclcpp::Logger * logger)
: config_(config), logger_(logger ? *logger : rclcpp::get_logger("aggregation_manager")) {
AggregationManager::AggregationManager(const AggregationConfig & config, const log::Logger * logger)
: config_(config), logger_(logger ? *logger : log::Logger("aggregation_manager")) {
for (const auto & peer_cfg : config_.peers) {
// Validate scheme for static peers (http/https only).
// Unlike mDNS peers, loopback addresses are valid for static config
Expand All @@ -204,14 +203,15 @@ AggregationManager::AggregationManager(const AggregationConfig & config, rclcpp:
if (peer_cfg.url.rfind("http://", 0) == 0) {
if (config_.require_tls) {
if (logger) {
RCLCPP_ERROR(*logger, "Aggregation: skipping peer '%s' at %s - require_tls is enabled but URL uses http://",
peer_cfg.name.c_str(), peer_cfg.url.c_str());
MEDKIT_LOG_ERROR(*logger,
"Aggregation: skipping peer '%s' at %s - require_tls is enabled but URL uses http://",
peer_cfg.name.c_str(), peer_cfg.url.c_str());
}
continue;
}
if (logger) {
RCLCPP_WARN(*logger, "Aggregation: peer '%s' at %s uses cleartext HTTP - consider using HTTPS",
peer_cfg.name.c_str(), peer_cfg.url.c_str());
MEDKIT_LOG_WARN(*logger, "Aggregation: peer '%s' at %s uses cleartext HTTP - consider using HTTPS",
peer_cfg.name.c_str(), peer_cfg.url.c_str());
}
}

Expand Down Expand Up @@ -265,10 +265,10 @@ void AggregationManager::add_discovered_peer(const std::string & url, const std:
// Enforce max_discovered_peers limit (static peers do not count)
size_t discovered_count = peers_.size() - static_peer_count_;
if (discovered_count >= config_.max_discovered_peers) {
RCLCPP_WARN(logger_,
"Aggregation: max_discovered_peers limit (%zu) reached, "
"rejecting peer '%s' at %s",
config_.max_discovered_peers, name.c_str(), url.c_str());
MEDKIT_LOG_WARN(logger_,
"Aggregation: max_discovered_peers limit (%zu) reached, "
"rejecting peer '%s' at %s",
config_.max_discovered_peers, name.c_str(), url.c_str());
return;
}

Expand Down Expand Up @@ -424,7 +424,7 @@ PeerEntities AggregationManager::replay_declaration(const std::string & peer_nam
AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_entities(
const std::vector<Area> & local_areas, const std::vector<Component> & local_components,
const std::vector<App> & local_apps, const std::vector<Function> & local_functions, size_t max_entities_per_peer,
rclcpp::Logger * logger) {
const log::Logger * logger) {
MergedPeerResult merged;
merged.areas = local_areas;
merged.components = local_components;
Expand Down Expand Up @@ -535,7 +535,8 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en
// would outlive the cycle that failed and be replayed as the peer's own
// account of itself.
if (logger) {
RCLCPP_WARN(*logger, "Failed to fetch entities from peer '%s': %s", pfr.peer_name.c_str(), pfr.error.c_str());
MEDKIT_LOG_WARN(*logger, "Failed to fetch entities from peer '%s': %s", pfr.peer_name.c_str(),
pfr.error.c_str());
}
unread_peers.push_back({pfr.peer_name, pfr.reachability,
pfr.reachability == Reachability::kReachable ? "refresh incomplete" : "not answering"});
Expand All @@ -546,8 +547,8 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en
for (const auto & route : pfr.entities.absent_routes) {
routes += routes.empty() ? route : ", " + route;
}
RCLCPP_WARN(*logger, "Peer '%s' does not expose %s; aggregating without the members those routes carry",
pfr.peer_name.c_str(), routes.c_str());
MEDKIT_LOG_WARN(*logger, "Peer '%s' does not expose %s; aggregating without the members those routes carry",
pfr.peer_name.c_str(), routes.c_str());
}
remember_declaration(pfr.peer_name, pfr.entities);
to_merge.push_back(std::move(pfr));
Expand All @@ -565,11 +566,11 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en
const size_t count =
retained.areas.size() + retained.components.size() + retained.apps.size() + retained.functions.size();
if (peer.reachability == Reachability::kUnreachable) {
RCLCPP_INFO(*logger, "Peer '%s' %s; retaining %zu declared entities as unavailable", peer.name.c_str(),
peer.reason.c_str(), count);
MEDKIT_LOG_INFO(*logger, "Peer '%s' %s; retaining %zu declared entities as unavailable", peer.name.c_str(),
peer.reason.c_str(), count);
} else {
RCLCPP_WARN(*logger, "Peer '%s' %s; it is still reachable, so its %zu declared entities stand as last read",
peer.name.c_str(), peer.reason.c_str(), count);
MEDKIT_LOG_WARN(*logger, "Peer '%s' %s; it is still reachable, so its %zu declared entities stand as last read",
peer.name.c_str(), peer.reason.c_str(), count);
}
}
PeerFetchResult replayed;
Expand Down Expand Up @@ -626,7 +627,7 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en
// Log collision-prefixed entities to help operators diagnose naming conflicts
if (id.find(EntityMerger::SEPARATOR) != std::string::npos) {
if (logger) {
RCLCPP_WARN(*logger, "Entity ID collision: '%s' prefixed for peer '%s'", id.c_str(), name.c_str());
MEDKIT_LOG_WARN(*logger, "Entity ID collision: '%s' prefixed for peer '%s'", id.c_str(), name.c_str());
}
}
}
Expand Down Expand Up @@ -657,7 +658,7 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en

if (logger) {
for (const auto & msg : classified.malformed_parent_warnings) {
RCLCPP_WARN(*logger, "Aggregation: %s", msg.c_str());
MEDKIT_LOG_WARN(*logger, "Aggregation: %s", msg.c_str());
}
for (const auto & w : merged.leaf_warnings) {
std::string peers_str;
Expand All @@ -667,8 +668,8 @@ AggregationManager::MergedPeerResult AggregationManager::fetch_and_merge_peer_en
}
peers_str += w.peer_names[i];
}
RCLCPP_WARN(*logger, "Leaf Component '%s' announced by multiple peers: %s. Routing uses last-writer-wins.",
w.entity_id.c_str(), peers_str.c_str());
MEDKIT_LOG_WARN(*logger, "Leaf Component '%s' announced by multiple peers: %s. Routing uses last-writer-wins.",
w.entity_id.c_str(), peers_str.c_str());
}
}

Expand Down
Loading
Loading