Composite / Multiple loggers logger - #1574
Conversation
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
|
| 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; |
There was a problem hiding this comment.
please use std::stringstream or similar. std::string is not built for this kind of repeated appending in a loop
There was a problem hiding this comment.
See how it's done in the TextLogger for reference.
| template <typename... Args> void log_all(Args&&... args) { | ||
| for (auto& child : children_) { | ||
| child->log(std::forward<Args>(args)...); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 Args... are nor about the qualification, we just pass them around. Same below.
figueroa1395
left a comment
There was a problem hiding this comment.
Partial review. I'll continue later
|
|
||
| LoggerType log_; | ||
| std::mutex mutex_; | ||
| mutable std::mutex mutex_; |
There was a problem hiding this comment.
This was initially strange for me, but it makes sense. See this Herb Sutter article for a nice explanation.
| // Clear accumulated output. Default: no-op. | ||
| virtual void clear() {} |
There was a problem hiding this comment.
Why is the default no-op? Shouldn't the default just be to clear the underlying logged data?
|
|
||
| // 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({}); } |
There was a problem hiding this comment.
Is this similar to flush() in the TextLogger? Or is their purpose different now?
I see get_output takes the callback as an argument, whereas flush takes the callback via the TextLogger constructor. It feels to me that both are attempting very similar things and only one should remain.
That said, taking it as an argument is a lot more flexible and perhaps aligns best with the C-API. So maybe flush can be removed?
Thoughts?
| 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; |
There was a problem hiding this comment.
See how it's done in the TextLogger for reference.
|
|
||
| protected: | ||
| std::string snapshot_locked() const override { return get().string_report(); } | ||
| void clear_locked() override { get().clear(); } |
There was a problem hiding this comment.
A couple of questions:
- Why is
clear_lockedprotected? It should be accessible by "everyone" now, right? Edit: I see now, CRTP, right? - Why not just name it
cleardirectly? The user would directly get this overload unless they explicitly cast the type to get the underlyingclear. Also, this avoid potential naming confusion. Edit: Due to CRTP the way to access it is then viaclear, as expected. This is just likeclear_impl, right? - Same questions from above but for
TextLogger.
| void flush() { get().flush(); } | ||
|
|
||
| protected: | ||
| std::string snapshot_locked() const override { return get().report(); } |
There was a problem hiding this comment.
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.
| std::string snapshot; | ||
| { | ||
| std::lock_guard const lock{mutex_}; | ||
| snapshot = snapshot_locked(); |
There was a problem hiding this comment.
I think this is an extra copy made. Maybe just passing around string_views is fine and converting it once to string at the caller fn point below is sufficient?
Also, since this involves a callback which may throw, it may be a good idea to do Lippincot pattern or similar like in flush for the TextLogger such that we handle exceptions or at least we propagate to one that points towards hey, something is wrong with your callback, can't do anything.
| 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; |
There was a problem hiding this comment.
Can this be placed in log_all? I believe it's only relevant there and it may lead to confusion later if we add another member function with log in the "wrong" place and unexpected behaviour triggers.
| // 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. |
There was a problem hiding this comment.
Since this logger is what will be shared, let's make sure to have these two things explicit in the documentation.
| } | ||
| loggers_.push_back(std::move(logger)); | ||
| } | ||
| void remove(MultiThreadedLogger const* logger) { |
There was a problem hiding this comment.
Should this also take in a share_ptr instead to keep it consistent? Probably not, but making sure.
| } | ||
| void reset() { loggers_.clear(); } | ||
|
|
||
| std::unique_ptr<Logger> create_child() override { |
There was a problem hiding this comment.
We probably want this one and below marked as final to avoid user overriding things and messing them up. Or should we leave that up to them?
| using MultiThreadedLogger::log; | ||
|
|
||
| // Fan out clear() to every registered logger. | ||
| void clear() override { |
There was a problem hiding this comment.
What's the difference in behaviour between reset and clear? Do we need both?
| template <typename... Args> void log_all(Args&&... args) { | ||
| for (auto& child : children_) { | ||
| child->log(std::forward<Args>(args)...); |
There was a problem hiding this comment.
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 Args... are nor about the qualification, we just pass them around. Same below.
There was a problem hiding this comment.
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.
| } | ||
| void reset() { loggers_.clear(); } | ||
|
|
||
| std::unique_ptr<Logger> create_child() override { |
There was a problem hiding this comment.
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?
| // 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I'm missing tests in which a "custom" logger inherits from MultiThreadedCompositeLogger. Also the get_output functionality with a custom callback must be tested (you can get inspiration from the TextLogger tests.


No description provided.