Skip to content

Add the logging system#36

Open
Shakai-Dev wants to merge 3 commits into
Redot-Engine:masterfrom
Shakai-Dev:logging-pr
Open

Add the logging system#36
Shakai-Dev wants to merge 3 commits into
Redot-Engine:masterfrom
Shakai-Dev:logging-pr

Conversation

@Shakai-Dev

@Shakai-Dev Shakai-Dev commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added a comprehensive logging system with severity levels (Fatal, Error, Warning, Display, Info, Verbose, VeryVerbose)
    • Support for console, file, and rolling file outputs, with async dispatch and reliable flushing during shutdown
    • Global and per-category verbosity control, plus runtime logging metrics
    • Introduced logging macros for defining categories and emitting logs with source context
  • Tests
    • Added a full Doctest suite covering lifecycle, sink routing, verbosity filtering, concurrency, and fatal behavior

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@AR-DEV-1, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 57 minutes and 9 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1bcaaba-a2eb-4a55-98be-4eaa4c41ba68

📥 Commits

Reviewing files that changed from the base of the PR and between f8bc400 and 4357f3a.

📒 Files selected for processing (1)
  • Engine/cpp/runtime/core/logging/logging.test.cpp
📝 Walkthrough

Walkthrough

Introduces a new C++20 core.logging module under draco::core::logging with an asynchronous lock-free ring-buffer pipeline, configurable verbosity and per-category overrides, a replaceable sink list (ConsoleSink, FileSink, RollingFileSink), a synchronous fatal() path, a preprocessor macro header, a full Doctest test suite, and the corresponding CMake build wiring.

Changes

core.logging Module

Layer / File(s) Summary
Public API contract, macros, and build wiring
Engine/cpp/runtime/core/logging/logging.cppm, Engine/cpp/runtime/core/logging/macros.h, Engine/cpp/runtime/core/core.cppm, Engine/cpp/runtime/core/CMakeLists.txt, Engine/cpp/runtime/CMakeLists.txt, Engine/cpp/runtime/core/io/image_loader.cpp
Exports LogVerbosity, LogCategory, LogMessage, ILogSink, three concrete sink class declarations, LoggerMetrics, lifecycle/control functions, and log_fmt/fatal_fmt template helpers. macros.h adds DECLARE_LOG_CATEGORY, DEFINE_LOG_CATEGORY, SET_CATEGORY_VERBOSITY, and typed LOG_* macros capturing source location. core.cppm re-exports the core.logging module. CMake wiring registers the logging target linked against definitions, adds it to the core public link list. TODO comment annotation added.
Lock-free ring buffer and queue state management
Engine/cpp/runtime/core/logging/logging.cpp (module declaration, buffer storage, atomic state, enqueue/dequeue)
Defines global atomic state for the bounded sequence-based ring buffer, maintains queue positions, metrics, lifecycle flags, and sink-list atomics. Implements lock-free enqueue/dequeue routines with overflow/drop tracking via compare-and-swap on the enqueue position.
Async worker thread dispatch
Engine/cpp/runtime/core/logging/logging.cpp (worker loop)
Implements the worker thread that continuously drains the ring buffer, snapshots the current sink list under a mutex, synchronizes sink writes, updates written/dispatched counters, and waits on the condition variable when idle.
Lifecycle, control APIs, and log/fatal dispatch
Engine/cpp/runtime/core/logging/logging.cpp (init/shutdown/flush, verbosity/sink control, log/fatal/metrics)
Implements init() to initialize queue and start worker, shutdown() with graceful producer termination and flush, flush() barrier to wait for queue drain. Adds set_global_verbosity() and copy-on-write add_sink()/remove_sink(). Implements log() entry point with active-producer coordination, verbosity gating, text truncation, and enqueue; synchronous fatal() with stdout formatting, sink writes/flushes, hook invocation, and abort. Adds get_metrics() reporting.
Sink implementations
Engine/cpp/runtime/core/logging/logging.cpp (ConsoleSink, FileSink, RollingFileSink)
Implements ConsoleSink (stdout via std::println, flush via std::fflush), FileSink (append-mode file I/O with per-message flush), and RollingFileSink (timestamped filename generation using atomic roll counter, size-based file rolling, per-message flush).
Behavioral and stress validation suite
Engine/cpp/runtime/core/logging/logging.test.cpp
Full Doctest coverage: lifecycle initialization/shutdown, async dispatch to sinks, flush barrier semantics, sequence-id monotonicity, global and per-category verbosity filtering, multi-sink add/remove behavior, multi-threaded concurrent logging stress test, queue-overflow stress test with drop accounting, fatal hook override and message validation, post-shutdown silent drops, FileSink file I/O integration, macro placeholder formatting, category verbosity overrides, and get_metrics() field validation.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Call Site (LOG_INFO macro)
  participant logfmt as log_fmt()
  participant logfn as log()
  participant RingBuffer as Lock-free Ring Buffer
  participant WorkerThread as Worker Thread
  participant Sink as ILogSink (write/flush)

  Caller->>logfmt: LOG_INFO(Category, fmt, args...)
  logfmt->>logfmt: std::format_to_n into stack buffer
  logfmt->>logfn: log(verbosity, category, file, func, line, text)
  logfn->>logfn: check accepting_logs + verbosity threshold
  logfn->>logfn: increment active_producers
  logfn->>RingBuffer: enqueue(LogMessage) via sequence CAS
  alt queue full
    RingBuffer-->>logfn: drop, increment messages_dropped
  else enqueued
    RingBuffer-->>WorkerThread: notify condition_variable
  end
  logfn->>logfn: decrement active_producers
  WorkerThread->>RingBuffer: dequeue next LogMessage
  WorkerThread->>Sink: write(LogMessage)
  WorkerThread->>WorkerThread: increment messages_written
Loading
sequenceDiagram
  participant Caller as Call Site (LOG_FATAL macro)
  participant fatalfmt as fatal_fmt()
  participant fatalfn as fatal()
  participant Stdout as stdout
  participant Sink as All registered ILogSinks
  participant Hook as internal::fatal_hook

  Caller->>fatalfmt: LOG_FATAL(Category, fmt, args...)
  fatalfmt->>fatalfn: fatal(category, file, func, line, text)
  fatalfn->>Stdout: fprintf formatted fatal line
  fatalfn->>Sink: write(LogMessage) for each sink
  fatalfn->>Sink: flush() for each sink
  fatalfn->>Hook: invoke fatal_hook()
  fatalfn->>fatalfn: std::abort()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • JoltedJon
  • mcdubhghlas
  • Arctis-Fireblight

Poem

🐇 Hop hop, a new log hops in,
Ring buffers spinning, messages begin,
Fatal hooks and sinks galore,
Async threads knock knock at the door,
The rabbit writes INFO with glee —
No more printf, just LOG_INFO for me! 🪵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add the logging system' directly and clearly summarizes the main change: introduction of a comprehensive logging module to the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
Engine/cpp/runtime/core/logging/logging.test.cpp (1)

247-265: ⚡ Quick win

Make fatal_hook restoration exception-safe.

This test mutates global process state and restores it manually; if control exits unexpectedly before Line 264, later tests inherit the wrong hook.

Suggested fix
-    draco::core::logging::internal::fatal_hook = [] { throw FatalTestException{}; };
+    auto previous_fatal_hook = draco::core::logging::internal::fatal_hook;
+    struct FatalHookRestore final
+    {
+        void (**slot)();
+        void (*old_value)();
+        ~FatalHookRestore() { *slot = old_value; }
+    } fatal_hook_restore{&draco::core::logging::internal::fatal_hook, previous_fatal_hook};
+
+    draco::core::logging::internal::fatal_hook = [] { throw FatalTestException{}; };
@@
-    draco::core::logging::internal::fatal_hook = [] { std::abort(); };
     shutdown();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Engine/cpp/runtime/core/logging/logging.test.cpp` around lines 247 - 265, The
fatal_hook is manually restored after the test logic, but if an exception is
thrown before reaching the restoration line at the end, the global state remains
corrupted for subsequent tests. Wrap the fatal_hook assignment at the beginning
of the test in a RAII pattern or scope guard that guarantees the hook is
restored to its original state (std::abort) even if exceptions occur during the
LOG_FATAL call or any other operation within the test scope. Ensure the
restoration happens automatically when exiting the test scope, rather than
relying on manual restoration at a specific line.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Engine/cpp/runtime/core/logging/logging.cpp`:
- Around line 444-451: The timestamp format at line 444 uses second-level
precision which causes filename collisions when multiple log rolls occur within
the same second, causing the std::ios::trunc flag at line 451 to overwrite
previously rotated logs. Add higher precision to the timestamp (such as
milliseconds or microseconds) or implement a counter-based collision avoidance
mechanism in the new_path construction at line 450 to ensure each rolled log
file gets a unique filename even when rolls happen within the same second.
- Around line 195-219: The init() function creates a new worker thread and
assigns it to g_worker_thread without checking if a thread is already running.
To prevent std::terminate errors from repeated init() calls, add a guard before
the thread creation line that checks if g_worker_thread is joinable; if it is,
return early from the init() function to prevent reassigning an active thread.
The check should happen before g_worker_thread is assigned the new
std::thread(worker_loop).
- Around line 162-169: The worker thread loop (where sink->write(msg) is called
around line 166-169), the flush() function (lines 258-260), and the fatal()
function (lines 368-369) all invoke sink I/O operations without synchronization,
creating a data race when using std::ofstream-backed sinks. Add a new mutex
(separate from the existing g_sink_update_mutex which only protects sink list
modifications) to serialize all sink I/O operations. Lock this mutex around the
sink->write() call in the worker thread dequeue loop, around the flush
operations in the flush() function, and around the sink operations in the
fatal() function to ensure thread-safe access to the sink objects across all
three call sites.

In `@Engine/cpp/runtime/core/logging/logging.test.cpp`:
- Around line 288-306: The test has two issues: the ifstream object infile
opened at the file validation section is never closed before attempting to
remove the file at the end of the test, which causes cross-platform failures on
Windows where open files cannot be deleted; additionally, the hardcoded
test_output.log path can cause collisions when tests run in parallel. Fix this
by ensuring the infile stream is closed before file removal (either by wrapping
the file reading operations in a separate scope or explicitly calling
infile.close()) and by replacing the hardcoded test_path string with a unique
temporary file path using std::filesystem mechanisms to generate a unique name
per test run.

In `@Engine/cpp/runtime/core/logging/macros.h`:
- Around line 3-12: The SET_CATEGORY_VERBOSITY macro uses
std::memory_order_relaxed which is declared in the <atomic> header, but the
header file does not include it. Add `#include` <atomic> at the top of the file
near the existing `#include` <format> to ensure the header is self-sufficient and
all dependencies are properly declared, preventing potential compilation issues
for callers that may not have included <atomic> before using these macros.

---

Nitpick comments:
In `@Engine/cpp/runtime/core/logging/logging.test.cpp`:
- Around line 247-265: The fatal_hook is manually restored after the test logic,
but if an exception is thrown before reaching the restoration line at the end,
the global state remains corrupted for subsequent tests. Wrap the fatal_hook
assignment at the beginning of the test in a RAII pattern or scope guard that
guarantees the hook is restored to its original state (std::abort) even if
exceptions occur during the LOG_FATAL call or any other operation within the
test scope. Ensure the restoration happens automatically when exiting the test
scope, rather than relying on manual restoration at a specific line.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2530ce9c-7b86-4ae8-8d00-4f2021e58a00

📥 Commits

Reviewing files that changed from the base of the PR and between 4b1ebc5 and 7f2885b.

📒 Files selected for processing (8)
  • Engine/cpp/runtime/CMakeLists.txt
  • Engine/cpp/runtime/core/CMakeLists.txt
  • Engine/cpp/runtime/core/core.cppm
  • Engine/cpp/runtime/core/io/image_loader.cpp
  • Engine/cpp/runtime/core/logging/logging.cpp
  • Engine/cpp/runtime/core/logging/logging.cppm
  • Engine/cpp/runtime/core/logging/logging.test.cpp
  • Engine/cpp/runtime/core/logging/macros.h

Comment thread Engine/cpp/runtime/core/logging/logging.cpp
Comment thread Engine/cpp/runtime/core/logging/logging.cpp
Comment thread Engine/cpp/runtime/core/logging/logging.cpp Outdated
Comment thread Engine/cpp/runtime/core/logging/logging.test.cpp Outdated
Comment thread Engine/cpp/runtime/core/logging/macros.h
Shakai-Dev and others added 2 commits June 17, 2026 21:40
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@JoltedJon JoltedJon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@OldDev78 Please check out my comments on my review

Comment on lines +15 to +20
#define LOG_FATAL(Category, fmt, ...) draco::core::logging::fatal_fmt(Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(Category, fmt, ...) draco::core::logging::log_fmt(draco::core::logging::LogVerbosity::Error, Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARNING(Category, fmt, ...) draco::core::logging::log_fmt(draco::core::logging::LogVerbosity::Warning, Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_DISPLAY(Category, fmt, ...) draco::core::logging::log_fmt(draco::core::logging::LogVerbosity::Display, Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(Category, fmt, ...) draco::core::logging::log_fmt(draco::core::logging::LogVerbosity::Info, Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_VERBOSE(Category, fmt, ...) draco::core::logging::log_fmt(draco::core::logging::LogVerbosity::Verbose, Log##Category, __FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should probably opt to use std::source_location instead of the macros if possible, which might bring some trouble to the VA_ARGS.

Also it is probably better to use template Variadic argument list instead which makes the logging types safer. @OldDev78 do you have any comments on this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use modern methods indeed. std::source_location is cross-platform, and I'd also very much prefer variadic templates over macros.

Comment on lines +55 to +92

struct ILogSink
{
virtual ~ILogSink() = default;
virtual void write(const LogMessage& msg) = 0;
virtual void flush() = 0;
};

class ConsoleSink final : public ILogSink
{
public:
void write(const LogMessage& msg) override;
void flush() override;
};

class FileSink : public ILogSink
{
protected:
std::ofstream m_stream;
public:
FileSink(const std::filesystem::path& path);
void write(const LogMessage& msg) override;
void flush() override;
};

class RollingFileSink final : public ILogSink
{
private:
std::filesystem::path m_base_path;
usize m_max_file_size;
usize m_current_size{0};
std::ofstream m_stream;
void roll_file();
public:
RollingFileSink(const std::filesystem::path& base_path, usize max_file_size);
void write(const LogMessage& msg) override;
void flush() override;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we get some documentation explaining each of these Sinks and their usages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can we get some documentation explaining each of these Sinks and their usages?

Sure, but should it be in Docs or here?

const char* function;
u32 line;

char text[MaxLogMessageLength];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will there ever be a case where we want to increase the log message length, or need it to be dynamically allocated?

@Shakai-Dev Shakai-Dev Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will there ever be a case where we want to increase the log message length, or need it to be dynamically allocated?

I don't think so but I'll let @OldDev78 comment on it

Comment on lines +114 to +115
void add_sink(std::shared_ptr<ILogSink> sink);
void remove_sink(std::shared_ptr<ILogSink> sink);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure if we should be using shared_ptr, @OldDev78 thoughts on how we're managing shared memory like this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will be using the default allocator (calling new) to begin with. If our allocators cannot be used with STL smart pointers, wrappers for such use need to be made.

Comment on lines +117 to +119
void log(LogVerbosity verbosity, const LogCategory& category, const char* file, const char* function, u32 line, const char* text);

[[noreturn]] void fatal(const LogCategory& category, const char* file, const char* function, u32 line, const char* text);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Like mentioned with macros.h we can probably be using std::source_location

@OldDev78 OldDev78 Jun 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should not be relying on macros at all.

Comment on lines +128 to +137
try
{
auto result = std::format_to_n(buffer, MaxLogMessageLength - 1, fmt, std::forward<Args>(args)...);
*result.out = '\0';
success = true;
}
catch (const std::format_error&)
{
// Only catch string formatting failures
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Spooky exceptions, I believe we're trying to not use exceptions so leaking exceptions might cause some issues down the line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In this level for sure, I'd prefer everything noexcept. If allocation fails at all, let terminate be called.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants