-
Notifications
You must be signed in to change notification settings - Fork 78
Composite / Multiple loggers logger #1574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,9 @@ | |
|
|
||
| #include <algorithm> | ||
| #include <concepts> | ||
| #include <format> | ||
| #include <map> | ||
| #include <string_view> | ||
| #include <string> | ||
| #include <type_traits> | ||
|
|
||
| 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 <std::derived_from<Logger> T> T& merge_into(T& destination) const { | ||
| if (&destination == this) { | ||
| return destination; // nothing to do | ||
|
|
@@ -109,7 +119,11 @@ class MultiThreadedCalculationInfo : public MultiThreadedLoggerImpl<CalculationI | |
| using Report = CalculationInfo::Report; | ||
|
|
||
| Report report() const { return get().report(); } | ||
| void clear() { get().clear(); } | ||
| std::string string_report() const { return get().string_report(); } | ||
|
|
||
| protected: | ||
| std::string snapshot_locked() const override { return get().string_report(); } | ||
| void clear_locked() override { get().clear(); } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A couple of questions:
|
||
| }; | ||
| } // namespace common::logging | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can become a bit obscure how the chain of logger, multithreadedlogger, compositelogger, multithreadedcompositelogger works, specially considering that after come the actual implementations. Can you add a brief description somewhere here explaining the flow a bit, otherwise in the tests. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| // SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org> | ||
| // | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| #pragma once | ||
|
|
||
| #include "logging.hpp" | ||
|
|
||
| #include <algorithm> | ||
| #include <memory> | ||
| #include <ranges> | ||
| #include <string_view> | ||
| #include <vector> | ||
|
|
||
| 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<std::unique_ptr<Logger>> 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be placed in |
||
|
|
||
| private: | ||
| std::vector<std::unique_ptr<Logger>> children_; | ||
|
|
||
| template <typename... Args> void log_all(Args&&... args) { | ||
| for (auto& child : children_) { | ||
|
Check warning on line 34 in power_grid_model_c/power_grid_model/include/power_grid_model/common/composite_logging.hpp
|
||
| child->log(std::forward<Args>(args)...); | ||
|
Comment on lines
+33
to
+35
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can't forward the same object multiple times. please add a test case that this is not accidentally done. i'd have expected sonar to warn about this
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An additional side note: Since we have some strong conventions about perfect forwarding, let's add a comment here for reference in the future. This cases do lay in one of the valid use cases: we don't care what |
||
| } | ||
| } | ||
| }; | ||
|
|
||
| // 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. | ||
|
Comment on lines
+46
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this logger is what will be shared, let's make sure to have these two things explicit in the documentation. |
||
| class MultiThreadedCompositeLogger : public MultiThreadedLogger { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm missing some reporting functionality at this stage, since the loggers will be under an abstraction, would reporting work directly via multithreaded? don't you need some overload where users can select from which or all loggers to report? |
||
| public: | ||
| MultiThreadedCompositeLogger() = default; | ||
| explicit MultiThreadedCompositeLogger(std::vector<std::shared_ptr<MultiThreadedLogger>> 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<MultiThreadedLogger> 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also take in a |
||
| 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<Logger> create_child() override { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably want this one and below marked as
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to leave the user have this control? I make for C-API users it makes sense, but Python users and Cpp users (?) shouldn't need to, right? |
||
| std::vector<std::unique_ptr<Logger>> child_loggers; | ||
| child_loggers.reserve(loggers_.size()); | ||
| for (auto const& logger : loggers_) { | ||
| child_loggers.push_back(logger->create_child()); | ||
| } | ||
| return std::make_unique<CompositeChildLogger>(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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the difference in behaviour between |
||
| for (auto const& logger : loggers_) { | ||
| logger->clear(); | ||
| } | ||
| } | ||
|
|
||
| [[nodiscard]] bool empty() const { return loggers_.empty(); } | ||
|
|
||
| private: | ||
| std::vector<std::shared_ptr<MultiThreadedLogger>> loggers_; // owning | ||
|
|
||
| template <typename... Args> void log_all(Args&&... args) { | ||
| for (auto const& logger : loggers_) { | ||
| logger->log(std::forward<Args>(args)...); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| } // namespace power_grid_model::common::logging | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| #include "common.hpp" | ||
|
|
||
| #include <cstdint> | ||
| #include <functional> | ||
| #include <memory> | ||
| #include <string_view> | ||
|
|
||
|
|
@@ -71,6 +72,13 @@ | |
|
|
||
| struct MultiThreadedLogger : public Logger { | ||
| virtual std::unique_ptr<Logger> 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<void(std::string_view)> const& callback) const { callback({}); } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this similar to I see That said, taking it as an argument is a lot more flexible and perhaps aligns best with the C-API. So maybe Thoughts? |
||
|
|
||
| // Clear accumulated output. Default: no-op. | ||
| virtual void clear() {} | ||
|
Check failure on line 81 in power_grid_model_c/power_grid_model/include/power_grid_model/common/logging.hpp
|
||
|
Comment on lines
+80
to
+81
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the default no-op? Shouldn't the default just be to clear the underlying logged data? |
||
| }; | ||
|
|
||
| } // namespace common::logging | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -75,11 +75,32 @@ | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| 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<void(std::string_view)> 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(); | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is an extra copy made. Maybe just passing around Also, since this involves a callback which may |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| fn(snapshot); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+88
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. declarative
Suggested change
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| void clear() final { | ||||||||||||||||||||||||||||||||
| std::lock_guard const lock{mutex_}; | ||||||||||||||||||||||||||||||||
| clear_locked(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| protected: | ||||||||||||||||||||||||||||||||
| virtual std::string snapshot_locked() const { return {}; } | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||
| virtual void clear_locked() {} | ||||||||||||||||||||||||||||||||
|
Check failure on line 97 in power_grid_model_c/power_grid_model/include/power_grid_model/common/multi_threaded_logging.hpp
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| private: | ||||||||||||||||||||||||||||||||
| friend class ThreadLogger; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| LoggerType log_; | ||||||||||||||||||||||||||||||||
| std::mutex mutex_; | ||||||||||||||||||||||||||||||||
| mutable std::mutex mutex_; | ||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was initially strange for me, but it makes sense. See this Herb Sutter article for a nice explanation.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why mutable? |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| void sync(ThreadLogger const& logger) { | ||||||||||||||||||||||||||||||||
| assert(&logger != &log_); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<TextLogger> { | |
| using MultiThreadedLoggerImpl<TextLogger>::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(); } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be made more efficient if you just get the "raw" data and the turn into a "string" or whatever you may need at the multi threaded logger side? Same for calculation info. I mention this because I believe this may copy the data twice, which can get expensive easily. |
||
| void clear_locked() override { get().clear(); } | ||
| }; | ||
| } // namespace common::logging | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm missing tests in which a "custom" logger inherits from |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org> | ||
| // | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| #include <power_grid_model/common/composite_logging.hpp> | ||
|
|
||
| #include <power_grid_model/common/logging.hpp> | ||
| #include <power_grid_model/common/text_logger.hpp> | ||
|
|
||
| #include <doctest/doctest.h> | ||
|
|
||
| #include <memory> | ||
|
|
||
| namespace power_grid_model::common::logging { | ||
| namespace { | ||
| using LoggerPtr = std::shared_ptr<MultiThreadedTextLogger>; | ||
|
|
||
| LoggerPtr make_text_logger() { return std::make_shared<MultiThreadedTextLogger>(); } | ||
| } // 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 |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use
std::stringstreamor similar.std::stringis not built for this kind of repeated appending in a loopThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See how it's done in the
TextLoggerfor reference.