ULog is a zero-allocation, coroutine-native asynchronous logger for the
uvent runtime.
It delivers spdlog-level performance without using extra threads, locks, or external dependencies.
| Feature | Description |
|---|---|
| ⚡️ Zero-overhead hot path | Log calls (ulog::info, etc.) never block or allocate memory |
| 🧩 MPMC lock-free queue | Multi-producer, single-consumer batching |
| 🧠 Coroutine flusher | Runs entirely inside uvent — no thread pools |
| 📦 Per-level sinks | Separate files for TRACE/DEBUG/INFO/WARN/ERROR |
| 🎨 Color output | ANSI colors for TTY when enable_color_stdout=true |
| 🪶 JSON mode | Structured output, one JSON object per line |
| 🔁 File rotation | Automatic size-based rotation with retention limit |
| 🧷 Guaranteed delivery | Per-thread overflow buffer prevents log loss |
| 📊 Metrics | Tracks queue saturation and overflow usage |
| 🌐 UTF-8 safe | Truncates only on valid code-point boundaries |
| 🧱 Zero dependencies | Pure C++23, no fmt, no spdlog, no libstdc++ extensions |
ULog runs inside your existing event loop — no threads, no sleeps, no std::async.
#include "ulog/ulog.h"
#include "ulog/LoggerFlushTask.h"
#include "uvent/Uvent.h"
using namespace usub;
int main() {
// 1. Configure logger
ulog::ULogInit cfg{
.trace_path = "./trace.log",
.debug_path = "./debug.log",
.info_path = "./info.log",
.warn_path = "./warn.log",
.error_path = "./error.log",
.flush_interval_ns = 2'000'000ULL, // 2ms
.queue_capacity_pow2 = 14, // 2^14 = 16384 entries
.batch_size = 512,
.enable_color_stdout = true,
.max_file_size_bytes = 10 * 1024 * 1024, // rotate at 10 MB
.max_files = 3,
.json_mode = false,
.track_metrics = true
};
ulog::init(cfg);
// 2. Run Uvent event loop
Uvent loop(4);
loop.run();
// 3. Shutdown cleanly
ulog::shutdown();
}ULog provides a familiar interface matching spdlog/fmt-style formatting:
ulog::trace("user {} connected from {}", user_id, ip);
ulog::debug("cache hit key={}", key);
ulog::info("started module '{}'", module_name);
ulog::warn("slow request latency_ms={}", latency_ms);
ulog::error("db connect failed: {}", err_msg);Each call:
- Builds a formatted UTF-8 string on stack
- Pushes it into a lock-free global queue
- Returns immediately (no disk access)
Actual writes and rotation happen later inside the coroutine flusher.
[2025-10-28 12:03:44.861][3][I] starting event loop...
{"time":"2025-10-28 12:03:44.861","thread":3,"level":"I","msg":"starting event loop..."}Each level has its own sink.
If the current file exceeds max_file_size_bytes, ULog:
fsync()s and closes it- Renames
file.log→file.log.1, shifts older files up to.max_files - Opens a fresh
file.log - Writes the next batch into the new file
Rotation is atomic per batch — a log batch is never split across files.
Enable metrics with:
.track_metrics = trueThen inspect them at runtime:
auto st = ulog::stats();
ulog::info("ulog: overflow_pushes={}, backpressure_spins={}",
st.overflow_pushes,
st.backpressure_spins);overflow_pushes: how many times the main queue was fullbackpressure_spins: how often producers had to spin because even overflow buffers were full
struct ULogInit {
const char* trace_path;
const char* debug_path;
const char* info_path;
const char* warn_path;
const char* error_path;
uint64_t flush_interval_ns;
std::size_t queue_capacity_pow2;
std::size_t batch_size;
bool enable_color_stdout;
std::size_t max_file_size_bytes;
uint32_t max_files;
bool json_mode;
bool track_metrics;
};All fields have defaults:
- all levels → stdout
- rotation disabled
- color enabled if tty
- queue = 2¹⁴ entries
- flush interval = 2ms
- batch size = 512
- MPMC queue between producers and the flusher
- Per-thread overflow ring (TLS)
- One coroutine handles all I/O and rotation
- No allocation after startup
- No mutexes
- UTF-8 validated truncation
- If running inside
uvent: prints worker index - Otherwise: uses a stable pseudo-ID derived from TLS
| Feature | spdlog::async_logger | ulog |
|---|---|---|
| Thread pool | ✅ yes | ❌ none (uses coroutine) |
| Lock-free queue | ✅ | ✅ |
| Overflow handling | drop / block | no-loss spill buffer |
| Rotation | ✅ | ✅ |
| JSON logs | via sink | built-in |
| Compile-time filtering | ✅ | ❌ |
| ANSI color | ✅ | ✅ |
| Metrics | ❌ | ✅ |
| UTF-8 safe truncation | ⚙️ partial | ✅ strict |
| Zero dependencies | ❌ (fmt, spdlog) |
✅ pure C++23 |
include/
└── ulog/
├── Logger.h
├── LoggerFlushTask.h
└── ulog.h
docs/
├── index.md
├── getting-started.md
├── config.md
└── api/
├── logger.md
├── logger-flush-task.md
└── internals.md
mkdir build && cd build
cmake ..
make -jRequires C++23 and uvent available as a submodule or system include.