Logger: Composite / Multiple loggers logger - #1574
nitbharambe wants to merge 8 commits into
Conversation
| 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.
There was a problem hiding this comment.
Slipped my mind. The only thing which is stopping from forwarding multiple times is that none of the downstream logs have a rvalue overload.
I dont see a use case for when we would like to forward instead of const&. Hence restricting this.
figueroa1395
left a comment
There was a problem hiding this comment.
Partial review. I'll continue later
| // 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?
There was a problem hiding this comment.
Default on any logger can ideally be a no-op.
Currently this path gets used in NoMultiThreadedLogger and MultiThreadedCompositeLogger. Hence defaulted instead of pure virtual.
|
|
||
| // 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?
There was a problem hiding this comment.
Well, flush gets the data-> passes to flush handler -> clears the data.
And get_output only gets the data and passes to callback without clearing.
| 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.
There was a problem hiding this comment.
(Renamed locked to thread_unsafe_impl as suggested by martijn)
Yes, for CRTP. we can make class friend / mark specific places protected. I chose later.
We do need separate handling in multithreaded logger to implement thread safe operations hence they were routed this way.
| 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.
There was a problem hiding this comment.
We need it because we dont define all overloads of log function
| // 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.
There was a problem hiding this comment.
This MultiThreadedCompositeLogger is not shared anywhere. Its owned fully by the handle. I guess this would be visible in C API PR.
Things inside it would have shared ownership
| } | ||
| 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.
There was a problem hiding this comment.
Yes, I think not. I suppose that would be redundant. After being passed to remove, the composite logger and C++ core should not need it anymore.
| } | ||
| 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?
There was a problem hiding this comment.
As per above comment, composite logger is never available to user. And we dont have a special need to make it explicitly final yet.
| 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?
There was a problem hiding this comment.
Reset which is a MultiThreadedCompositeLogger method, and it empties/clears the loggers registered in composite logger. Its loggers_.clear() where loggers_ is a vector here.
vs
clear is a generic Logger method applicable for all logger types. Its for means emptying / clearing output.
For MultiThreadedCompositeLogger, its clears all the child loggers inside the composite logger.
We need 2 distinct methods in the end.
| 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?
There was a problem hiding this comment.
Answered above. No need to expose it to user.
| // 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.
composite logger would not be exposed to the users. And indeed its get output would not be called. Its supposed to be "write only". The purpose of it is internal logging management.
It would be a bit complicated to have a "report all" functionality and we dont see a use case for it.
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.
|
|
||
| LoggerType log_; | ||
| std::mutex mutex_; | ||
| mutable std::mutex mutex_; |
There was a problem hiding this comment.
To make get_output const.
I see this seems like an acceptable way as described in the article suggested by https://github.com/PowerGridModel/power-grid-model/pull/1574/changes#r3976369778
| }(); | ||
| fn(snapshot); | ||
| } | ||
| void clear() final { |
There was a problem hiding this comment.
Thinking about this situation makes me think if we need special handling for multi threaded text logger and flushing. Will explore later
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
bc7d190 to
c9dee9a
Compare
|



Implementation from POC in pgm/feature/logger-api-poc