Skip to content

Logger: Composite / Multiple loggers logger - #1574

Open
nitbharambe wants to merge 8 commits into
mainfrom
pgm/feature/multiple-loggers
Open

nitbharambe wants to merge 8 commits into
mainfrom
pgm/feature/multiple-loggers

Conversation

@nitbharambe

@nitbharambe nitbharambe commented Sep 7, 2026

Copy link
Copy Markdown
Member

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

@nitbharambe nitbharambe changed the title Multiple loggers logger Composite / Multiple loggers logger Sep 7, 2026
@nitbharambe nitbharambe added the feature New feature or request label Sep 7, 2026
@nitbharambe
nitbharambe marked this pull request as ready for review September 9, 2026 14:07
@nitbharambe nitbharambe added improvement Improvement on internal implementation and removed feature New feature or request labels Sep 9, 2026
Comment on lines +97 to +102
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;

@mgovers mgovers Sep 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use std::stringstream or similar. std::string is not built for this kind of repeated appending in a loop

Copy link
Copy Markdown
Member

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 TextLogger for reference.

Comment on lines +33 to +35
template <typename... Args> void log_all(Args&&... args) {
for (auto& child : children_) {
child->log(std::forward<Args>(args)...);

@mgovers mgovers Sep 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 Args... are nor about the qualification, we just pass them around. Same below.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 figueroa1395 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review. I'll continue later

Comment on lines +80 to +81
// Clear accumulated output. Default: no-op.
virtual void clear() {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({}); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +97 to +102
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;

Copy link
Copy Markdown
Member

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 TextLogger for reference.


protected:
std::string snapshot_locked() const override { return get().string_report(); }
void clear_locked() override { get().clear(); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of questions:

  • Why is clear_locked protected? It should be accessible by "everyone" now, right? Edit: I see now, CRTP, right?
  • Why not just name it clear directly? The user would directly get this overload unless they explicitly cast the type to get the underlying clear. Also, this avoid potential naming confusion. Edit: Due to CRTP the way to access it is then via clear, as expected. This is just like clear_impl, right?
  • Same questions from above but for TextLogger.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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(); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

std::string snapshot;
{
std::lock_guard const lock{mutex_};
snapshot = snapshot_locked();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it because we dont define all overloads of log function

Comment on lines +46 to +47
// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also take in a share_ptr instead to keep it consistent? Probably not, but making sure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference in behaviour between reset and clear? Do we need both?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +33 to +35
template <typename... Args> void log_all(Args&&... args) {
for (auto& child : children_) {
child->log(std::forward<Args>(args)...);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 Args... are nor about the qualification, we just pass them around. Same below.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

}
void reset() { loggers_.clear(); }

std::unique_ptr<Logger> create_child() override {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why mutable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this situation makes me think if we need special handling for multi threaded text logger and flushing. Will explore later

@nitbharambe nitbharambe changed the title Composite / Multiple loggers logger Logger: Composite / Multiple loggers logger Sep 16, 2026
@nitbharambe
nitbharambe added this pull request to stack #1592 September 18, 2026 11:27
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>
@nitbharambe
nitbharambe force-pushed the pgm/feature/multiple-loggers branch from bc7d190 to c9dee9a Compare September 18, 2026 11:29
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement on internal implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants