Configuration¶
ULog is configured with a single struct: ULogInit.
struct ULogInit {
const char* trace_path; // per-level sink paths
const char* debug_path;
const char* info_path;
const char* warn_path;
const char* error_path;
uint64_t flush_interval_ns; // flush period for the background coroutine
std::size_t queue_capacity_pow2; // queue capacity as 2^N
std::size_t batch_size; // max logs written per flush
bool enable_color_stdout; // allow ANSI color on TTY sinks
// rotation
std::size_t max_file_size_bytes; // 0 = disabled
uint32_t max_files; // number of rotated backups to keep
// structured mode
bool json_mode; // emit JSON lines if true
// metrics
bool track_metrics; // expose overflow/backpressure stats
};
All fields have sane defaults in ulog::init() if you don't pass a config.
Output routing¶
Each log level can go to its own file:
.trace_path = "./trace.log",
.debug_path = "./debug.log",
.info_path = "./info.log",
.warn_path = "./warn.log",
.error_path = "./error.log",
If the path for a level is nullptr, that level goes to stdout.
So this:
.trace_path = nullptr,
.debug_path = nullptr,
.info_path = "./service.log",
.warn_path = "./service.log",
.error_path = "./service.log",
means:
- TRACE and DEBUG go to stdout (with color if TTY),
- INFO/WARN/ERROR go to
service.log.
Rotation¶
ULog supports size-based log rotation per level.
Fields:
max_file_size_bytes: when the sink for a level exceeds this size, rotation triggers.max_files: how many rotated versions to keep (file.log.1,file.log.2, ...).
How it works:
- The logger flush coroutine checks the file size before each write batch.
-
If the next batch would exceed
max_file_size_bytes:- It
fsync()s and closes the current file. -
It renames:
file.log.(N-1)→file.log.N- ...
file.log.1→file.log.2file.log→file.log.1- Then it opens a fresh new
file.log.
- Then it opens a fresh new
- The whole pending batch then goes into the new file.
- It
No producer thread ever rotates or touches file descriptors. Rotation is exclusively done in the flush coroutine.
If max_file_size_bytes == 0, rotation is disabled.
If a level is logging to stdout (path is nullptr), rotation is also disabled for that level.
JSON mode¶
If json_mode == false (default), each line looks like:
If json_mode == true, each line looks like:
Notes:
- In JSON mode, color is not applied.
- Message text is JSON-escaped (
",\n,\t, etc.). - One JSON object per line → log aggregators love this.
You can switch this at startup only. It's global.
Queue sizing¶
queue_capacity_pow2 controls the MPMC ring buffer size as a power of two.
Examples:
queue_capacity_pow2 = 12→ 2^12 = 4096 entriesqueue_capacity_pow2 = 14→ 2^14 = 16384 entriesqueue_capacity_pow2 = 16→ 65536 entries
Each entry holds:
- timestamp
- thread id
- level
- message (up to 4096 bytes, UTF-8 safe truncated)
Large queues = more burst tolerance, more memory.
Metrics¶
If track_metrics == true, ULog records internal pressure data:
- how often it had to fall back to a per-thread overflow buffer
- how often that overflow buffer was also full and it had to spin-push
You can fetch them via:
auto st = ulog::stats();
ulog::info("ulog stats: overflow_pushes={} backpressure_spins={}",
st.overflow_pushes,
st.backpressure_spins);
Metrics are purely in-memory counters and do not impact performance meaningfully.