diff --git a/CMakeLists.txt b/CMakeLists.txt index 7499aba143..cdd2b514ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,6 +364,15 @@ if(ECAL_USE_PROTOBUF) add_subdirectory(app/app_pb) endif() +# -------------------------------------------------------- +# ecal tracing +# -------------------------------------------------------- +if(ECAL_BUILD_TESTS) + enable_testing() +endif() + +add_subdirectory(contrib/tracing) + # -------------------------------------------------------- # ecal core # -------------------------------------------------------- diff --git a/cmake/submodule_dependencies.cmake b/cmake/submodule_dependencies.cmake index 622a81b21c..c411c57239 100644 --- a/cmake/submodule_dependencies.cmake +++ b/cmake/submodule_dependencies.cmake @@ -15,6 +15,7 @@ set(ecal_submodule_dependencies HDF5 #libssh2 nanobind + nlohmann_json Protobuf protozero qwt diff --git a/conanfile.py b/conanfile.py index e0584304f6..a04a9f19bd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -17,6 +17,7 @@ def build_requirements(self): def requirements(self): self.requires("hdf5/1.10.6") self.requires("protobuf/3.17.1") + self.requires("nlohmann_json/3.11.2") self.requires("libcurl/7.78.0") self.requires("qt/5.15.2") self.requires("spdlog/1.9.2") @@ -51,6 +52,6 @@ def generate(self): else: tc.variables["Protobuf_PROTOC_EXECUTABLE"] = os.path.join(self.deps_cpp_info["protobuf"].rootpath, "bin", "protoc") tc.generate() - - - + + + diff --git a/contrib/tracing/CMakeLists.txt b/contrib/tracing/CMakeLists.txt new file mode 100644 index 0000000000..985001cc8a --- /dev/null +++ b/contrib/tracing/CMakeLists.txt @@ -0,0 +1,51 @@ +# ========================= eCAL LICENSE ================================= +# +# Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. +# +# 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. +# +# ========================= eCAL LICENSE ================================= + +project(tracing) + +find_package(nlohmann_json REQUIRED) + +add_library(${PROJECT_NAME}) +add_library(eCAL::tracing ALIAS ${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} + PUBLIC + include/ecal/tracing/types.h + include/ecal/tracing/writer.h + include/ecal/tracing/writer_jsonl.h + PRIVATE + src/writer_jsonl.cpp +) + +target_include_directories(${PROJECT_NAME} + PUBLIC + $ +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + nlohmann_json::nlohmann_json +) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) + +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER contrib/tracing) + +if(ECAL_BUILD_TESTS) + add_subdirectory(tests) +endif() \ No newline at end of file diff --git a/contrib/tracing/include/ecal/tracing/types.h b/contrib/tracing/include/ecal/tracing/types.h new file mode 100644 index 0000000000..5d0a60513c --- /dev/null +++ b/contrib/tracing/include/ecal/tracing/types.h @@ -0,0 +1,97 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include +#include +#include +#include + +namespace eCAL +{ + namespace tracing + { + constexpr const char* kTracingVersion = "1.0.0"; + + enum operation_type + { + send = 0, + receive = 1, + callback_execution = 2 + }; + + enum topic_direction + { + publisher = 0, + subscriber = 1 + }; + + enum eTracingLayerType : uint64_t + { + tl_trace_none = 0, + tl_trace_shm = 1 << 0, + tl_trace_udp = 1 << 1, + tl_trace_tcp = 1 << 2, + tl_trace_shm_udp = tl_trace_shm | tl_trace_udp, + tl_trace_shm_tcp = tl_trace_shm | tl_trace_tcp, + tl_trace_udp_tcp = tl_trace_udp | tl_trace_tcp, + tl_trace_all = tl_trace_shm | tl_trace_udp | tl_trace_tcp, + }; + + struct STopicMetadata + { + std::string tracing_version{kTracingVersion}; + uint64_t entity_id{ 0 }; + int32_t process_id{ 0 }; + std::string process_name; + std::string host_name; + std::string topic_name; + std::string encoding; + std::string type_name; + topic_direction direction{ topic_direction::publisher }; + }; + + struct SPublisherSpanData + { + operation_type op_type; + uint64_t entity_id; + size_t payload_size; + long long clock; + uint64_t layer; + long long start_ns; + long long end_ns; + }; + + struct SSubscriberSpanData + { + operation_type op_type; + uint64_t entity_id; + uint64_t topic_id; + size_t payload_size; + long long clock; + uint64_t layer; + long long start_ns; + long long end_ns; + }; + + using SpanData = std::variant; + using TraceInfo = std::variant; + } +} \ No newline at end of file diff --git a/contrib/tracing/include/ecal/tracing/writer.h b/contrib/tracing/include/ecal/tracing/writer.h new file mode 100644 index 0000000000..6f8ae6e964 --- /dev/null +++ b/contrib/tracing/include/ecal/tracing/writer.h @@ -0,0 +1,38 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include + +#include + +namespace eCAL +{ + namespace tracing + { + class TracingWriter + { + public: + virtual ~TracingWriter() = default; + + virtual void WriteTraceInfo(const std::vector& batch) = 0; + }; + } +} \ No newline at end of file diff --git a/contrib/tracing/include/ecal/tracing/writer_jsonl.h b/contrib/tracing/include/ecal/tracing/writer_jsonl.h new file mode 100644 index 0000000000..9d749bc4ed --- /dev/null +++ b/contrib/tracing/include/ecal/tracing/writer_jsonl.h @@ -0,0 +1,61 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace eCAL +{ + namespace tracing + { + class CTracingWriterJSONL : public TracingWriter + { + public: + CTracingWriterJSONL(const std::string& file_id, std::filesystem::path trace_directory); + ~CTracingWriterJSONL() override = default; + + CTracingWriterJSONL(const CTracingWriterJSONL&) = delete; + CTracingWriterJSONL& operator=(const CTracingWriterJSONL&) = delete; + CTracingWriterJSONL(CTracingWriterJSONL&&) = delete; + CTracingWriterJSONL& operator=(CTracingWriterJSONL&&) = delete; + + void WriteTraceInfo(const std::vector& batch) override; + + std::filesystem::path GetSpansFilePath() const; + std::filesystem::path GetTopicMetadataFilePath() const; + + private: + void WriteSpanData(const SpanData& span_data); + void WriteTopicMetadata(const STopicMetadata& metadata); + + std::filesystem::path spans_file_path_; + std::filesystem::path metadata_file_path_; + + std::ofstream spans_file_; + std::ofstream metadata_file_; + }; + } +} \ No newline at end of file diff --git a/contrib/tracing/src/writer_jsonl.cpp b/contrib/tracing/src/writer_jsonl.cpp new file mode 100644 index 0000000000..708ef99c34 --- /dev/null +++ b/contrib/tracing/src/writer_jsonl.cpp @@ -0,0 +1,170 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include + +#include + +#include +#include +#include +#include + +using json = nlohmann::json; + +namespace { + + template + struct Overloaded : Ts... { + using Ts::operator()...; + }; + + template + Overloaded(Ts...) -> Overloaded; + + using namespace eCAL::tracing; + + json toJson(const STopicMetadata& metadata) + { + json metadata_as_json; + metadata_as_json["tracing_version"] = metadata.tracing_version; + metadata_as_json["entity_id"] = metadata.entity_id; + metadata_as_json["process_id"] = metadata.process_id; + metadata_as_json["process_name"] = metadata.process_name; + metadata_as_json["host_name"] = metadata.host_name; + metadata_as_json["topic_name"] = metadata.topic_name; + metadata_as_json["encoding"] = metadata.encoding; + metadata_as_json["type_name"] = metadata.type_name; + metadata_as_json["direction"] = (metadata.direction == topic_direction::publisher) ? "publisher" : "subscriber"; + return metadata_as_json; + } + + json toJson(const SPublisherSpanData& span) + { + json span_as_json; + span_as_json["op_type"] = static_cast(span.op_type); + span_as_json["entity_id"] = span.entity_id; + span_as_json["payload_size"] = span.payload_size; + span_as_json["clock"] = span.clock; + span_as_json["layer"] = span.layer; + span_as_json["start_ns"] = span.start_ns; + span_as_json["end_ns"] = span.end_ns; + return span_as_json; + } + + json toJson(const SSubscriberSpanData& span) + { + json span_as_json; + span_as_json["op_type"] = static_cast(span.op_type); + span_as_json["entity_id"] = span.entity_id; + span_as_json["topic_id"] = span.topic_id; + span_as_json["payload_size"] = span.payload_size; + span_as_json["clock"] = span.clock; + span_as_json["layer"] = span.layer; + span_as_json["start_ns"] = span.start_ns; + span_as_json["end_ns"] = span.end_ns; + return span_as_json; + } + + std::ofstream CreateWithDirectory(const std::filesystem::path& file_path, std::ios_base::openmode flags) + { + std::filesystem::create_directories(file_path.parent_path()); + return std::ofstream(file_path, flags); + } +} + +namespace eCAL +{ + namespace tracing + { + CTracingWriterJSONL::CTracingWriterJSONL(const std::string& file_id, std::filesystem::path trace_directory) + : spans_file_path_(trace_directory / (std::string("ecal_spans_") + file_id + ".jsonl")) + , metadata_file_path_(trace_directory / (std::string("ecal_metadata_") + file_id + ".jsonl")) + , spans_file_(CreateWithDirectory(spans_file_path_, std::ios::out | std::ios::trunc)) + , metadata_file_(CreateWithDirectory(metadata_file_path_, std::ios::out | std::ios::trunc)) + { + } + + std::filesystem::path CTracingWriterJSONL::GetSpansFilePath() const + { + return spans_file_path_; + } + + std::filesystem::path CTracingWriterJSONL::GetTopicMetadataFilePath() const + { + return metadata_file_path_; + } + + void CTracingWriterJSONL::WriteTraceInfo(const std::vector& batch) + { + for (const auto& span_variant : batch) + { + std::visit( + Overloaded{ + [this](const SpanData& span) { WriteSpanData(span); }, + [this](const STopicMetadata& metadata) { WriteTopicMetadata(metadata); } + }, + span_variant + ); + } + + if (spans_file_.is_open()) + { + spans_file_.flush(); + } + + if (metadata_file_.is_open()) + { + metadata_file_.flush(); + } + } + + void CTracingWriterJSONL::WriteSpanData(const SpanData& span_data) + { + try + { + if (spans_file_.is_open()) + { + auto span_obj = std::visit([](const auto& span) { return toJson(span); }, span_data); + spans_file_ << span_obj.dump() << "\n"; + } + } + catch (const std::exception& e) + { + std::cerr << "Error writing spans to JSONL: " << e.what() << std::endl; + } + } + + void CTracingWriterJSONL::WriteTopicMetadata(const STopicMetadata& metadata) + { + try + { + if (metadata_file_.is_open()) + { + auto metadata_json = toJson(metadata); + metadata_file_ << metadata_json.dump() << "\n"; + } + } + catch (const std::exception& e) + { + std::cerr << "Error writing metadata to JSONL: " << e.what() << std::endl; + } + } + } +} \ No newline at end of file diff --git a/contrib/tracing/tests/CMakeLists.txt b/contrib/tracing/tests/CMakeLists.txt new file mode 100644 index 0000000000..0baf23d141 --- /dev/null +++ b/contrib/tracing/tests/CMakeLists.txt @@ -0,0 +1,49 @@ +# ========================= eCAL LICENSE ================================= +# +# Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. +# +# 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. +# +# ========================= eCAL LICENSE ================================= + +project(test_contrib_tracing) + +find_package(GTest REQUIRED) +find_package(nlohmann_json REQUIRED) + +set(tracing_test_src + src/tracing_test_helpers.h + src/tracing_writer_test.cpp +) + +ecal_add_gtest(${PROJECT_NAME} ${tracing_test_src}) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/src +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + eCAL::tracing + nlohmann_json::nlohmann_json +) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) + +ecal_install_gtest(${PROJECT_NAME}) + +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER tests/contrib/tracing) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${tracing_test_src} +) \ No newline at end of file diff --git a/contrib/tracing/tests/src/tracing_test_helpers.h b/contrib/tracing/tests/src/tracing_test_helpers.h new file mode 100644 index 0000000000..0f9f4ac2bf --- /dev/null +++ b/contrib/tracing/tests/src/tracing_test_helpers.h @@ -0,0 +1,69 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright (C) 2016 - 2025 Continental Corporation + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +class ScopedTestDirectory +{ +public: + explicit ScopedTestDirectory(const std::string& name) + : path_(std::filesystem::temp_directory_path() / name) + { + std::filesystem::remove_all(path_); + std::filesystem::create_directories(path_); + } + + ~ScopedTestDirectory() + { + std::filesystem::remove_all(path_); + } + + const std::filesystem::path& Path() const + { + return path_; + } + +private: + std::filesystem::path path_; +}; + +inline size_t CountAndValidateJsonlLines(const std::filesystem::path& filepath) +{ + std::ifstream file(filepath); + EXPECT_TRUE(file.is_open()) << "Failed to open: " << filepath; + + size_t count = 0; + std::string line; + while (std::getline(file, line)) + { + if (line.empty()) continue; + EXPECT_NO_THROW(nlohmann::json::parse(line)) + << "Invalid JSON on line " << (count + 1) << ": " << line; + ++count; + } + return count; +} \ No newline at end of file diff --git a/contrib/tracing/tests/src/tracing_writer_test.cpp b/contrib/tracing/tests/src/tracing_writer_test.cpp new file mode 100644 index 0000000000..d5d6441e8d --- /dev/null +++ b/contrib/tracing/tests/src/tracing_writer_test.cpp @@ -0,0 +1,178 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright (C) 2016 - 2025 Continental Corporation + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include "tracing_test_helpers.h" + +#include +#include + +#include + +#include +#include +#include + +#include + +TEST(TestTracingWriterJSONL, SpanWrites) +{ + constexpr size_t total_spans = 4000; + ScopedTestDirectory trace_directory("ecal_tracing_writer_span_writes"); + + std::filesystem::path spans_path; + + { + eCAL::tracing::CTracingWriterJSONL writer("span_writes", trace_directory.Path()); + spans_path = writer.GetSpansFilePath(); + + std::vector batch; + batch.reserve(total_spans); + + for (size_t i = 0; i < total_spans; ++i) + { + eCAL::tracing::SPublisherSpanData span{}; + span.op_type = eCAL::tracing::operation_type::send; + span.entity_id = static_cast(i); + span.payload_size = 64; + span.clock = static_cast(i); + span.layer = eCAL::tracing::tl_trace_shm; + span.start_ns = 1000; + span.end_ns = 2000; + batch.push_back(span); + } + + writer.WriteTraceInfo(batch); + } + + EXPECT_EQ(spans_path.filename().string(), "ecal_spans_span_writes.jsonl"); + EXPECT_EQ(CountAndValidateJsonlLines(spans_path), total_spans); +} + +TEST(TestTracingWriterJSONL, MetadataWrites) +{ + constexpr size_t metadata_instances = 200; + ScopedTestDirectory trace_directory("ecal_tracing_writer_metadata_writes"); + + std::filesystem::path metadata_path; + + { + eCAL::tracing::CTracingWriterJSONL writer("metadata_writes", trace_directory.Path()); + metadata_path = writer.GetTopicMetadataFilePath(); + + for (size_t i = 0; i < metadata_instances; ++i) + { + eCAL::tracing::STopicMetadata metadata{}; + metadata.entity_id = static_cast(i); + metadata.process_id = 1; + metadata.process_name = "my_process"; + metadata.host_name = "test_host"; + metadata.topic_name = "topic_" + std::to_string(i); + metadata.encoding = "protobuf"; + metadata.type_name = "TestType"; + metadata.direction = eCAL::tracing::topic_direction::publisher; + writer.WriteTraceInfo({metadata}); + } + } + + EXPECT_EQ(metadata_path.filename().string(), "ecal_metadata_metadata_writes.jsonl"); + EXPECT_EQ(CountAndValidateJsonlLines(metadata_path), metadata_instances); +} + +TEST(TestTracingWriterJSONL, PublisherSpanJsonFields) +{ + ScopedTestDirectory trace_directory("ecal_tracing_writer_publisher_fields"); + + std::filesystem::path spans_path; + + { + eCAL::tracing::CTracingWriterJSONL writer("publisher_fields", trace_directory.Path()); + spans_path = writer.GetSpansFilePath(); + + eCAL::tracing::SPublisherSpanData span{}; + span.op_type = eCAL::tracing::operation_type::send; + span.entity_id = 42; + span.payload_size = 256; + span.clock = 7; + span.layer = eCAL::tracing::tl_trace_udp; + span.start_ns = 1000; + span.end_ns = 2000; + + std::vector batch; + batch.push_back(span); + writer.WriteTraceInfo(batch); + } + + std::ifstream file(spans_path); + ASSERT_TRUE(file.is_open()); + + std::string line; + ASSERT_TRUE(std::getline(file, line)); + auto j = nlohmann::json::parse(line); + + EXPECT_EQ(j.at("op_type"), static_cast(eCAL::tracing::operation_type::send)); + EXPECT_EQ(j.at("entity_id"), 42u); + EXPECT_EQ(j.at("payload_size"), 256u); + EXPECT_EQ(j.at("clock"), 7); + EXPECT_EQ(j.at("layer"), static_cast(eCAL::tracing::tl_trace_udp)); + EXPECT_EQ(j.at("start_ns"), 1000); + EXPECT_EQ(j.at("end_ns"), 2000); +} + +TEST(TestTracingWriterJSONL, SubscriberSpanJsonFields) +{ + ScopedTestDirectory trace_directory("ecal_tracing_writer_subscriber_fields"); + + std::filesystem::path spans_path; + + { + eCAL::tracing::CTracingWriterJSONL writer("subscriber_fields", trace_directory.Path()); + spans_path = writer.GetSpansFilePath(); + + eCAL::tracing::SSubscriberSpanData span{}; + span.op_type = eCAL::tracing::operation_type::receive; + span.entity_id = 99; + span.topic_id = 55; + span.payload_size = 512; + span.clock = 3; + span.layer = eCAL::tracing::tl_trace_tcp; + span.start_ns = 3000; + span.end_ns = 4000; + + std::vector batch; + batch.push_back(span); + writer.WriteTraceInfo(batch); + } + + std::ifstream file(spans_path); + ASSERT_TRUE(file.is_open()); + + std::string line; + ASSERT_TRUE(std::getline(file, line)); + auto j = nlohmann::json::parse(line); + + EXPECT_EQ(j.at("op_type"), static_cast(eCAL::tracing::operation_type::receive)); + EXPECT_EQ(j.at("entity_id"), 99u); + EXPECT_EQ(j.at("topic_id"), 55u); + EXPECT_EQ(j.at("payload_size"), 512u); + EXPECT_EQ(j.at("clock"), 3); + EXPECT_EQ(j.at("layer"), static_cast(eCAL::tracing::tl_trace_tcp)); + EXPECT_EQ(j.at("start_ns"), 3000); + EXPECT_EQ(j.at("end_ns"), 4000); +} \ No newline at end of file diff --git a/ecal/core/CMakeLists.txt b/ecal/core/CMakeLists.txt index 9f31e3e087..a11b2c82a2 100644 --- a/ecal/core/CMakeLists.txt +++ b/ecal/core/CMakeLists.txt @@ -1,7 +1,7 @@ # ========================= eCAL LICENSE ================================= # # Copyright (C) 2016 - 2025 Continental Corporation -# Copyright 2025 AUMOVIO and subsidiaries. All rights reserved. +# Copyright 2025 - 2026 AUMOVIO and subsidiaries. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -457,6 +457,20 @@ if(ECAL_CORE_TIMEPLUGIN) ) endif() +###################################### +# tracing +###################################### +set(ecal_tracing_src + src/tracing/layer_type.cpp + src/tracing/layer_type.h + src/tracing/span.cpp + src/tracing/span.h + src/tracing/trace_provider.cpp + src/tracing/trace_provider.h + src/tracing/trace_provider_default.cpp + src/tracing/trace_provider_default.h +) + ###################################### # util ###################################### @@ -567,6 +581,7 @@ set(ecal_public_headers include/ecal/config/registration.h include/ecal/config/subscriber.h include/ecal/config/time.h + include/ecal/config/tracing.h include/ecal/config/transport_layer.h include/ecal/pubsub/subscriber.h include/ecal/pubsub/types.h @@ -629,6 +644,7 @@ set(ecal_sources ${ecal_serialization_src} ${ecal_service_src} ${ecal_time_src} + ${ecal_tracing_src} ${ecal_util_src} ${ecal_cmn_src} ${ecal_builder_src} @@ -721,6 +737,7 @@ target_link_libraries(ecal_core_private asio::asio Threads::Threads eCAL::ecal-utils + eCAL::tracing PRIVATE # we need this so that e.g. the test can just link ecal_core_private and will get the object files of this library. ecal_core_serialization $ diff --git a/ecal/core/include/ecal/config/configuration.h b/ecal/core/include/ecal/config/configuration.h index 77728641a5..437cf535db 100644 --- a/ecal/core/include/ecal/config/configuration.h +++ b/ecal/core/include/ecal/config/configuration.h @@ -30,6 +30,7 @@ #include #include #include +#include #include @@ -58,6 +59,7 @@ namespace eCAL Time::Configuration timesync; Application::Configuration application; Logging::Configuration logging; + Tracing::Configuration tracing; eCommunicationMode communication_mode { eCommunicationMode::local }; /*!< eCAL components communication mode: local: local host only communication (default) diff --git a/ecal/core/include/ecal/config/tracing.h b/ecal/core/include/ecal/config/tracing.h new file mode 100644 index 0000000000..7f2ec86aa1 --- /dev/null +++ b/ecal/core/include/ecal/config/tracing.h @@ -0,0 +1,38 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright (C) 2016 - 2025 Continental Corporation + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +/** + * @file config/tracing.h + * @brief eCAL tracing configuration +**/ + +#pragma once + +#include + +namespace eCAL +{ + namespace Tracing + { + struct Configuration + { + bool enabled { false }; //!< Enable tracing (Default: false) + }; + } +} diff --git a/ecal/core/include/ecal/util.h b/ecal/core/include/ecal/util.h index d957336326..1b5793ee6a 100644 --- a/ecal/core/include/ecal/util.h +++ b/ecal/core/include/ecal/util.h @@ -64,6 +64,24 @@ namespace eCAL **/ ECAL_API std::string GeteCALLogDir(); + /** + * @brief Returns the path to the eCAL trace directory. + * + * Searches in following order: + * 1. Environment variable ECAL_TRACE_DIR + * 2. Environment variable ECAL_DATA (also checking for traces subdirectory) + * 3. The path where ecal.yaml was loaded from (also checking for traces subdirectory) + * 4. The temporary directory (e.g. /tmp [unix], Appdata/local/Temp [win]) + * 5. Fallback path /ecal_tmp + * + * In case of 4/5, a unique temporary folder will be created. + * + * @returns The path to the eCAL trace directory. + * The subdirectory traces might not exist yet. + * Returns empty string if no root path could be found. + **/ + ECAL_API std::string GeteCALTraceDir(); + /** * @brief Send shutdown event to specified local user process using it's unit name. * diff --git a/ecal/core/src/config/configuration_to_yaml.cpp b/ecal/core/src/config/configuration_to_yaml.cpp index 58867f8e0b..de43de28f4 100644 --- a/ecal/core/src/config/configuration_to_yaml.cpp +++ b/ecal/core/src/config/configuration_to_yaml.cpp @@ -673,6 +673,28 @@ namespace YAML } + /* + ______ _ + /_ __/______ _____(_)__ ___ _ + / / / __/ _ `/ __/ / _ \/ _ `/ + /_/ /_/ \_,_/\__/_/_//_/\_, / + /___/ + */ + + Node convert::encode(const eCAL::Tracing::Configuration& config_) + { + Node node; + node["enabled"] = config_.enabled; + return node; + } + + bool convert::decode(const Node& node_, eCAL::Tracing::Configuration& config_) + { + AssignValue(config_.enabled, node_, "enabled"); + return true; + } + + /* __ ___ _ ____ __ _ / |/ /__ _(_)__ _______ ___ / _(_)__ ___ _________ _/ /_(_)__ ___ @@ -691,6 +713,7 @@ namespace YAML node["time"] = config_.timesync; node["application"] = config_.application; node["logging"] = config_.logging; + node["tracing"] = config_.tracing; node["communication_mode"] = config_.communication_mode == eCAL::eCommunicationMode::network ? "network" : "local"; return node; @@ -705,6 +728,7 @@ namespace YAML AssignValue(config_.timesync, node_, "time"); AssignValue(config_.application, node_, "application"); AssignValue(config_.logging, node_, "logging"); + AssignValue(config_.tracing, node_, "tracing"); std::string communication_mode; AssignValue(communication_mode, node_, "communication_mode"); diff --git a/ecal/core/src/config/configuration_to_yaml.h b/ecal/core/src/config/configuration_to_yaml.h index 012c3e2a0d..8ad27f25f2 100644 --- a/ecal/core/src/config/configuration_to_yaml.h +++ b/ecal/core/src/config/configuration_to_yaml.h @@ -343,6 +343,22 @@ namespace YAML }; + /* + ______ _ + /_ __/______ _____(_)__ ___ _ + / / / __/ _ `/ __/ / _ \/ _ `/ + /_/ /_/ \_,_/\__/_/_//_/\_, / + /___/ + */ + template<> + struct convert + { + static Node encode(const eCAL::Tracing::Configuration& config_); + + static bool decode(const Node& node_, eCAL::Tracing::Configuration& config_); + }; + + /* __ ___ _ ____ __ _ / |/ /__ _(_)__ _______ ___ / _(_)__ ___ _________ _/ /_(_)__ ___ diff --git a/ecal/core/src/config/default_configuration.cpp b/ecal/core/src/config/default_configuration.cpp index 02facd3b24..1e538dd624 100644 --- a/ecal/core/src/config/default_configuration.cpp +++ b/ecal/core/src/config/default_configuration.cpp @@ -381,6 +381,11 @@ namespace eCAL ss << R"( # UDP Port for sending logging data)" << "\n"; ss << R"( port: )" << config_.logging.receiver.udp_config.port << "\n"; ss << R"()" << "\n"; + ss << R"(# Tracing configuration)" << "\n"; + ss << R"(tracing:)" << "\n"; + ss << R"( # Enable tracing (Default: false))" << "\n"; + ss << R"( enabled: )" << config_.tracing.enabled << "\n"; + ss << R"()" << "\n"; return ss; } diff --git a/ecal/core/src/config/ecal_path_processing.cpp b/ecal/core/src/config/ecal_path_processing.cpp index 01ad66357d..ab1b4cd546 100644 --- a/ecal/core/src/config/ecal_path_processing.cpp +++ b/ecal/core/src/config/ecal_path_processing.cpp @@ -150,6 +150,31 @@ namespace #endif } + + std::string GeteCALOutputDirImpl(const eCAL::Util::IDirProvider& dir_provider_, const eCAL::Util::IDirManager& dir_manager_, const std::string& env_var_name_, const std::string& output_subdirectory_, const std::string& config_file_path_, const std::string& config_output_dir_) + { + const std::string config_file_dir = dir_manager_.getDirectoryPath(config_file_path_); + const std::string ecal_data_env_dir = dir_provider_.eCALEnvVar(ECAL_DATA_VAR); + + const std::vector output_paths = { + dir_provider_.eCALEnvVar(env_var_name_), + buildPath(ecal_data_env_dir, output_subdirectory_), + ecal_data_env_dir, + config_output_dir_, + buildPath(config_file_dir, output_subdirectory_), + config_file_dir + }; + + for (const auto& path : output_paths) + { + if (!path.empty() && dir_manager_.dirExists(path) && dir_manager_.canWriteToDirectory(path)) + { + return path; + } + } + + return dir_provider_.uniqueTmpDir(dir_manager_); + } } namespace eCAL @@ -181,7 +206,8 @@ namespace eCAL // create also logs directory const std::string log_path = buildPath(path_, ECAL_FOLDER_NAME_LOG); - return dirExistsOrCreate(log_path); + const std::string traces_path = buildPath(path_, ECAL_FOLDER_NAME_TRACE); + return dirExistsOrCreate(log_path) && dirExistsOrCreate(traces_path); } std::string DirManager::findFileInPaths(const std::vector& paths_, const std::string& file_name_) const @@ -315,27 +341,15 @@ namespace eCAL { std::string GeteCALLogDirImpl(const Util::IDirProvider& dir_provider_ /* = Util::DirProvider() */, const Util::IDirManager& dir_manager_ /* = Util::DirManager() */, const eCAL::Configuration& config_ /* = eCAL::GetConfiguration() */) { - const std::string config_file_dir = dir_manager_.getDirectoryPath(config_.GetConfigurationFilePath()); - const std::string ecal_data_env_dir = dir_provider_.eCALEnvVar(ECAL_DATA_VAR); - - const std::vector log_paths = { - dir_provider_.eCALEnvVar(ECAL_LOG_VAR), - buildPath(ecal_data_env_dir, ECAL_FOLDER_NAME_LOG), - ecal_data_env_dir, - config_.logging.provider.file_config.path, - buildPath(config_file_dir, ECAL_FOLDER_NAME_LOG), - config_file_dir - }; - - for (const auto& path : log_paths) - { - if (!path.empty() && dir_manager_.dirExists(path) && dir_manager_.canWriteToDirectory(path)) - return path; - } - // if no path is available, we create temp directories for logging // check now for a tmp directory and return - return dir_provider_.uniqueTmpDir(dir_manager_); + return GeteCALOutputDirImpl(dir_provider_, dir_manager_, ECAL_LOG_VAR, ECAL_FOLDER_NAME_LOG, config_.GetConfigurationFilePath(), config_.logging.provider.file_config.path); + } + + std::string GeteCALTraceDirImpl(const Util::IDirProvider& dir_provider_ /* = Util::DirProvider() */, const Util::IDirManager& dir_manager_ /* = Util::DirManager() */, const eCAL::Configuration& config_ /* = eCAL::GetConfiguration() */) + { + // if no path is available, we create temp directories for tracing + return GeteCALOutputDirImpl(dir_provider_, dir_manager_, ECAL_TRACE_VAR, ECAL_FOLDER_NAME_TRACE, config_.GetConfigurationFilePath(), {}); } std::string checkForValidConfigFilePath(const std::string& config_file_, const Util::DirProvider& dir_provider_ /* = Util::DirProvider() */, const Util::DirManager& dir_manager_ /* = Util::DirManager() */) diff --git a/ecal/core/src/config/ecal_path_processing.h b/ecal/core/src/config/ecal_path_processing.h index 75147618a0..a0143e4bbc 100644 --- a/ecal/core/src/config/ecal_path_processing.h +++ b/ecal/core/src/config/ecal_path_processing.h @@ -221,6 +221,24 @@ namespace eCAL */ std::string GeteCALLogDirImpl(const Util::IDirProvider& dir_provider_ = Util::DirProvider(), const Util::IDirManager& dir_manager_ = Util::DirManager(), const eCAL::Configuration& config_ = eCAL::GetConfiguration()); + /** + * @brief Returns the path to the eCAL trace directory. + * + * Searches in following order: + * 1. Environment variable ECAL_TRACE_DIR + * 2. Environment variable ECAL_DATA (also checking for traces subdirectory) + * 3. The path where ecal.yaml was loaded from (also checking for traces subdirectory) + * 4. The temporary directory (e.g. /tmp [unix], Appdata/local/Temp [win]) + * 5. Fallback path /ecal_tmp + * + * In case of 4/5, a unique temporary folder will be created. + * + * @returns The path to the eCAL trace directory. + * The subdirectory traces might not exist yet. + * Returns empty string if no root path could be found. + */ + std::string GeteCALTraceDirImpl(const Util::IDirProvider& dir_provider_ = Util::DirProvider(), const Util::IDirManager& dir_manager_ = Util::DirManager(), const eCAL::Configuration& config_ = eCAL::GetConfiguration()); + /** * @brief Returns the path to the eCAL data directory. Searches in following order: * diff --git a/ecal/core/src/ecal.cpp b/ecal/core/src/ecal.cpp index 6f661a9f48..b6c5611d68 100644 --- a/ecal/core/src/ecal.cpp +++ b/ecal/core/src/ecal.cpp @@ -114,6 +114,7 @@ namespace eCAL SetGlobalUnitName(unit_name_.c_str()); if ((components_ & Init::Logging) != 0u) InitializeLogging(config_); + InitializeTracing(config_); auto globals_instance = CreateGlobalsInstance(); if (!globals_instance) return false; @@ -164,6 +165,7 @@ namespace eCAL ResetGlobalEcalConfiguration(); + ResetTracing(); ResetLogging(); return finalized; diff --git a/ecal/core/src/ecal_def.h b/ecal/core/src/ecal_def.h index d77747bf7d..18c82aa6bb 100644 --- a/ecal/core/src/ecal_def.h +++ b/ecal/core/src/ecal_def.h @@ -35,6 +35,7 @@ constexpr const char* ECAL_FOLDER_NAME_WINDOWS = "eCAL"; constexpr const char* ECAL_FOLDER_NAME_LINUX = "ecal"; constexpr const char* ECAL_FOLDER_NAME_HOME_LINUX = ".ecal"; constexpr const char* ECAL_FOLDER_NAME_LOG = "logs"; +constexpr const char* ECAL_FOLDER_NAME_TRACE = "traces"; constexpr const char* ECAL_FOLDER_NAME_TMP_WINDOWS = "Temp"; #ifdef ECAL_OS_WINDOWS @@ -51,6 +52,7 @@ constexpr const char* ECAL_DEFAULT_CFG = "ecal.yaml"; /* environment variables */ constexpr const char* ECAL_DATA_VAR = "ECAL_DATA"; constexpr const char* ECAL_LOG_VAR = "ECAL_LOG_DIR"; +constexpr const char* ECAL_TRACE_VAR = "ECAL_TRACE_DIR"; constexpr const char* ECAL_LINUX_HOME_VAR = "HOME"; constexpr const char* ECAL_LINUX_TMP_VAR = "TMPDIR"; diff --git a/ecal/core/src/ecal_global_accessors.cpp b/ecal/core/src/ecal_global_accessors.cpp index 2970bf58b5..2473d8136b 100644 --- a/ecal/core/src/ecal_global_accessors.cpp +++ b/ecal/core/src/ecal_global_accessors.cpp @@ -32,6 +32,8 @@ #include "config/builder/logging_attribute_builder.h" #include "logging/ecal_log_provider.h" #include "logging/ecal_log_receiver.h" +#include "tracing/trace_provider.h" +#include "tracing/trace_provider_default.h" #include #include @@ -57,6 +59,8 @@ namespace eCAL std::shared_ptr g_log_provider_instance; std::shared_ptr g_log_receiver_instance; + std::shared_ptr g_trace_provider_instance; + void SetGlobalUnitName(const char *unit_name_) { if(unit_name_ != nullptr) g_unit_name = unit_name_; @@ -118,6 +122,21 @@ namespace eCAL return nullptr; } + void InitializeTracing(const eCAL::Configuration& config_) + { + g_trace_provider_instance = tracing::TraceProvider::Create(config_.tracing); + } + + void ResetTracing() + { + g_trace_provider_instance.reset(); + } + + std::shared_ptr g_trace_provider() + { + return g_trace_provider_instance; + } + std::shared_ptr g_globals() { if (auto globals_instance = g_globals_instance; globals_instance) diff --git a/ecal/core/src/ecal_global_accessors.h b/ecal/core/src/ecal_global_accessors.h index a4562f6383..3fd8ba8ee4 100644 --- a/ecal/core/src/ecal_global_accessors.h +++ b/ecal/core/src/ecal_global_accessors.h @@ -43,6 +43,11 @@ namespace eCAL class CLogReceiver; } + namespace tracing + { + class TraceProvider; + } + #if ECAL_CORE_MONITORING class CMonitoring; #endif @@ -80,6 +85,9 @@ namespace eCAL void InitializeLogging(const eCAL::Configuration& config_); void ResetLogging(); + void InitializeTracing(const eCAL::Configuration& config_); + void ResetTracing(); + // Declaration of getter functions for globally accessible variable instances std::shared_ptr g_globals(); #if ECAL_CORE_MONITORING @@ -110,6 +118,8 @@ namespace eCAL std::shared_ptr g_logging_provider(); std::shared_ptr g_logging_receiver(); + std::shared_ptr g_trace_provider(); + // declaration of globally accessible variables extern std::string g_default_ini_file; extern Configuration g_ecal_configuration; diff --git a/ecal/core/src/ecal_util.cpp b/ecal/core/src/ecal_util.cpp index ae9c4815ba..5cb42ffb18 100644 --- a/ecal/core/src/ecal_util.cpp +++ b/ecal/core/src/ecal_util.cpp @@ -46,6 +46,11 @@ namespace eCAL return eCAL::Config::GeteCALLogDirImpl(); } + std::string GeteCALTraceDir() + { + return eCAL::Config::GeteCALTraceDirImpl(); + } + #if ECAL_CORE_MONITORING // take monitoring snapshot static Monitoring::SMonitoring GetMonitoring() diff --git a/ecal/core/src/pubsub/ecal_publisher_impl.cpp b/ecal/core/src/pubsub/ecal_publisher_impl.cpp index 437217e9d8..33e6404d13 100644 --- a/ecal/core/src/pubsub/ecal_publisher_impl.cpp +++ b/ecal/core/src/pubsub/ecal_publisher_impl.cpp @@ -46,6 +46,11 @@ #include "registration/ecal_registration_provider.h" +#include "tracing/span.h" +#include "tracing/trace_provider.h" + +#include + #include #include #include @@ -122,6 +127,23 @@ namespace eCAL m_topic_id.topic_id.host_name = m_attributes.host_name; m_topic_id.topic_id.process_id = m_attributes.process_id; + // record topic metadata for tracing + { + if (auto provider = g_trace_provider(); provider) { + eCAL::tracing::STopicMetadata meta; + meta.entity_id = m_publisher_id; + meta.process_id = m_attributes.process_id; + meta.process_name = m_attributes.process_name; + meta.host_name = m_attributes.host_name; + meta.topic_name = m_attributes.topic_name; + meta.encoding = m_topic_info.encoding; + meta.type_name = m_topic_info.name; + meta.direction = eCAL::tracing::topic_direction::publisher; + + provider->WriteMetadata(meta); + } + } + // mark as created m_created = true; } @@ -179,6 +201,23 @@ namespace eCAL // prepare counter and internal states const size_t snd_hash = PrepareWrite(filter_id_, payload_buf_size); + // determine active transport layer for tracing + eCAL::tracing::eTracingLayerType active_layer = eCAL::tracing::tl_trace_none; + { +#if ECAL_CORE_TRANSPORT_SHM + if (m_writer_shm) { active_layer = static_cast(active_layer | eCAL::tracing::tl_trace_shm); } +#endif +#if ECAL_CORE_TRANSPORT_UDP + if (m_writer_udp) { active_layer = static_cast(active_layer | eCAL::tracing::tl_trace_udp); } +#endif +#if ECAL_CORE_TRANSPORT_TCP + if (m_writer_tcp) { active_layer = static_cast(active_layer | eCAL::tracing::tl_trace_tcp); } +#endif + } + + // create tracing span for the send operation + auto send_span = eCAL::tracing::CPublisherSpan::Create(m_topic_id, m_clock, active_layer, payload_buf_size, eCAL::tracing::operation_type::send); + // did we write anything bool written(false); diff --git a/ecal/core/src/pubsub/ecal_subscriber_impl.cpp b/ecal/core/src/pubsub/ecal_subscriber_impl.cpp index dea6e0b20a..56b7dd003b 100644 --- a/ecal/core/src/pubsub/ecal_subscriber_impl.cpp +++ b/ecal/core/src/pubsub/ecal_subscriber_impl.cpp @@ -23,6 +23,10 @@ **/ #include "ecal_subscriber_impl.h" +#include "tracing/layer_type.h" +#include "tracing/span.h" +#include "tracing/trace_provider.h" + #include #include #include @@ -93,6 +97,23 @@ namespace eCAL m_topic_id.topic_id.host_name = m_attributes.host_name; m_topic_id.topic_id.process_id = m_attributes.process_id; + // record topic metadata for tracing + { + if (auto provider = g_trace_provider(); provider) { + eCAL::tracing::STopicMetadata meta; + meta.entity_id = m_subscriber_id; + meta.process_id = m_attributes.process_id; + meta.process_name = m_attributes.process_name; + meta.host_name = m_attributes.host_name; + meta.topic_name = m_attributes.topic_name; + meta.encoding = m_topic_info.encoding; + meta.type_name = m_topic_info.name; + meta.direction = eCAL::tracing::topic_direction::subscriber; + + provider->WriteMetadata(meta); + } + } + // start transport layers InitializeLayers(); StartTransportLayer(); @@ -373,6 +394,15 @@ namespace eCAL size_t CSubscriberImpl::ApplySample(const Payload::TopicInfo& topic_info_, const char* payload_, size_t size_, long long id_, long long clock_, long long time_, size_t /*hash_*/, eTLayerType layer_) { + auto receive_span = eCAL::tracing::CSubscriberSpan::Create( + m_subscriber_id, + topic_info_, + clock_, + tracing::ToTracingLayerType(layer_), + size_, + eCAL::tracing::operation_type::receive + ); + // ensure thread safety const std::lock_guard lock(m_receive_callback_mutex); if (!m_created) return(0); @@ -450,7 +480,18 @@ namespace eCAL // execute it const std::lock_guard exec_lock(m_connection_map_mtx); - (m_receive_callback)(topic_id, m_connection_map[pub_info].data_type_info, cb_data); + { + auto callback_span = eCAL::tracing::CSubscriberSpan::Create( + m_subscriber_id, + topic_info_, + clock_, + tracing::ToTracingLayerType(layer_), + size_, + eCAL::tracing::operation_type::callback_execution + ); + + + (m_receive_callback)(topic_id, m_connection_map[pub_info].data_type_info, cb_data); } processed = true; } } diff --git a/ecal/core/src/tracing/span.cpp b/ecal/core/src/tracing/span.cpp new file mode 100644 index 0000000000..d26e10d177 --- /dev/null +++ b/ecal/core/src/tracing/span.cpp @@ -0,0 +1,109 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include "span.h" +#include "trace_provider.h" +#include "ecal_global_accessors.h" + +#include + +using tracing_clock = std::chrono::system_clock; +using namespace std::chrono; + +namespace eCAL +{ + namespace tracing + { + OptionalPublisherSpan CPublisherSpan::Create(const STopicId& topic_id, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type) + { + auto provider = g_trace_provider(); + if (!provider) return std::nullopt; + + return OptionalPublisherSpan{ + std::in_place, + CPublisherSpan::ConstructionToken{}, + std::move(provider), + topic_id, + clock, + layer, + payload_size, + op_type + }; + } + + // Send span constructor + CPublisherSpan::CPublisherSpan(ConstructionToken /*token*/, std::shared_ptr provider_, const STopicId& topic_id, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type) + : provider(std::move(provider_)) + { + auto now = tracing_clock::now(); + data.start_ns = duration_cast(now.time_since_epoch()).count(); + data.entity_id = topic_id.topic_id.entity_id; + data.payload_size = payload_size; + data.clock = clock; + data.layer = layer; + data.op_type = op_type; + } + + CPublisherSpan::~CPublisherSpan() + { + auto now = tracing_clock::now(); + data.end_ns = duration_cast(now.time_since_epoch()).count(); + provider->WriteSpan(data); + } + + OptionalSubscriberSpan CSubscriberSpan::Create(EntityIdT entity_id, const eCAL::Payload::TopicInfo& topic_info, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type) + { + auto provider = g_trace_provider(); + if (!provider) return std::nullopt; + + return OptionalSubscriberSpan{ + std::in_place, + CSubscriberSpan::ConstructionToken{}, + std::move(provider), + entity_id, + topic_info, + clock, + layer, + payload_size, + op_type + }; + } + + // Receive span constructor + CSubscriberSpan::CSubscriberSpan(ConstructionToken /*token*/, std::shared_ptr provider_, EntityIdT entity_id, const eCAL::Payload::TopicInfo& topic_info, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type) + : provider(std::move(provider_)) + { + auto now = tracing_clock::now(); + data.start_ns = duration_cast(now.time_since_epoch()).count(); + data.entity_id = entity_id; + data.topic_id = topic_info.topic_id; + data.payload_size = payload_size; + data.clock = clock; + data.layer = layer; + data.op_type = op_type; + } + + CSubscriberSpan::~CSubscriberSpan() + { + auto now = tracing_clock::now(); + data.end_ns = duration_cast(now.time_since_epoch()).count(); + provider->WriteSpan(data); + } + } +} diff --git a/ecal/core/src/tracing/span.h b/ecal/core/src/tracing/span.h new file mode 100644 index 0000000000..9d43d1fd15 --- /dev/null +++ b/ecal/core/src/tracing/span.h @@ -0,0 +1,98 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace eCAL +{ + namespace tracing + { + class TraceProvider; + + // RAII span for send (publisher) operations. + // Records start_ns on construction, end_ns + buffer on destruction. + class CPublisherSpan + { + private: + struct ConstructionToken + { + private: + ConstructionToken() = default; + friend class CPublisherSpan; + }; + + public: + static std::optional Create(const STopicId& topic_id, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type); + + CPublisherSpan(ConstructionToken token, std::shared_ptr provider_, const STopicId& topic_id, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type); + ~CPublisherSpan(); + + CPublisherSpan(const CPublisherSpan&) = delete; + CPublisherSpan& operator=(const CPublisherSpan&) = delete; + CPublisherSpan(CPublisherSpan&&) = default; + CPublisherSpan& operator=(CPublisherSpan&&) = delete; + + private: + SPublisherSpanData data{}; + std::shared_ptr provider; + }; + + using OptionalPublisherSpan = std::optional; + + // RAII span for receive (subscriber) operations. + // Records start_ns on construction, end_ns + buffer on destruction. + class CSubscriberSpan + { + private: + struct ConstructionToken + { + private: + ConstructionToken() = default; + friend class CSubscriberSpan; + }; + + public: + static std::optional Create(EntityIdT entity_id, const eCAL::Payload::TopicInfo& topic_info, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type); + CSubscriberSpan(ConstructionToken token, std::shared_ptr provider_, EntityIdT entity_id, const eCAL::Payload::TopicInfo& topic_info, long long clock, eTracingLayerType layer, size_t payload_size, operation_type op_type); + ~CSubscriberSpan(); + + private: + CSubscriberSpan(const CSubscriberSpan&) = delete; + CSubscriberSpan& operator=(const CSubscriberSpan&) = delete; + CSubscriberSpan(CSubscriberSpan&&) = default; + CSubscriberSpan& operator=(CSubscriberSpan&&) = delete; + + private: + SSubscriberSpanData data{}; + std::shared_ptr provider; + }; + + using OptionalSubscriberSpan = std::optional; + + } +} diff --git a/ecal/core/src/tracing/trace_provider.cpp b/ecal/core/src/tracing/trace_provider.cpp new file mode 100644 index 0000000000..f855ce8ea0 --- /dev/null +++ b/ecal/core/src/tracing/trace_provider.cpp @@ -0,0 +1,36 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include "trace_provider.h" +#include "trace_provider_default.h" + +namespace eCAL +{ + namespace tracing + { + std::shared_ptr TraceProvider::Create(const eCAL::Tracing::Configuration& config_) + { + if (config_.enabled) + { + return CTraceProviderDefault::CreateDefault(); + } + return nullptr; + } + } +} diff --git a/ecal/core/src/tracing/trace_provider.h b/ecal/core/src/tracing/trace_provider.h new file mode 100644 index 0000000000..1d9f5a545f --- /dev/null +++ b/ecal/core/src/tracing/trace_provider.h @@ -0,0 +1,49 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include + +#include + +#include + +namespace eCAL +{ + namespace tracing + { + class TraceProvider + { + public: + TraceProvider() = default; + virtual ~TraceProvider() = default; + + TraceProvider(const TraceProvider&) = delete; + TraceProvider& operator=(const TraceProvider&) = delete; + TraceProvider(TraceProvider&&) = delete; + TraceProvider& operator=(TraceProvider&&) = delete; + + static std::shared_ptr Create(const eCAL::Tracing::Configuration& config_); + + virtual void WriteSpan(const SpanData& span_data) = 0; + virtual void WriteMetadata(const STopicMetadata& metadata) = 0; + }; + } +} diff --git a/ecal/core/src/tracing/trace_provider_default.cpp b/ecal/core/src/tracing/trace_provider_default.cpp new file mode 100644 index 0000000000..7a9cedf968 --- /dev/null +++ b/ecal/core/src/tracing/trace_provider_default.cpp @@ -0,0 +1,131 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include "trace_provider_default.h" +#include "util/single_instance_helper.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + std::string GetCurrentTimestamp() + { + std::time_t now = std::time(nullptr); + std::tm* tm_info = std::localtime(&now); + std::ostringstream oss; + oss << std::put_time(tm_info, "%Y%m%d_%H%M%S"); + return oss.str(); + } +} + +namespace eCAL +{ + namespace tracing + { + std::shared_ptr CTraceProviderDefault::Create(std::unique_ptr writer, size_t batch_size) + { + try + { + return Util::CSingleInstanceHelper::Create(std::move(writer), batch_size); + } + catch (const std::exception&) + { + return nullptr; + } + } + + std::shared_ptr CTraceProviderDefault::CreateDefault(size_t batch_size) + { + const auto file_id = std::to_string(eCAL::Process::GetProcessID()) + "_" + GetCurrentTimestamp(); + return Create(std::make_unique(file_id, std::filesystem::path(Util::GeteCALTraceDir())), batch_size); + } + + + CTraceProviderDefault::CTraceProviderDefault(std::unique_ptr writer, size_t batch_size) + : batch_size_(batch_size), writer_(std::move(writer)) + { + writer_thread_ = std::thread(&CTraceProviderDefault::WriterThreadLoop, this); + } + + CTraceProviderDefault::~CTraceProviderDefault() + { + { + std::lock_guard lock(thread_mutex); + stop_thread_ = true; + write_cv_.notify_all(); + } + writer_thread_.join(); + } + + void CTraceProviderDefault::WriteSpan(const SpanData& span_data) + { + WriteTraceInfo(span_data); + } + + void CTraceProviderDefault::WriteMetadata(const STopicMetadata& metadata) + { + WriteTraceInfo(metadata); + } + + void CTraceProviderDefault::WriteTraceInfo(const TraceInfo& info) + { + std::lock_guard lock(thread_mutex); + span_buffer_.push_back(info); + if (span_buffer_.size() >= batch_size_) + { + write_cv_.notify_one(); + } + } + + void CTraceProviderDefault::WriterThreadLoop() + { + std::vector span_flusher; + while (true) + { + { + std::unique_lock lock(thread_mutex); + write_cv_.wait(lock, [this]() + { + return stop_thread_ || (span_buffer_.size() >= batch_size_); + }); + if (stop_thread_ && span_buffer_.empty()) + { + break; + } + span_flusher.swap(span_buffer_); + } + if (!span_flusher.empty()) + { + writer_->WriteTraceInfo(span_flusher); + span_flusher.clear(); + } + } + } + } +} diff --git a/ecal/core/src/tracing/trace_provider_default.h b/ecal/core/src/tracing/trace_provider_default.h new file mode 100644 index 0000000000..a4656ba78a --- /dev/null +++ b/ecal/core/src/tracing/trace_provider_default.h @@ -0,0 +1,79 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright 2026 AUMOVIO and subsidiaries. All rights reserved. + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include "trace_provider.h" +#include "util/single_instance_helper.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace eCAL +{ + namespace tracing + { + constexpr size_t kDefaultTracingBatchSize = 128; + + class CTraceProviderDefault : public TraceProvider + { + friend class Util::CSingleInstanceHelper; + + public: + static std::shared_ptr Create(std::unique_ptr writer, size_t batch_size = kDefaultTracingBatchSize); + static std::shared_ptr CreateDefault(size_t batch_size = kDefaultTracingBatchSize); + + CTraceProviderDefault(const CTraceProviderDefault&) = delete; + CTraceProviderDefault& operator=(const CTraceProviderDefault&) = delete; + CTraceProviderDefault(CTraceProviderDefault&&) = delete; + CTraceProviderDefault& operator=(CTraceProviderDefault&&) = delete; + + ~CTraceProviderDefault() override; + + // Write span data to buffer (accepts any span type via variant) + void WriteSpan(const SpanData& span_data) override; + + + void WriteMetadata(const STopicMetadata& metadata) override; + + private: + void WriteTraceInfo(const TraceInfo& info); + + CTraceProviderDefault(std::unique_ptr writer, size_t batch_size); + void WriterThreadLoop(); + + std::atomic batch_size_{kDefaultTracingBatchSize}; + std::vector span_buffer_; + mutable std::mutex thread_mutex; + std::condition_variable write_cv_; + bool stop_thread_{false}; + std::thread writer_thread_; + std::unique_ptr writer_; + }; + } +} diff --git a/ecal/tests/CMakeLists.txt b/ecal/tests/CMakeLists.txt index 298e02e01e..438d79fbdb 100644 --- a/ecal/tests/CMakeLists.txt +++ b/ecal/tests/CMakeLists.txt @@ -27,7 +27,7 @@ add_subdirectory(cpp/process_test) add_subdirectory(cpp/descgate_test) add_subdirectory(cpp/config_test) - +add_subdirectory(cpp/tracing_test) if(ECAL_CORE_REGISTRATION) add_subdirectory(cpp/registration_test) diff --git a/ecal/tests/cpp/config_test/src/path_processing_test.cpp b/ecal/tests/cpp/config_test/src/path_processing_test.cpp index 531cf18135..f91068c0df 100644 --- a/ecal/tests/cpp/config_test/src/path_processing_test.cpp +++ b/ecal/tests/cpp/config_test/src/path_processing_test.cpp @@ -444,3 +444,68 @@ TEST(core_cpp_dir_manager /*unused*/, can_write_to_directory_unicode_path /*unus std::filesystem::remove_all(unicode_dir_path, ec); } #endif /* ECAL_OS_WINDOWS */ + + +TEST(core_cpp_path_processing /*unused*/, ecal_trace_order_test /*unused*/) +{ + const std::string ecal_trace_env_var = "/ecal/trace/env"; + const std::string ecal_data_env_var = "/ecal/data/env"; + const std::string ecal_data_env_trace_var = ecal_data_env_var + path_separator + ECAL_FOLDER_NAME_TRACE; + const std::string ecal_yaml_dir = "/dir/to/current/yaml"; + const std::string ecal_yaml_trace_dir = ecal_yaml_dir + path_separator + ECAL_FOLDER_NAME_TRACE; + const std::string unique_tmp_dir = "/tmp/unique"; + + const MockDirProvider mock_dir_provider; + const NiceMock mock_dir_manager; + + EXPECT_CALL(mock_dir_provider, eCALEnvVar(ECAL_TRACE_VAR)) + .Times(6) + .WillOnce(testing::Return(ecal_trace_env_var)) + .WillRepeatedly(testing::Return("")); + EXPECT_CALL(mock_dir_provider, eCALEnvVar(ECAL_DATA_VAR)) + .Times(6) + .WillRepeatedly(testing::Return(ecal_data_env_var)); + + EXPECT_CALL(mock_dir_manager, getDirectoryPath(testing::_)) + .Times(6) + .WillRepeatedly(testing::Return(ecal_yaml_dir)); + + EXPECT_CALL(mock_dir_manager, dirExists(ecal_trace_env_var)) + .Times(1) + .WillOnce(testing::Return(true)); + EXPECT_CALL(mock_dir_manager, dirExists(ecal_data_env_trace_var)) + .Times(5) + .WillOnce(testing::Return(true)) + .WillRepeatedly(testing::Return(false)); + EXPECT_CALL(mock_dir_manager, dirExists(ecal_data_env_var)) + .Times(4) + .WillOnce(testing::Return(true)) + .WillRepeatedly(testing::Return(false)); + + EXPECT_CALL(mock_dir_manager, dirExists(ecal_yaml_trace_dir)) + .Times(3) + .WillOnce(testing::Return(true)) + .WillRepeatedly(testing::Return(false)); + EXPECT_CALL(mock_dir_manager, dirExists(ecal_yaml_dir)) + .Times(2) + .WillOnce(testing::Return(true)) + .WillRepeatedly(testing::Return(false)); + EXPECT_CALL(mock_dir_manager, dirExistsOrCreate(testing::_)) + .Times(0); + + EXPECT_CALL(mock_dir_provider, uniqueTmpDir(::testing::Ref(mock_dir_manager))) + .Times(1) + .WillRepeatedly(testing::Return(unique_tmp_dir)); + + ON_CALL(mock_dir_manager, dirExists(testing::_)).WillByDefault(testing::Return(false)); + ON_CALL(mock_dir_manager, canWriteToDirectory(testing::_)).WillByDefault(testing::Return(true)); + + auto config = eCAL::GetConfiguration(); + + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), ecal_trace_env_var); + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), ecal_data_env_trace_var); + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), ecal_data_env_var); + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), ecal_yaml_trace_dir); + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), ecal_yaml_dir); + EXPECT_EQ(eCAL::Config::GeteCALTraceDirImpl(mock_dir_provider, mock_dir_manager, config), unique_tmp_dir); +} diff --git a/ecal/tests/cpp/config_test/src/yaml_processing_test.cpp b/ecal/tests/cpp/config_test/src/yaml_processing_test.cpp index ef05d22690..cad63b057a 100644 --- a/ecal/tests/cpp/config_test/src/yaml_processing_test.cpp +++ b/ecal/tests/cpp/config_test/src/yaml_processing_test.cpp @@ -91,6 +91,8 @@ TEST(core_cpp_config_yaml /*unused*/, yaml_processing_comparison /*unused*/) config.application.startup.terminal_emulator = "term_emulator"; config.application.sys.filter_excl = "filter_excl"; + config.tracing.enabled = true; + config.logging.provider.console.enable = false; config.logging.provider.console.log_level = eCAL::Logging::eLogLevel::log_level_debug1; config.logging.provider.file.enable = true; @@ -161,6 +163,7 @@ TEST(core_cpp_config_yaml /*unused*/, yaml_processing_comparison /*unused*/) EXPECT_EQ(config.logging.provider.udp_config.port, config_from_yaml.logging.provider.udp_config.port); EXPECT_EQ(config.logging.receiver.enable, config_from_yaml.logging.receiver.enable); EXPECT_EQ(config.logging.receiver.udp_config.port, config_from_yaml.logging.receiver.udp_config.port); + EXPECT_EQ(config.tracing.enabled, config_from_yaml.tracing.enabled); auto yaml_from_config = YAML::Node(config); eCAL::Configuration config_from_yaml_config = yaml_from_config.as(); @@ -220,6 +223,7 @@ TEST(core_cpp_config_yaml /*unused*/, yaml_processing_comparison /*unused*/) EXPECT_EQ(config.logging.provider.udp_config.port, config_from_yaml_config.logging.provider.udp_config.port); EXPECT_EQ(config.logging.receiver.enable, config_from_yaml_config.logging.receiver.enable); EXPECT_EQ(config.logging.receiver.udp_config.port, config_from_yaml_config.logging.receiver.udp_config.port); + EXPECT_EQ(config.tracing.enabled, config_from_yaml_config.tracing.enabled); } TEST(core_cpp_config /*unused*/, read_write_file_test /*unused*/) diff --git a/ecal/tests/cpp/tracing_test/CMakeLists.txt b/ecal/tests/cpp/tracing_test/CMakeLists.txt new file mode 100644 index 0000000000..21a6f4546b --- /dev/null +++ b/ecal/tests/cpp/tracing_test/CMakeLists.txt @@ -0,0 +1,55 @@ +# ========================= eCAL LICENSE ================================= +# +# Copyright (C) 2016 - 2025 Continental Corporation +# +# 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. +# +# ========================= eCAL LICENSE ================================= + +project(test_tracing) + +find_package(Threads REQUIRED) +find_package(GTest REQUIRED) + +set(tracing_test_src + src/tracing_test_helpers.h + src/trace_provider_test.cpp +) + +ecal_add_gtest(${PROJECT_NAME} ${tracing_test_src}) + +target_include_directories(${PROJECT_NAME} PRIVATE + $ + ${CMAKE_CURRENT_LIST_DIR}/src + ${ECAL_CORE_PROJECT_ROOT}/core/src + ${ECAL_CORE_PROJECT_ROOT}/core/src/serialization +) + +target_link_libraries(${PROJECT_NAME} + PRIVATE + ecal_core_private + eCAL::ecal-utils + Threads::Threads +) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) + +target_compile_definitions(${PROJECT_NAME} PRIVATE ECAL_CORE_COMMAND_LINE) + +ecal_install_gtest(${PROJECT_NAME}) + +set_property(TARGET ${PROJECT_NAME} PROPERTY FOLDER tests/cpp/tracing) + +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${tracing_test_src} +) diff --git a/ecal/tests/cpp/tracing_test/src/trace_provider_test.cpp b/ecal/tests/cpp/tracing_test/src/trace_provider_test.cpp new file mode 100644 index 0000000000..a4bb64f686 --- /dev/null +++ b/ecal/tests/cpp/tracing_test/src/trace_provider_test.cpp @@ -0,0 +1,128 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright (C) 2016 - 2025 Continental Corporation + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#include "tracing_test_helpers.h" + +#include + +#include +#include + +#include + +#include + +#include +#include +#include + +TEST(TestTraceProvider, ConcurrentSpanWrites) +{ + constexpr size_t num_threads = 100; + constexpr size_t spans_per_thread = 100; + constexpr size_t total_spans = num_threads * spans_per_thread; // 10000 + constexpr size_t batch_size = 500; // Flush every 500 spans + + MockTracingWriter mock_writer; + + auto provider = eCAL::tracing::CTraceProviderDefault::Create( + std::make_unique(mock_writer), batch_size); + ASSERT_NE(provider, nullptr); + + Barrier barrier(num_threads); + + std::vector threads; + threads.reserve(num_threads); + + for (size_t t = 0; t < num_threads; ++t) + { + threads.emplace_back([&, t]() + { + barrier.wait(); + for (size_t i = 0; i < spans_per_thread; ++i) + { + eCAL::tracing::SPublisherSpanData span{}; + span.op_type = eCAL::tracing::operation_type::send; + span.entity_id = static_cast(t * spans_per_thread + i); + span.payload_size = 42; + span.clock = static_cast(i); + span.layer = eCAL::tracing::tl_trace_shm; + span.start_ns = 1000 + static_cast(i); + span.end_ns = 2000 + static_cast(i); + provider->WriteSpan(span); + } + }); + } + + // Join all writer threads. + for (auto& th : threads) + th.join(); + + // Destroy the provider to flush any remaining buffered spans. + provider.reset(); + + // Every span must be written exactly once — no duplicates, no drops. + EXPECT_EQ(mock_writer.SpanCount(), total_spans); +} + +TEST(TestTraceProvider, ConcurrentMetadataWrites) +{ + constexpr size_t num_threads = 100; + constexpr size_t metadata_per_thread = 100; + constexpr size_t total_metadata = num_threads * metadata_per_thread; // 10000 + constexpr size_t batch_size = 500; + + MockTracingWriter mock_writer; + + auto provider = eCAL::tracing::CTraceProviderDefault::Create( + std::make_unique(mock_writer), batch_size); + ASSERT_NE(provider, nullptr); + + Barrier barrier(num_threads); + + std::vector threads; + threads.reserve(num_threads); + + for (size_t t = 0; t < num_threads; ++t) + { + threads.emplace_back([&, t]() + { + barrier.wait(); + for (size_t i = 0; i < metadata_per_thread; ++i) + { + eCAL::tracing::STopicMetadata metadata{}; + metadata.entity_id = static_cast(t * metadata_per_thread + i); + metadata.process_id = 1; + metadata.host_name = "test_host"; + metadata.topic_name = "topic_" + std::to_string(t) + "_" + std::to_string(i); + metadata.encoding = "protobuf"; + metadata.type_name = "TestType"; + metadata.direction = eCAL::tracing::topic_direction::publisher; + provider->WriteMetadata(metadata); + } + }); + } + + for (auto& th : threads) + th.join(); + + provider.reset(); + + EXPECT_EQ(mock_writer.MetadataCount(), total_metadata); +} \ No newline at end of file diff --git a/ecal/tests/cpp/tracing_test/src/tracing_test_helpers.h b/ecal/tests/cpp/tracing_test/src/tracing_test_helpers.h new file mode 100644 index 0000000000..a28150d618 --- /dev/null +++ b/ecal/tests/cpp/tracing_test/src/tracing_test_helpers.h @@ -0,0 +1,88 @@ +/* ========================= eCAL LICENSE ================================= + * + * Copyright (C) 2016 - 2025 Continental Corporation + * + * 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. + * + * ========================= eCAL LICENSE ================================= +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +// Mock writer that counts spans and metadata for test assertions. +class MockTracingWriter : public eCAL::tracing::TracingWriter +{ +public: + void WriteTraceInfo(const std::vector& batch) override + { + for (const auto& info : batch) + { + std::visit([this](const auto& span) { this->Write(span); }, info); + } + } + + size_t SpanCount() const + { + return span_count_.load(); + } + + size_t MetadataCount() const + { + return metadata_count_.load(); + } + + void Clear() + { + span_count_ = 0; + metadata_count_ = 0; + + } + +private: + void Write(const eCAL::tracing::SpanData& /*spandata*/) + { + ++span_count_; + } + + void Write(const eCAL::tracing::STopicMetadata& /*metadata*/) + { + ++metadata_count_; + } + + mutable std::mutex mutex_; + std::atomic span_count_{0}; + std::atomic metadata_count_{0}; +}; + +// This allows the mock to outlive the CTraceProvider that only owns this proxy. +class ProxyTracingWriter : public eCAL::tracing::TracingWriter +{ +public: + explicit ProxyTracingWriter(MockTracingWriter& target) : target_(target) {} + + void WriteTraceInfo(const std::vector& batch) override + { + target_.WriteTraceInfo(batch); + } + +private: + MockTracingWriter& target_; +}; + diff --git a/thirdparty/nlohmann_json/build-nlohmann_json.cmake b/thirdparty/nlohmann_json/build-nlohmann_json.cmake new file mode 100644 index 0000000000..ae2984d190 --- /dev/null +++ b/thirdparty/nlohmann_json/build-nlohmann_json.cmake @@ -0,0 +1,13 @@ +include_guard(GLOBAL) +include(FetchContent) + +FetchContent_Declare( + nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH SHA256=d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d +) + +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +set(JSON_Install OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(nlohmann_json)