Skip to content

Composite / Multiple loggers logger - #1574

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

Composite / Multiple loggers logger#1574
nitbharambe wants to merge 2 commits into
mainfrom
pgm/feature/multiple-loggers

Conversation

@nitbharambe

Copy link
Copy Markdown
Member

No description provided.

Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
@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
Signed-off-by: Nitish Bharambe <nitish.bharambe@alliander.com>
@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
@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
65.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

@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


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.

This was initially strange for me, but it makes sense. See this Herb Sutter article for a nice explanation.

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?


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

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.

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.

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.

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

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

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?

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?

// 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

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.

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