Skip to content
Open
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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
os_image: ubuntu:jammy
- ros_distro: rolling
os_image: ubuntu:noble
continue-on-error: ${{ matrix.ros_distro == 'rolling' }}
container:
image: ${{ matrix.os_image }}
timeout-minutes: 60
Expand Down Expand Up @@ -59,7 +58,13 @@ jobs:
fi
source /opt/ros/${{ matrix.ros_distro }}/setup.bash
rosdep update
rosdep install --from-paths src --ignore-src -y
# Linters (clang-tidy / clang-format) are gated to the Jazzy lint job
# in quality.yml, but multiple package.xml files still list them as
# <test_depend>. Their rosdep keys are not registered for noble
# (Rolling), which breaks rosdep install on that runner. Skip them
# on every distro this workflow targets - the linters never run here.
rosdep install --from-paths src --ignore-src -y \
--skip-keys "ament_cmake_clang_tidy ament_cmake_clang_format"

- name: Build packages
env:
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/opcua-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ jobs:
apt-get install -y ros-${{ matrix.ros_distro }}-test-msgs libyaml-cpp-dev libssl-dev
source /opt/ros/${{ matrix.ros_distro }}/setup.bash
rosdep update
# Only skip nav2_msgs because vda5050_agent declares it as a dep but
# the apt package is not available on all distros. We do NOT skip
# ament_cmake_clang_format or test_msgs here: upstream medkit
# packages (ros2_medkit_serialization, gateway) require them at
# configure time via find_package.
# Skip nav2_msgs (vda5050_agent declares it; not available on all
# distros) and the linter rosdep keys. The clang_format / clang_tidy
# keys are not registered for noble (Rolling), and the upstream
# medkit packages have already been switched to QUIET + FOUND for
# those find_package calls so the build degrades gracefully without
# the linter packages installed.
rosdep install --from-paths src --ignore-src -y \
--skip-keys='nav2_msgs'
--skip-keys='nav2_msgs ament_cmake_clang_format ament_cmake_clang_tidy'

- name: Build ros2_medkit_opcua (and upstream deps)
env:
Expand Down
11 changes: 9 additions & 2 deletions docs/api/rest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,8 @@ Logs Endpoints
--------------

Query and configure the /rosout ring buffer for an entity. Supported entity types:
**areas** (namespace prefix match), **components** (namespace prefix match), **apps** (exact FQN match),
**areas** (aggregated from hosted apps, namespace prefix fallback), **components** (aggregated from
hosted apps, namespace prefix fallback for manifest-only deployments), **apps** (exact FQN match),
and **functions** (aggregated from hosted apps).

.. note::
Expand All @@ -748,7 +749,13 @@ and **functions** (aggregated from hosted apps).
storage backend or take full ownership of the log pipeline (see plugin development docs).

``GET /api/v1/components/{id}/logs``
Query log entries for all nodes in the component namespace (prefix match).
Query log entries aggregated from the component's hosted apps. Resolves child apps via
the entity cache and queries each by exact FQN. Falls back to namespace prefix match only
when the component has no hosted apps but declares a non-empty namespace (manifest-only
deployments where the component groups topics rather than nodes). The response always
carries ``x-medkit.aggregation_level=component`` and ``aggregated=true``; the
``app_count`` and ``aggregation_sources`` fields are populated only when hosted-app
aggregation is active and are omitted under the namespace-prefix fallback.

``GET /api/v1/apps/{id}/logs``
Query log entries for the specific app node (exact match).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,17 @@ if(BUILD_TESTING)
)
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_clang_format REQUIRED)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
# ci.yml skips the ament_cmake_clang_format rosdep key on Humble + Rolling
# (linters only run in the Jazzy quality job), so QUIET + FOUND check
# avoids hard-failing on those runners.
find_package(ament_cmake_clang_format QUIET)
if(ament_cmake_clang_format_FOUND)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
endif()

find_package(ament_cmake_gtest REQUIRED)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,17 @@ if(BUILD_TESTING)
)
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_clang_format REQUIRED)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
# ci.yml skips the ament_cmake_clang_format rosdep key on Humble + Rolling
# (linters only run in the Jazzy quality job), so QUIET + FOUND check
# avoids hard-failing on those runners.
find_package(ament_cmake_clang_format QUIET)
if(ament_cmake_clang_format_FOUND)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
endif()

find_package(ament_cmake_gmock REQUIRED)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,17 @@ if(BUILD_TESTING)
)
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_clang_format REQUIRED)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
# ci.yml skips the ament_cmake_clang_format rosdep key on Humble + Rolling
# (linters only run in the Jazzy quality job), so QUIET + FOUND check
# avoids hard-failing on those runners.
find_package(ament_cmake_clang_format QUIET)
if(ament_cmake_clang_format_FOUND)
file(GLOB_RECURSE _format_files
"include/*.hpp" "src/*.cpp" "test/*.cpp"
)
ament_clang_format(${_format_files}
CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../../.clang-format")
endif()

find_package(ament_cmake_gtest REQUIRED)

Expand Down
28 changes: 17 additions & 11 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -327,17 +327,23 @@ if(BUILD_TESTING)
)
ament_copyright(EXCLUDE ${VENDORED_FILES})

# Configure clang-format to only check our source files (not vendored)
find_package(ament_cmake_clang_format REQUIRED)
file(GLOB_RECURSE _format_files
"include/*.h" "include/*.hpp"
"src/*.cpp" "src/*.h" "src/*.hpp"
"test/*.cpp" "test/*.h" "test/*.hpp"
)
list(FILTER _format_files EXCLUDE REGEX ".*/vendored/.*")
ament_clang_format(${_format_files}
CONFIG_FILE "${ament_cmake_clang_format_CONFIG_FILE}"
)
# Configure clang-format to only check our source files (not vendored).
# ament_cmake_clang_format is intentionally only installed in the Jazzy
# quality job (ci.yml skips its rosdep key on Humble + Rolling), so use
# QUIET + FOUND check instead of REQUIRED to gracefully degrade on the
# build-and-test runners without breaking the build.
find_package(ament_cmake_clang_format QUIET)
if(ament_cmake_clang_format_FOUND)
file(GLOB_RECURSE _format_files
"include/*.h" "include/*.hpp"
"src/*.cpp" "src/*.h" "src/*.hpp"
"test/*.cpp" "test/*.h" "test/*.hpp"
)
list(FILTER _format_files EXCLUDE REGEX ".*/vendored/.*")
ament_clang_format(${_format_files}
CONFIG_FILE "${ament_cmake_clang_format_CONFIG_FILE}"
)
endif()

ros2_medkit_clang_tidy(
HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/(include|src|test)/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <httplib.h>

#include <string>
#include <vector>

#include "ros2_medkit_gateway/http/handlers/handler_context.hpp"

Expand Down Expand Up @@ -117,12 +118,10 @@ class BulkDataHandlers {
/**
* @brief Get source filters for rosbag queries based on entity type.
*
* For apps/components/areas: returns the entity's FQN or namespace path.
* For functions: aggregates FQNs from all hosting apps (read-only
* aggregated view - upload/delete are blocked at the route level).
*
* @param entity Entity information
* @return Vector of source filter strings (empty if no valid filters)
* Thin instance wrapper that fetches the cache from ctx_ and delegates to
* detail::compute_bulkdata_source_filters. The pure logic (entity-type
* branching) is unit-tested via the free function instead of the member
* to keep the handler's public surface unchanged.
*/
std::vector<std::string> get_source_filters(const EntityInfo & entity) const;

Comment thread
bburda marked this conversation as resolved.
Expand All @@ -148,5 +147,33 @@ class BulkDataHandlers {
static std::string resolve_rosbag_file_path(const std::string & path);
};

namespace detail {

/**
* @brief Compute rosbag source filters for an entity based on its type.
*
* Pure helper that drives ``BulkDataHandlers::get_source_filters``. Lives in
* a ``detail`` namespace to signal "not part of the public API" while still
* being directly unit-testable without spinning up a ``GatewayNode``.
*
* - APP / AREA: returns the entity's FQN or namespace path (single filter).
* - FUNCTION: aggregates non-empty ``effective_fqn()`` values across all
* hosted apps (no fallback - functions are pure aggregated views).
* - COMPONENT: aggregates from hosted apps; falls back to FQN/namespace_path
* only when the component has no hosted apps (manifest deployments where
* the component groups topics rather than nodes). This avoids the
* synthetic-component bug where empty fqn + empty namespace_path produced
* zero source filters.
*
* @param cache Entity cache to resolve hosted apps in (used for FUNCTION /
* COMPONENT only)
* @param entity Entity information
* @return Vector of source filter strings (empty if no valid filters)
*/
std::vector<std::string> compute_bulkdata_source_filters(const ThreadSafeEntityCache & cache,
const EntityInfo & entity);

} // namespace detail

} // namespace handlers
} // namespace ros2_medkit_gateway
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
#include <string>
#include <tl/expected.hpp>

#include <vector>

#include "ros2_medkit_gateway/auth/auth_config.hpp"
#include "ros2_medkit_gateway/auth/auth_manager.hpp"
#include "ros2_medkit_gateway/config.hpp"
#include "ros2_medkit_gateway/http/error_codes.hpp"
#include "ros2_medkit_gateway/http/http_utils.hpp"
#include "ros2_medkit_gateway/models/entity_capabilities.hpp"
#include "ros2_medkit_gateway/models/entity_types.hpp"
#include "ros2_medkit_gateway/models/thread_safe_entity_cache.hpp"

namespace ros2_medkit_gateway {

Expand Down Expand Up @@ -316,6 +319,24 @@ class HandlerContext {
return rclcpp::get_logger("rest_server");
}

/**
* @brief Resolve a list of app IDs to their non-empty effective FQNs.
*
* Apps that are missing from the cache or that have an empty effective_fqn()
* are skipped silently. The returned vector preserves the input app_ids order
* (minus skipped entries) and may be empty.
*
* Used by log_handlers and bulkdata_handlers to aggregate per-component /
* per-function resource queries from the entity's hosted apps. Static + public
* to enable direct unit testing without standing up a full GatewayNode fixture.
*
* @param cache Entity cache to look up apps in
* @param app_ids App IDs to resolve
* @return Effective FQNs for the apps that resolved
*/
static std::vector<std::string> resolve_app_host_fqns(const ThreadSafeEntityCache & cache,
const std::vector<std::string> & app_ids);

private:
GatewayNode * node_;
CorsConfig cors_config_;
Expand Down
40 changes: 25 additions & 15 deletions src/ros2_medkit_gateway/src/http/handlers/bulkdata_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -539,31 +539,41 @@ std::string BulkDataHandlers::get_rosbag_mimetype(const std::string & format) {
}

std::vector<std::string> BulkDataHandlers::get_source_filters(const EntityInfo & entity) const {
return detail::compute_bulkdata_source_filters(ctx_.node()->get_thread_safe_cache(), entity);
}

namespace detail {

std::vector<std::string> compute_bulkdata_source_filters(const ThreadSafeEntityCache & cache,
const EntityInfo & entity) {
if (entity.type == EntityType::FUNCTION) {
// Functions aggregate rosbags from all hosting apps
const auto & cache = ctx_.node()->get_thread_safe_cache();
auto host_app_ids = cache.get_apps_for_function(entity.id);
std::vector<std::string> filters;
filters.reserve(host_app_ids.size());
for (const auto & app_id : host_app_ids) {
auto app = cache.get_app(app_id);
if (app) {
auto fqn = app->effective_fqn();
if (!fqn.empty()) {
filters.push_back(fqn);
}
}
// Functions are pure aggregated views over hosted apps - if no apps host the function,
// there is nothing to query. No fall-through to fqn/namespace_path.
return HandlerContext::resolve_app_host_fqns(cache, cache.get_apps_for_function(entity.id));
}

if (entity.type == EntityType::COMPONENT) {
// Synthetic / runtime-discovered components have an empty fqn / namespace_path,
// so the bare-fqn path used to silently return zero source filters and produce
// empty descriptor lists plus failed ownership checks on download. Resolve hosted
// apps first; manifest deployments where the component groups topics rather than
// nodes still need the namespace prefix path, so fall through if no apps host it.
auto filters = HandlerContext::resolve_app_host_fqns(cache, cache.get_apps_for_component(entity.id));
if (!filters.empty()) {
return filters;
}
return filters;
// fall through to fqn/namespace_path
}

// For other entity types, use FQN or namespace_path
// For other entity types and manifest-only components, use FQN or namespace_path
std::string filter = entity.fqn.empty() ? entity.namespace_path : entity.fqn;
if (filter.empty()) {
return {};
}
return {filter};
}

} // namespace detail

} // namespace handlers
} // namespace ros2_medkit_gateway
17 changes: 17 additions & 0 deletions src/ros2_medkit_gateway/src/http/handlers/handler_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,5 +346,22 @@ void HandlerContext::send_json(httplib::Response & res, const json & data) {
res.set_content(data.dump(2), "application/json");
}

std::vector<std::string> HandlerContext::resolve_app_host_fqns(const ThreadSafeEntityCache & cache,
const std::vector<std::string> & app_ids) {
std::vector<std::string> fqns;
fqns.reserve(app_ids.size());
for (const auto & app_id : app_ids) {
auto app = cache.get_app(app_id);
if (!app) {
continue;
}
auto fqn = app->effective_fqn();
if (!fqn.empty()) {
fqns.push_back(std::move(fqn));
}
}
return fqns;
}

} // namespace handlers
} // namespace ros2_medkit_gateway
Loading
Loading