Dated record timestamps, per-sink timestamp format, and archive names that keep the extension last (<stem>.<timestamp>.<ext>).
6.4 KiB
libenderlog
A standalone C++26 logging library, built as a C++20/26 module (import ender.log;)
with std::stacktrace call-stack capture on severe records.
It was extracted from the ender-physics engine, where it started life as
ender.log, and turned into a library that has no other dependency — not on the
engine, not on a logging framework.
Features
- Leveled records.
trace,debug,info,warn,error,critical, filtered by an atomicminimumcheck that is cheap enough to guard expensive message construction:if (log::enabled(log::level::debug)) { ... }. - Source location. Every record carries the file, line and function of the
caller, taken from
std::source_locationat the call site — exact even in a stripped release binary, because it is a compile-time constant. - Call stacks. Records at or above
options::stacktrace_fromcarry a formattedstd::stacktrace. The frames belonging to the library itself are stripped by symbol, so the first reported frame is the caller regardless of the optimisation level (the level wrappers get inlined away under-O). - Pluggable sinks. A
console_sink(stderr by default) and amemory_sink(for tests and in-game consoles) ship;sinkis a small interface. - File output with archiving.
file_sinkwrites to a file and, on open, moves an existing log aside to a timestamped archive, so a run never appends onto a previous run's log. It can also rotate by size, bound how many archives are kept, and format both the record timestamp and the archive names. - No stacktrace? No problem. Where
<stacktrace>is missing (libc++, and therefore every cross target), the module still compiles and records still carry their call site — they simply have no stack.
Requirements
C++26 modules and import std; need a recent toolchain:
| Requirement | Version |
|---|---|
| Compiler | GCC 15+ (or Clang with a standard library that provides the std module) |
| CMake | 3.30+ (for CMAKE_EXPERIMENTAL_CXX_IMPORT_STD) |
| Standard library | libstdc++ for std::stacktrace |
Ubuntu 26.04's default g++ (GCC 15) and CMake 4 satisfy this, and that is the
release the CI targets and the .deb is built for. Ubuntu 24.04 ships GCC 13 and
CMake 3.28 and cannot build import std; without extra toolchains, so it is not
supported.
Building
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure
Options:
| Option | Default | Description |
|---|---|---|
ENDERLOG_WERROR |
OFF |
Treat warnings as errors |
ENDERLOG_BACKTRACE_SYMBOLS |
ON |
Link executables with -rdynamic so traces can name frames |
ENDERLOG_BUILD_TESTS |
ON |
Build the test suite |
ENDERLOG_BUILD_EXAMPLES |
ON |
Build the example program |
Build Debug or RelWithDebInfo when you need to read a trace: frame names come
from debug information, and a release build often reports application frames as
<unknown>.
Using the library
add_subdirectory
add_subdirectory(libenderlog)
target_link_libraries(my_app PRIVATE enderlog::enderlog)
Installed package
find_package(enderlog REQUIRED)
target_link_libraries(my_app PRIVATE enderlog::enderlog)
The static archive is installed together with the module interface source
(ender.log.cppm) because a module's BMI is compiler-version-specific — the
consumer rebuilds it from the source.
In code
import std;
import ender.log;
namespace log = ender::log;
auto main() -> int {
log::configure({.minimum = log::level::debug, .stacktrace_from = log::level::warn});
log::info(std::format("body {} moved to {:.2f}", 7, 12.35)); // formatted by the caller
log::error("a body left the world"); // carries a stack trace
}
Example output:
[2026-10-01 11:32:18] ERROR example: a body left the world (examples/main.cpp:8)
#0 simulate_one_step (examples/main.cpp:8)
#1 main (examples/main.cpp:20)
#2 <unknown>
#3 __libc_start_main
#4 _start
Configuration
log::configure({
.minimum = log::level::debug, // drop everything below this
.stacktrace_from = log::level::error, // capture a stack at/above this
.stacktrace_depth = 16, // max frames kept
.stacktrace_skip = 2, // frames dropped before the caller is found
});
log::current_options() reads it back, log::add_sink(...) adds a destination,
and log::set_sinks({...}) replaces them.
Writing to a file
namespace log = ender::log;
// Archive any existing enderlog.log to enderlog.<timestamp>.log, then start a
// fresh file for this run. Rotate at 64 KiB and keep the last 5 archives.
auto sink = log::add_file_sink("enderlog.log", {.max_file_size = 64 * 1024, .max_archives = 5});
- No appending onto a previous run. On open, an existing non-empty
enderlog.logis renamed toenderlog.<timestamp>.logbefore the new file is created, so every run gets its own file and the previous run's log is preserved. The timestamp is inserted before the extension, which stays last (.logwhen the active file has none). A leftover empty file is simply replaced. file_options::max_file_size(0 disables) rotates the active file mid-run the same way, and never archives an empty file.file_options::max_archives(0 keeps all) deletes the oldest archives beyond the limit.file_options::flush_each_record(on by default) flushes after every record so a crash keeps the tail.file_options::timestamp_format(chrono syntax, default%Y-%m-%d %H:%M:%S) controls the timestamp on each record's header line;file_options::archive_time_format(chrono syntax, default%Y%m%d-%H%M%S) controls the timestamp inserted into archive names. A chrono format string must begin with%(e.g.%Y-%m-%d_%H%M%S).add_file_sinkadds the sink to the global logger and returns it;path()andarchives()expose what it wrote. Thefile_sinkclass can also be used directly and installed withset_sinks.
Packaging
cpack produces a Debian package:
cmake -S . -B build -DENDERLOG_DISTRO=ubuntu26.04
cmake --build build -j
cd build
cpack
# -> libenderlog-dev_0.0.1_amd64_ubuntu26.04.deb
The package installs the static archive, the module interface source and the CMake package config. CI builds it for Ubuntu 26.04 and publishes it as a job artifact.
License
MIT — see LICENSE.