Files
OpenRA3/third_party/libenderlog/README.md
T
EnderTheCoder 2f6ff74670 chore(vendor): update vendored libenderlog to v0.0.3
Dated record timestamps, per-sink timestamp format, and archive names
that keep the extension last (<stem>.<timestamp>.<ext>).
2026-10-01 01:05:21 +08:00

178 lines
6.4 KiB
Markdown

# 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 atomic `minimum` check 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_location` at 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_from` carry a
formatted `std::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 a `memory_sink`
(for tests and in-game consoles) ship; `sink` is a small interface.
- **File output with archiving.** `file_sink` writes 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
```sh
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`
```cmake
add_subdirectory(libenderlog)
target_link_libraries(my_app PRIVATE enderlog::enderlog)
```
### Installed package
```cmake
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
```cpp
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
```cpp
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
```cpp
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.log` is renamed to `enderlog.<timestamp>.log` before 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
(`.log` when 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_sink` adds the sink to the global logger and returns it; `path()` and
`archives()` expose what it wrote. The `file_sink` class can also be used
directly and installed with `set_sinks`.
## Packaging
`cpack` produces a Debian package:
```sh
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](LICENSE).