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>).
This commit is contained in:
EnderTheCoder
2026-10-01 01:05:21 +08:00
parent 2c348cb590
commit 2f6ff74670
2 changed files with 59 additions and 30 deletions
+13 -6
View File
@@ -23,8 +23,8 @@ engine, not on a logging framework.
(for tests and in-game consoles) ship; `sink` is a small interface. (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, - **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 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 and bound how many onto a previous run's log. It can also rotate by size, bound how many archives
archives are kept. are kept, and format both the record timestamp and the archive names.
- **No stacktrace? No problem.** Where `<stacktrace>` is missing (libc++, and - **No stacktrace? No problem.** Where `<stacktrace>` is missing (libc++, and
therefore every cross target), the module still compiles and records still therefore every cross target), the module still compiles and records still
carry their call site — they simply have no stack. carry their call site — they simply have no stack.
@@ -104,7 +104,7 @@ auto main() -> int {
Example output: Example output:
``` ```
[11:32:18] ERROR example: a body left the world (examples/main.cpp:8) [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) #0 simulate_one_step (examples/main.cpp:8)
#1 main (examples/main.cpp:20) #1 main (examples/main.cpp:20)
#2 <unknown> #2 <unknown>
@@ -137,14 +137,21 @@ auto sink = log::add_file_sink("enderlog.log", {.max_file_size = 64 * 1024, .max
``` ```
- **No appending onto a previous run.** On open, an existing non-empty - **No appending onto a previous run.** On open, an existing non-empty
`enderlog.log` is renamed to `enderlog.<YYYYmmdd-HHMMSS>.log` before the new `enderlog.log` is renamed to `enderlog.<timestamp>.log` before the new file is
file is created, so every run gets its own file and the previous run's log is created, so every run gets its own file and the previous run's log is
preserved. A leftover empty file is simply replaced. 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 - `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` same way, and never archives an empty file. `file_options::max_archives`
(0 keeps all) deletes the oldest archives beyond the limit. (0 keeps all) deletes the oldest archives beyond the limit.
- `file_options::flush_each_record` (on by default) flushes after every record so - `file_options::flush_each_record` (on by default) flushes after every record so
a crash keeps the tail. 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 - `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 `archives()` expose what it wrote. The `file_sink` class can also be used
directly and installed with `set_sinks`. directly and installed with `set_sinks`.
+46 -24
View File
@@ -54,6 +54,9 @@ export namespace ender::log {
return "?"; return "?";
} }
/** Default timestamp rendered on a record, chrono format syntax. */
inline constexpr std::string_view default_time_format{"%Y-%m-%d %H:%M:%S"};
/** Logger configuration. */ /** Logger configuration. */
struct options { struct options {
/** Records below this level are dropped before anything is built. */ /** Records below this level are dropped before anything is built. */
@@ -90,15 +93,32 @@ export namespace ender::log {
}; };
namespace detail { namespace detail {
/**
* Render a time point with a runtime chrono format string.
*
* `std::format`'s format string is compile-time only, so the spec is
* wrapped in a replacement field and fed to `std::vformat`; a bare spec
* would be read as literal text rather than a chrono conversion.
*/
[[nodiscard]] inline auto format_time(const std::chrono::system_clock::time_point time,
const std::string_view time_format) -> std::string {
const auto moment = std::chrono::floor<std::chrono::seconds>(time);
const auto pattern = std::string{"{:"}.append(time_format).append("}");
return std::vformat(pattern, std::make_format_args(moment));
}
/** /**
* Render one record as a human-readable block: a header line and, when * Render one record as a human-readable block: a header line and, when
* present, the indented stack frames. Shared by the stream sinks. * present, the indented stack frames. Shared by the stream sinks.
*
* @param time_format A chrono format string applied to the record's
* timestamp; defaults to the date and time of day.
*/ */
[[nodiscard]] inline auto format_record(const record &entry) -> std::string { [[nodiscard]] inline auto format_record(const record &entry,
auto text = std::format("[{:%H:%M:%S}] {:<8} {}", const std::string_view time_format = default_time_format)
std::chrono::floor<std::chrono::seconds>(entry.time), -> std::string {
to_string(entry.severity), const auto stamp = format_time(entry.time, time_format);
entry.message); auto text = std::format("[{}] {:<8} {}", stamp, to_string(entry.severity), entry.message);
if (!entry.file.empty()) { if (!entry.file.empty()) {
text += std::format(" ({}:{})", entry.file, entry.line); text += std::format(" ({}:{})", entry.file, entry.line);
} }
@@ -125,15 +145,18 @@ export namespace ender::log {
/** Writes a human-readable line per record to a stream (stderr by default). */ /** Writes a human-readable line per record to a stream (stderr by default). */
class console_sink final: public sink { class console_sink final: public sink {
public: public:
explicit console_sink(std::ostream &stream = std::cerr): stream_(&stream) {} explicit console_sink(std::ostream &stream = std::cerr,
std::string time_format = std::string{default_time_format})
: stream_(&stream), time_format_(std::move(time_format)) {}
auto write(const record &entry) -> void override { auto write(const record &entry) -> void override {
*stream_ << detail::format_record(entry); *stream_ << detail::format_record(entry, time_format_);
stream_->flush(); stream_->flush();
} }
private: private:
std::ostream *stream_; std::ostream *stream_;
std::string time_format_;
}; };
/** Keeps every record in memory; useful for tests and in-game consoles. */ /** Keeps every record in memory; useful for tests and in-game consoles. */
@@ -174,6 +197,10 @@ export namespace ender::log {
std::size_t max_file_size{0}; std::size_t max_file_size{0};
/** Keep at most this many archives, dropping the oldest first; 0 keeps them all. */ /** Keep at most this many archives, dropping the oldest first; 0 keeps them all. */
std::size_t max_archives{0}; std::size_t max_archives{0};
/** Timestamp format used on each record's header line (chrono syntax). */
std::string timestamp_format{std::string{default_time_format}};
/** Chrono format for the timestamp inserted into archive names. */
std::string archive_time_format{"%Y%m%d-%H%M%S"};
}; };
/** /**
@@ -182,8 +209,9 @@ export namespace ender::log {
* `path` is the active file. When the sink opens it and the file already * `path` is the active file. When the sink opens it and the file already
* holds data, that file is renamed to a timestamped archive first, so a run * holds data, that file is renamed to a timestamped archive first, so a run
* never appends onto a previous run's log: every start begins a fresh file * never appends onto a previous run's log: every start begins a fresh file
* and the old one is preserved with the timestamp before its extension, as * and the old one is preserved as `<stem>.<timestamp><extension>` (`.log`
* `<stem>.<YYYYmmdd-HHMMSS>.log`. The same * when the active file has no extension), where the timestamp is rendered by
* `file_options::archive_time_format` (default `<YYYYmmdd-HHMMSS>`). The same
* happens mid-run once the active file passes `file_options::max_file_size`. * happens mid-run once the active file passes `file_options::max_file_size`.
* `file_options::max_archives` bounds how many archives are kept. * `file_options::max_archives` bounds how many archives are kept.
* *
@@ -206,7 +234,7 @@ export namespace ender::log {
} }
auto write(const record &entry) -> void override { auto write(const record &entry) -> void override {
const auto block = detail::format_record(entry); const auto block = detail::format_record(entry, options_.timestamp_format);
// Rotate before writing, but never rotate an empty file: that would // Rotate before writing, but never rotate an empty file: that would
// archive nothing and lose the record that is about to be written. // archive nothing and lose the record that is about to be written.
if (options_.max_file_size > 0 && size_ > 0 && size_ + block.size() > options_.max_file_size) { if (options_.max_file_size > 0 && size_ > 0 && size_ + block.size() > options_.max_file_size) {
@@ -237,23 +265,17 @@ export namespace ender::log {
auto archive_current() -> void { auto archive_current() -> void {
if (stream_.is_open()) stream_.close(); if (stream_.is_open()) stream_.close();
const auto stamp = std::format("{:%Y%m%d-%H%M%S}", const auto stamp = detail::format_time(std::chrono::system_clock::now(), options_.archive_time_format);
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now())); // Keep the extension last: `<stem>.<timestamp><extension>`, falling
// Keep the original extension last, with the timestamp in the // back to `.log` when the active file has none.
// middle: `<stem>.<stamp>[.<n>]<ext>`. const auto stem = path_.stem().string();
const auto name = [&](const std::size_t counter) { const auto extension = path_.has_extension() ? path_.extension().string() : std::string{".log"};
auto candidate = path_.parent_path() / path_.stem(); auto archive = path_.parent_path() / (stem + "." + stamp + extension);
candidate += ".";
candidate += stamp;
if (counter > 0) candidate += std::format(".{}", counter);
candidate += path_.extension();
return candidate;
};
auto archive = name(0);
// Two rotations can land in the same second; disambiguate with a // Two rotations can land in the same second; disambiguate with a
// counter rather than overwrite the earlier archive. // counter rather than overwrite the earlier archive.
for (auto counter = 1; std::filesystem::exists(archive); ++counter) { for (auto counter = 1; std::filesystem::exists(archive); ++counter) {
archive = name(counter); archive = path_.parent_path() /
(stem + "." + stamp + "." + std::to_string(counter) + extension);
} }
std::filesystem::rename(path_, archive); std::filesystem::rename(path_, archive);
archives_.push_back(archive); archives_.push_back(archive);