From 9bb8ef0ab814d12ca2253eb7d88fd0274ff0a903 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Fri, 4 Sep 2026 11:47:32 +0200 Subject: [PATCH 01/11] start from poc Signed-off-by: Nitish Bharambe --- .../common/calculation_info.hpp | 18 ++- .../common/composite_logging.hpp | 108 ++++++++++++++ .../power_grid_model/common/logging.hpp | 8 + .../common/multi_threaded_logging.hpp | 21 +++ .../power_grid_model/common/text_logger.hpp | 7 +- tests/cpp_unit_tests/logging/CMakeLists.txt | 1 + .../logging/test_composite_logging.cpp | 140 ++++++++++++++++++ 7 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp create mode 100644 tests/cpp_unit_tests/logging/test_composite_logging.cpp diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp index 021a8ff530..ea9f168c3a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp @@ -10,8 +10,9 @@ #include #include +#include #include -#include +#include #include namespace power_grid_model { @@ -92,6 +93,15 @@ class CalculationInfo : public Logger { Report report() const { return data_; } void clear() { data_.clear(); } + std::string string_report() const { + std::string result; + for (auto const& [tag, value] : data_) { + // Each line has format: EVENT_CODE\tVALUE + result += std::format("{}\t{}\n", std::to_underlying(tag), value); + } + return result; + } + template T> T& merge_into(T& destination) const { if (&destination == this) { return destination; // nothing to do @@ -109,7 +119,11 @@ class MultiThreadedCalculationInfo : public MultiThreadedLoggerImpl +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "logging.hpp" + +#include +#include +#include +#include +#include + +namespace power_grid_model::common::logging { + +// Owns a list of child loggers (created by MultiThreadedCompositeLogger::create_child) and fans all log calls out to +// each of them. The children are owned by this logger; their lifetimes are tied to this object. +class CompositeChildLogger : public Logger { + public: + explicit CompositeChildLogger(std::vector> children) : children_{std::move(children)} {} + + void log(LogEvent tag) override { log_all(tag); } + void log(LogEvent tag, std::string_view message) override { log_all(tag, message); } + void log(LogEvent tag, double value) override { log_all(tag, value); } + void log(LogEvent tag, Idx value) override { log_all(tag, value); } + + using Logger::log; + + private: + std::vector> children_; + + template void log_all(Args&&... args) { + for (auto& child : children_) { + child->log(std::forward(args)...); + } + } +}; + +// Owning fan-out MultiThreadedLogger. Holds shared ownership of MultiThreadedLogger instances and forwards +// all log calls to each. create_child() creates a CompositeChildLogger that owns one child per registered logger. +// +// Lifetime contract: each registered logger is kept alive by this composite for as long as it remains +// registered (shared ownership), regardless of whether any other owner (e.g. a C API wrapper) has released +// its own reference. This is what makes destroying the wrapper while still registered safe. +// Dedupe: registering the same logger twice is a no-op (idempotent, consistent with logging conventions). +// UB: modifying the logger list while a calculation is in progress. +class MultiThreadedCompositeLogger : public MultiThreadedLogger { + public: + MultiThreadedCompositeLogger() = default; + explicit MultiThreadedCompositeLogger(std::vector> loggers) + : loggers_{std::move(loggers)} {} + + // Add/remove a logger. The object address is unchanged so any existing reference_wrapper + // pointing to this composite remains valid. Do not call while a calculation is in progress. + void add(std::shared_ptr logger) { + if (logger == nullptr) { + return; // defensively ignore null registrations + } + if (std::ranges::any_of(loggers_, [&](auto const& existing) { return existing.get() == logger.get(); })) { + return; // already registered — dedupe silently, consistent with logging API conventions + } + loggers_.push_back(std::move(logger)); + } + void remove(MultiThreadedLogger const* logger) { + if (auto it = std::ranges::find_if(loggers_, [&](auto const& existing) { return existing.get() == logger; }); + it != loggers_.end()) { + loggers_.erase(it); + } + } + void reset() { loggers_.clear(); } + + std::unique_ptr create_child() override { + std::vector> child_loggers; + child_loggers.reserve(loggers_.size()); + for (auto const& logger : loggers_) { + child_loggers.push_back(logger->create_child()); + } + return std::make_unique(std::move(child_loggers)); + } + + void log(LogEvent tag) override { log_all(tag); } + void log(LogEvent tag, std::string_view message) override { log_all(tag, message); } + void log(LogEvent tag, double value) override { log_all(tag, value); } + void log(LogEvent tag, Idx value) override { log_all(tag, value); } + + using MultiThreadedLogger::log; + + // Fan out clear() to every registered logger. + void clear() override { + for (auto const& logger : loggers_) { + logger->clear(); + } + } + + [[nodiscard]] bool empty() const { return loggers_.empty(); } + + private: + std::vector> loggers_; // owning + + template void log_all(Args&&... args) { + for (auto const& logger : loggers_) { + logger->log(std::forward(args)...); + } + } +}; + +} // namespace power_grid_model::common::logging diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp index 15ef2e3265..378627e88d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp @@ -7,6 +7,7 @@ #include "common.hpp" #include +#include #include #include @@ -71,6 +72,13 @@ class Logger { struct MultiThreadedLogger : public Logger { virtual std::unique_ptr create_child() = 0; + + // The function is called exactly once with a string_view valid only for the duration of the call. + // Default: no op / delivers an empty view + virtual void get_output(std::function const& callback) const { callback({}); } + + // Clear accumulated output. Default: no-op. + virtual void clear() {} }; } // namespace common::logging diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp index 9f2b7d27c5..f22d957376 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp @@ -75,6 +75,27 @@ class MultiThreadedLoggerImpl : public MultiThreadedLogger { using MultiThreadedLogger::log; + // Lock-safe overrides. Marked final so subclasses cannot bypass the lock; override + // snapshot_locked / clear_locked instead to add type-specific behaviour. + void get_output(std::function const& fn) const final { + // Snapshot under the lock, then call fn without the lock so user callbacks + // cannot re-enter logger APIs and deadlock on the non-recursive mutex. + std::string snapshot; + { + std::lock_guard const lock{mutex_}; + snapshot = snapshot_locked(); + } + fn(snapshot); + } + void clear() final { + std::lock_guard const lock{mutex_}; + clear_locked(); + } + + protected: + virtual std::string snapshot_locked() const { return {}; } + virtual void clear_locked() {} + private: friend class ThreadLogger; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp index a4b6afc58b..a0a95bde42 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp @@ -85,6 +85,7 @@ class TextLogger : public Logger { data_.clear(); // reset error flags } std::string report() const { return data_.str(); } + std::string_view report_view() const { return data_.view(); } void flush() { if (flush_handler_) { // exception swallowing: if the handler throws, we leave the logger in valid state and the caller handles it @@ -113,8 +114,12 @@ class MultiThreadedTextLogger : public MultiThreadedLoggerImpl { using MultiThreadedLoggerImpl::MultiThreadedLoggerImpl; std::string report() const { return get().report(); } - void clear() { get().clear(); } + std::string_view report_view() const { return get().report_view(); } void flush() { get().flush(); } + + protected: + std::string snapshot_locked() const override { return get().report(); } + void clear_locked() override { get().clear(); } }; } // namespace common::logging diff --git a/tests/cpp_unit_tests/logging/CMakeLists.txt b/tests/cpp_unit_tests/logging/CMakeLists.txt index 7a733fd3b9..9320773f5e 100644 --- a/tests/cpp_unit_tests/logging/CMakeLists.txt +++ b/tests/cpp_unit_tests/logging/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable( power_grid_model_unit_tests_logging "../test_entry_point.cpp" "test_calculation_info.cpp" + "test_composite_logging.cpp" "test_timer.cpp" "test_text_logger.cpp" ) diff --git a/tests/cpp_unit_tests/logging/test_composite_logging.cpp b/tests/cpp_unit_tests/logging/test_composite_logging.cpp new file mode 100644 index 0000000000..7b9cb08161 --- /dev/null +++ b/tests/cpp_unit_tests/logging/test_composite_logging.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +#include +#include + +#include + +#include + +namespace power_grid_model::common::logging { +namespace { +using LoggerPtr = std::shared_ptr; + +LoggerPtr make_text_logger() { return std::make_shared(); } +} // namespace + +TEST_CASE("Test MultiThreadedCompositeLogger") { + MultiThreadedCompositeLogger composite; + + SUBCASE("Empty composite has no output and is empty") { CHECK(composite.empty()); } + + SUBCASE("Adding a null logger is a no-op") { + composite.add(nullptr); + CHECK(composite.empty()); + } + + SUBCASE("Logging fans out to a single registered logger") { + auto logger = make_text_logger(); + composite.add(logger); + CHECK_FALSE(composite.empty()); + + composite.log(LogEvent::total, Idx{1}); + + CHECK(logger->report().find("Tag:0") != std::string::npos); + } + + SUBCASE("Logging fans out to multiple registered loggers") { + auto logger_a = make_text_logger(); + auto logger_b = make_text_logger(); + composite.add(logger_a); + composite.add(logger_b); + + composite.log(LogEvent::total, Idx{1}); + + CHECK_FALSE(logger_a->report().empty()); + CHECK_FALSE(logger_b->report().empty()); + } + + SUBCASE("Registering the same logger twice is idempotent") { + auto logger = make_text_logger(); + composite.add(logger); + composite.add(logger); // second add — silent no-op + + composite.log(LogEvent::total, Idx{1}); + + // Only one entry should be logged, i.e. exactly one occurrence of the tag. + auto const report = logger->report(); + auto const first = report.find("Tag:0"); + CHECK(first != std::string::npos); + CHECK(report.find("Tag:0", first + 1) == std::string::npos); + } + + SUBCASE("Remove detaches a specific logger without affecting others") { + auto logger_a = make_text_logger(); + auto logger_b = make_text_logger(); + composite.add(logger_a); + composite.add(logger_b); + + composite.remove(logger_a.get()); + composite.log(LogEvent::total, Idx{1}); + + CHECK(logger_a->report().empty()); + CHECK_FALSE(logger_b->report().empty()); + } + + SUBCASE("Remove of an unregistered logger is a no-op") { + auto logger = make_text_logger(); + composite.remove(logger.get()); // never added + CHECK(composite.empty()); + } + + SUBCASE("Reset detaches all loggers") { + auto logger_a = make_text_logger(); + auto logger_b = make_text_logger(); + composite.add(logger_a); + composite.add(logger_b); + + composite.reset(); + CHECK(composite.empty()); + + composite.log(LogEvent::total, Idx{1}); + CHECK(logger_a->report().empty()); + CHECK(logger_b->report().empty()); + } + + SUBCASE("clear() fans out to every registered logger") { + auto logger = make_text_logger(); + composite.add(logger); + composite.log(LogEvent::total, Idx{1}); + CHECK_FALSE(logger->report().empty()); + + composite.clear(); + CHECK(logger->report().empty()); + } + + SUBCASE("Registered logger implementation stays alive after the caller drops its own shared_ptr") { + MultiThreadedTextLogger const* raw_logger{}; + { + auto logger = make_text_logger(); + raw_logger = logger.get(); + composite.add(logger); + } // caller's shared_ptr is dropped here; the composite keeps its own shared_ptr alive. + CHECK_FALSE(composite.empty()); + + // The composite still owns the implementation, so logging must not crash and must produce output. + // Observing through raw_logger is not UB: the composite's shared_ptr keeps the object alive. + composite.log(LogEvent::total, Idx{1}); + CHECK_FALSE(raw_logger->report().empty()); + } + + SUBCASE("create_child fans out to a child of every registered logger") { + auto logger_a = make_text_logger(); + auto logger_b = make_text_logger(); + composite.add(logger_a); + composite.add(logger_b); + + { + auto child = composite.create_child(); + child->log(LogEvent::total, Idx{1}); + } // child destroyed here; TextLogger children merge into their parent on destruction + + CHECK_FALSE(logger_a->report().empty()); + CHECK_FALSE(logger_b->report().empty()); + } +} +} // namespace power_grid_model::common::logging From 76a037474170ac7af4b3f262292fb42756e50028 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 9 Sep 2026 16:07:15 +0200 Subject: [PATCH 02/11] make mutex mutable Signed-off-by: Nitish Bharambe --- .../include/power_grid_model/common/multi_threaded_logging.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp index f22d957376..4018041948 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp @@ -100,7 +100,7 @@ class MultiThreadedLoggerImpl : public MultiThreadedLogger { friend class ThreadLogger; LoggerType log_; - std::mutex mutex_; + mutable std::mutex mutex_; void sync(ThreadLogger const& logger) { assert(&logger != &log_); From b6ce5147943e9e79233ff5361ab3b12133bf8384 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 16 Sep 2026 10:59:17 +0200 Subject: [PATCH 03/11] address comments Signed-off-by: Nitish Bharambe --- .../common/calculation_info.hpp | 4 ++-- .../common/multi_threaded_logging.hpp | 17 +++++++++-------- .../power_grid_model/common/text_logger.hpp | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp index ea9f168c3a..95094a8988 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp @@ -122,8 +122,8 @@ class MultiThreadedCalculationInfo : public MultiThreadedLoggerImpl const& fn) const final { // Snapshot under the lock, then call fn without the lock so user callbacks // cannot re-enter logger APIs and deadlock on the non-recursive mutex. - std::string snapshot; - { + std::string const snapshot = [&] { std::lock_guard const lock{mutex_}; - snapshot = snapshot_locked(); - } + return snapshot_thread_unsafe_impl(); + }(); fn(snapshot); } void clear() final { std::lock_guard const lock{mutex_}; - clear_locked(); + clear_thread_unsafe_impl(); } protected: - virtual std::string snapshot_locked() const { return {}; } - virtual void clear_locked() {} + // Snapshot implementation. Thread-safety must be handled by the caller + virtual std::string snapshot_thread_unsafe_impl() const { return {}; } + virtual void clear_thread_unsafe_impl() {} private: friend class ThreadLogger; LoggerType log_; + // Mutable to enable locking in const methods like snapshot_thread_unsafe_impl and get_output. mutable std::mutex mutex_; void sync(ThreadLogger const& logger) { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp index a0a95bde42..dbb0e39657 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/text_logger.hpp @@ -118,8 +118,8 @@ class MultiThreadedTextLogger : public MultiThreadedLoggerImpl { void flush() { get().flush(); } protected: - std::string snapshot_locked() const override { return get().report(); } - void clear_locked() override { get().clear(); } + std::string snapshot_thread_unsafe_impl() const override { return get().report(); } + void clear_thread_unsafe_impl() override { get().clear(); } }; } // namespace common::logging From 37f37f6eaec96ee178337d743f06d7e851e130c7 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 16 Sep 2026 11:11:08 +0200 Subject: [PATCH 04/11] address comments forwarding Signed-off-by: Nitish Bharambe --- .../common/composite_logging.hpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp index 544b58be25..7803bce876 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp @@ -20,19 +20,19 @@ class CompositeChildLogger : public Logger { public: explicit CompositeChildLogger(std::vector> children) : children_{std::move(children)} {} - void log(LogEvent tag) override { log_all(tag); } + void log(LogEvent tag) override { log_all(tag); } void log(LogEvent tag, std::string_view message) override { log_all(tag, message); } - void log(LogEvent tag, double value) override { log_all(tag, value); } - void log(LogEvent tag, Idx value) override { log_all(tag, value); } + void log(LogEvent tag, double value) override { log_all(tag, value); } + void log(LogEvent tag, Idx value) override { log_all(tag, value); } using Logger::log; private: std::vector> children_; - template void log_all(Args&&... args) { + template void log_all(Args const&... args) { for (auto& child : children_) { - child->log(std::forward(args)...); + child->log(args...); } } }; @@ -79,10 +79,10 @@ class MultiThreadedCompositeLogger : public MultiThreadedLogger { return std::make_unique(std::move(child_loggers)); } - void log(LogEvent tag) override { log_all(tag); } + void log(LogEvent tag) override { log_all(tag); } void log(LogEvent tag, std::string_view message) override { log_all(tag, message); } - void log(LogEvent tag, double value) override { log_all(tag, value); } - void log(LogEvent tag, Idx value) override { log_all(tag, value); } + void log(LogEvent tag, double value) override { log_all(tag, value); } + void log(LogEvent tag, Idx value) override { log_all(tag, value); } using MultiThreadedLogger::log; @@ -98,9 +98,9 @@ class MultiThreadedCompositeLogger : public MultiThreadedLogger { private: std::vector> loggers_; // owning - template void log_all(Args&&... args) { + template void log_all(Args const&... args) { for (auto const& logger : loggers_) { - logger->log(std::forward(args)...); + logger->log(args...); } } }; From 3fd603dc41db4855db5d4b7d2d3c27d343f01be3 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 16 Sep 2026 13:36:41 +0200 Subject: [PATCH 05/11] address comments sonar + others Signed-off-by: Nitish Bharambe --- .../power_grid_model/common/composite_logging.hpp | 2 +- .../power_grid_model/common/multi_threaded_logging.hpp | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp index 7803bce876..7e077e61ee 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp @@ -31,7 +31,7 @@ class CompositeChildLogger : public Logger { std::vector> children_; template void log_all(Args const&... args) { - for (auto& child : children_) { + for (auto const& child : children_) { child->log(args...); } } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp index 1e9b3bad82..82c7724f6b 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp @@ -93,8 +93,14 @@ class MultiThreadedLoggerImpl : public MultiThreadedLogger { protected: // Snapshot implementation. Thread-safety must be handled by the caller - virtual std::string snapshot_thread_unsafe_impl() const { return {}; } - virtual void clear_thread_unsafe_impl() {} + virtual std::string snapshot_thread_unsafe_impl() const { + return { + // The default logger has no state to snapshot; stateful loggers override this hook. + }; + } + virtual void clear_thread_unsafe_impl() { + // The default logger has no state to clear; stateful loggers override this hook. + } private: friend class ThreadLogger; From 5e62c35cc0f77ef239f74b684d3e827406851f49 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 16 Sep 2026 16:06:06 +0200 Subject: [PATCH 06/11] clang tidy Signed-off-by: Nitish Bharambe --- tests/cpp_unit_tests/logging/test_composite_logging.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cpp_unit_tests/logging/test_composite_logging.cpp b/tests/cpp_unit_tests/logging/test_composite_logging.cpp index 7b9cb08161..38e7326c36 100644 --- a/tests/cpp_unit_tests/logging/test_composite_logging.cpp +++ b/tests/cpp_unit_tests/logging/test_composite_logging.cpp @@ -4,6 +4,7 @@ #include +#include #include #include From 8d55f7a9b15e44897fcdb71f121e0705e0a89cdf Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Wed, 16 Sep 2026 17:12:13 +0200 Subject: [PATCH 07/11] add get output test Signed-off-by: Nitish Bharambe --- .../logging/test_calculation_info.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/cpp_unit_tests/logging/test_calculation_info.cpp b/tests/cpp_unit_tests/logging/test_calculation_info.cpp index de3296a2a7..0f5828ec55 100644 --- a/tests/cpp_unit_tests/logging/test_calculation_info.cpp +++ b/tests/cpp_unit_tests/logging/test_calculation_info.cpp @@ -11,6 +11,7 @@ #include +#include #include #include #include @@ -189,6 +190,38 @@ TEST_CASE("Test MultiThreadedCalculationInfo") { CHECK(clean_report.empty()); } + SUBCASE("Get output snapshot") { + logger_helper(multi_threaded_info); + auto const expected_output = multi_threaded_info.string_report(); + std::string output; + + // Re-enter from the callback to verify get_output releases its mutex before + // invoking user code and that the callback receives a pre-clear snapshot. + multi_threaded_info.get_output([&](std::string_view snapshot) { + output = snapshot; + multi_threaded_info.clear(); + }); + + CHECK(output == expected_output); + CHECK(multi_threaded_info.report().empty()); + } + + SUBCASE("Get output snapshot - multi threaded") { + run_parallel_jobs(arbitrary_n_threads, single_thread_job); + auto const expected_output = multi_threaded_info.string_report(); + std::string output; + + // Re-enter from the callback to verify get_output releases its mutex before + // invoking user code and that the callback receives a pre-clear snapshot. + multi_threaded_info.get_output([&](std::string_view snapshot) { + output = snapshot; + multi_threaded_info.clear(); + }); + + CHECK(output == expected_output); + CHECK(multi_threaded_info.report().empty()); + } + SUBCASE("Getters of underlying CalculationInfo") { auto const n_threads = static_cast(std::jthread::hardware_concurrency()); run_parallel_jobs(n_threads, single_thread_job); From c9dee9abc070b720f1f01e90043792fee31c5773 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Thu, 17 Sep 2026 16:21:55 +0200 Subject: [PATCH 08/11] address sonar Signed-off-by: Nitish Bharambe --- .../include/power_grid_model/common/logging.hpp | 5 +++-- tests/cpp_unit_tests/logging/test_calculation_info.cpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp index 378627e88d..062cdabf15 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp @@ -77,8 +77,9 @@ struct MultiThreadedLogger : public Logger { // Default: no op / delivers an empty view virtual void get_output(std::function const& callback) const { callback({}); } - // Clear accumulated output. Default: no-op. - virtual void clear() {} + virtual void clear() { + // Clear accumulated output. Default: no-op. + } }; } // namespace common::logging diff --git a/tests/cpp_unit_tests/logging/test_calculation_info.cpp b/tests/cpp_unit_tests/logging/test_calculation_info.cpp index 0f5828ec55..c871d4ecd3 100644 --- a/tests/cpp_unit_tests/logging/test_calculation_info.cpp +++ b/tests/cpp_unit_tests/logging/test_calculation_info.cpp @@ -197,7 +197,7 @@ TEST_CASE("Test MultiThreadedCalculationInfo") { // Re-enter from the callback to verify get_output releases its mutex before // invoking user code and that the callback receives a pre-clear snapshot. - multi_threaded_info.get_output([&](std::string_view snapshot) { + multi_threaded_info.get_output([&output, &multi_threaded_info](std::string_view snapshot) { output = snapshot; multi_threaded_info.clear(); }); @@ -213,7 +213,7 @@ TEST_CASE("Test MultiThreadedCalculationInfo") { // Re-enter from the callback to verify get_output releases its mutex before // invoking user code and that the callback receives a pre-clear snapshot. - multi_threaded_info.get_output([&](std::string_view snapshot) { + multi_threaded_info.get_output([&output, &multi_threaded_info](std::string_view snapshot) { output = snapshot; multi_threaded_info.clear(); }); From 472908ebcac8e568015edb3476fab14861baad4e Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Mon, 21 Sep 2026 10:17:59 +0200 Subject: [PATCH 09/11] change to ostringstream Signed-off-by: Nitish Bharambe --- .../include/power_grid_model/common/calculation_info.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp index 95094a8988..a6c6cf7e7a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp @@ -10,8 +10,8 @@ #include #include -#include #include +#include #include #include @@ -94,12 +94,12 @@ class CalculationInfo : public Logger { void clear() { data_.clear(); } std::string string_report() const { - std::string result; + std::ostringstream result; for (auto const& [tag, value] : data_) { // Each line has format: EVENT_CODE\tVALUE - result += std::format("{}\t{}\n", std::to_underlying(tag), value); + result << std::to_underlying(tag) << '\t' << value << '\n'; } - return result; + return result.str(); } template T> T& merge_into(T& destination) const { From 967c9ced34a3120f4a790889641e23f4f89c3eb2 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe <78108900+nitbharambe@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:09:19 +0200 Subject: [PATCH 10/11] Apply suggestion from @mgovers Co-authored-by: Martijn Govers Signed-off-by: Nitish Bharambe <78108900+nitbharambe@users.noreply.github.com> --- .../include/power_grid_model/common/calculation_info.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp index a6c6cf7e7a..7401869a27 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/calculation_info.hpp @@ -99,7 +99,7 @@ class CalculationInfo : public Logger { // Each line has format: EVENT_CODE\tVALUE result << std::to_underlying(tag) << '\t' << value << '\n'; } - return result.str(); + return std::move(result).str(); } template T> T& merge_into(T& destination) const { From a212e77a68e1bbf03e76198547fa48587f9c1414 Mon Sep 17 00:00:00 2001 From: Nitish Bharambe Date: Tue, 22 Sep 2026 12:06:12 +0200 Subject: [PATCH 11/11] address comments Signed-off-by: Nitish Bharambe --- .../include/power_grid_model/common/composite_logging.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp index 7e077e61ee..b6ad039416 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp @@ -20,13 +20,13 @@ class CompositeChildLogger : public Logger { public: explicit CompositeChildLogger(std::vector> children) : children_{std::move(children)} {} + using Logger::log; + void log(LogEvent tag) override { log_all(tag); } void log(LogEvent tag, std::string_view message) override { log_all(tag, message); } void log(LogEvent tag, double value) override { log_all(tag, value); } void log(LogEvent tag, Idx value) override { log_all(tag, value); } - using Logger::log; - private: std::vector> children_; @@ -63,6 +63,9 @@ class MultiThreadedCompositeLogger : public MultiThreadedLogger { loggers_.push_back(std::move(logger)); } void remove(MultiThreadedLogger const* logger) { + if (logger == nullptr) { + return; // defensively ignore null removals but it should be unreachable. + } if (auto it = std::ranges::find_if(loggers_, [&](auto const& existing) { return existing.get() == logger; }); it != loggers_.end()) { loggers_.erase(it);