FoundationLogging¶
Ludus::FoundationLogging is the engine's custom logging subsystem. It
implements the logging redesign specified in
.kiro/specs/logging-redesign/ (review phases 0–4): a corrected synchronous
core, a lightweight split frontend, a bounded no-heap typed formatter, an
independent emergency reporter in FoundationBase, and a bounded asynchronous
MPSC backend.
Dependency direction¶
FoundationBase ← FoundationLogging ← (Platform / Rendering / Runtime / ...)
FoundationBase must never depend on FoundationLogging. The emergency
reporting primitive lives in FoundationBase (base/diagnostic.hpp) so Base and
the assertion subsystem can report failures without an upward dependency. The
logging module is platform-independent: the log directory is injected through
LogConfig::Directory (a borrowed UTF-8 path) rather than resolved inside this
module.
Headers (split by cost and purpose)¶
| Header | Purpose | Heavy includes |
|---|---|---|
logging/log.hpp |
Common call site: severities, categories, gating macros, raw text (LUDUS_LOG_TEXT), direct debug-critical delivery (LUDUS_LOG_DEBUG_SYNC), source capture |
none (no <format>/<filesystem>/<chrono>) |
logging/log_format.hpp |
Opt-in typed formatting (LUDUS_LOG_{TRACE..FATAL} with {}) |
none — uses the bounded engine, not <format> |
logging/log_system.hpp |
Lifecycle/config/flush/health (LogConfig, LogSystem, FlushKind, results) |
none (no public <filesystem>) |
logging/level.hpp, logging/category.hpp |
Severity + category vocabulary | none |
A translation unit that only needs raw-text logging includes log.hpp only and
pays no formatting cost. Formatting sites additionally include log_format.hpp.
Usage¶
#include <ludus/foundation/logging/log.hpp> // raw text + gating
#include <ludus/foundation/logging/log_format.hpp> // typed "{}" formatting
#include <ludus/foundation/logging/log_system.hpp> // lifecycle
using namespace ludus::foundation::logging;
LogConfig config{};
config.GlobalLevel = LogLevel::Info;
config.Directory = userLogDir; // borrowed UTF-8 path, copied on Initialize
config.Mode = LogMode::Asynchronous; // or Synchronous (tools/tests)
const LogInitResult init = LogSystem::Initialize(config);
// init.EffectiveMode reports the mode actually in effect.
LUDUS_LOG_INFO(LOG_CORE, "Ludus {} starting", version); // typed
LUDUS_LOG_TEXT(LOG_CORE, Warning, driverMessage); // raw (no format parse)
const auto v = LUDUS_LOG_DEBUG_SYNC(LOG_CORE, "before submit"); // direct, worker-independent
const auto f = LogSystem::Flush(FlushKind::Visible, 1000); // acknowledged fence flush
LogSystem::Shutdown(); // closes admission, drains, joins the worker
Categories are declared/defined with process-lifetime names:
// header
LUDUS_DECLARE_LOG_CATEGORY(LOG_RENDER);
// one .cpp
LUDUS_DEFINE_LOG_CATEGORY(LOG_RENDER, "Render");
// or, for a local constant:
inline constexpr LogCategory LogPlatform{"Platform"};
Category names must have process lifetime (string literals / constant categories). The async backend stores the category name as a borrowed view; a name backed by a temporary would dangle.
Delivery paths¶
- Ordinary (
LUDUS_LOG_*,LUDUS_LOG_TEXT): cheap gating; in async mode a bounded owned record is published to an MPSC queue drained by one worker that owns the sinks. In sync mode the record is written under an exclusive dispatch lock.Error/Fatalare flushed promptly in both modes. - Direct debugger-critical (
LUDUS_LOG_DEBUG_SYNC): delivered synchronously to stderr/debugger before returning, independent of the worker, so a breakpoint immediately after still observes it. Returns aDeliveryResult. - Emergency / Fatal: reported first via the independent
FoundationBaseprimitive (allocation-free, no engine locks), before the normal backend.
Filtering cost¶
- Compiled-out level (below
LUDUS_COMPILED_LOG_LEVEL): the whole statement is((void)0); neither the category nor the arguments are evaluated and no formatting is instantiated. - Runtime-disabled call: a lock-free scalar predicate (one atomic override load + one global threshold load) — no lock, no allocation, no map lookup, no timestamp, no argument evaluation.
Formatter¶
A narrow, bounded, no-heap typed formatter (chosen over a std::format
erasure after measuring build cost and the no-allocation/no-terminate contract —
see the decision log). Grammar: string/string_view, bool, Ludus integers,
pointer-as-address, float32/float64; {} plus {:x} (hex) and brace
escaping ({{→{). Unsupported argument types are a compile error. Output
is bounded; oversize input is truncated with a flag, never allocated.
Files and sessions¶
- One session file per process, created by exclusive creation (never truncates an existing/same-name file) named with UTC time + PID + nonce.
- Size-based rotation opens the replacement before releasing the old handle; on failure the sink is marked unhealthy (not silently disabled).
- Retention prunes only whole inactive session groups this logger created — never an active session and never unrelated files. Configurable session-count, total-byte, and age caps (0 means "no cap" for that dimension, explicitly).
Flush(FlushKind::Visible)pushes buffers to the OS;FlushKind::Durableadditionally issues an OS sync (fdatasync). Neither guarantees survival of arbitrary hardware failure.
Compiled level per build¶
LUDUS_COMPILED_LOG_LEVEL (0=Trace … 5=Fatal) defaults by build type and is
overridable via the CMake cache / presets:
| Build (preset) | Compiled minimum |
|---|---|
| Debug | Trace |
| Development (RelWithDebInfo) | Debug |
Profile (linux-clang-profile) |
Info |
| Release / MinSizeRel | Warning |
Fatal is never compiled out. Fatal reports; it does not terminate the
process (termination is owned by the caller / the assertion subsystem).
Deferred (separately scoped follow-ups)¶
- Optional JSON-lines serialization and bounded typed fields.
- In-editor console subscriber; remote export/transport.
- Crash-reporter integration, native stack capture, offline symbolization (the emergency path is not async-signal-safe and is not a crash handler).
- Per-thread queues / deferred formatting — only if MPSC producer tails miss a measured budget.
See .kiro/specs/logging-redesign/ for the full requirements, design, tasks, and
decision log.
Session and rotation filenames use the bounded Ludus formatter, checking
truncation and format errors before file creation. Native asynchronous backend
ownership uses UniquePtr and nonthrowing allocation; if that allocation fails,
initialization reports Degraded with Synchronous mode and keeps the prepared
sinks. It disables the timed flush worker to avoid allocating another thread after
the failure; explicit and shutdown flushes remain available. Standard formatting machinery is not used by the logger or its fixtures.