From 2f6ff74670742729facaefe5dee80e906f598f0f Mon Sep 17 00:00:00 2001 From: EnderTheCoder Date: Thu, 1 Oct 2026 01:05:21 +0800 Subject: [PATCH] chore(vendor): update vendored libenderlog to v0.0.3 Dated record timestamps, per-sink timestamp format, and archive names that keep the extension last (..). --- third_party/libenderlog/README.md | 19 ++++-- third_party/libenderlog/src/ender.log.cppm | 70 ++++++++++++++-------- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/third_party/libenderlog/README.md b/third_party/libenderlog/README.md index 44e7aec..efc5897 100644 --- a/third_party/libenderlog/README.md +++ b/third_party/libenderlog/README.md @@ -23,8 +23,8 @@ engine, not on a logging framework. (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 and bound how many - archives are kept. + 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 `` is missing (libc++, and therefore every cross target), the module still compiles and records still carry their call site — they simply have no stack. @@ -104,7 +104,7 @@ auto main() -> int { 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) #1 main (examples/main.cpp:20) #2 @@ -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 - `enderlog.log` is renamed to `enderlog..log` before the new - file is created, so every run gets its own file and the previous run's log is - preserved. A leftover empty file is simply replaced. + `enderlog.log` is renamed to `enderlog..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`. diff --git a/third_party/libenderlog/src/ender.log.cppm b/third_party/libenderlog/src/ender.log.cppm index 684eb6e..ce54bcf 100644 --- a/third_party/libenderlog/src/ender.log.cppm +++ b/third_party/libenderlog/src/ender.log.cppm @@ -54,6 +54,9 @@ export namespace ender::log { 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. */ struct options { /** Records below this level are dropped before anything is built. */ @@ -90,15 +93,32 @@ export namespace ender::log { }; 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(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 * 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 { - auto text = std::format("[{:%H:%M:%S}] {:<8} {}", - std::chrono::floor(entry.time), - to_string(entry.severity), - entry.message); + [[nodiscard]] inline auto format_record(const record &entry, + const std::string_view time_format = default_time_format) + -> std::string { + const auto stamp = format_time(entry.time, time_format); + auto text = std::format("[{}] {:<8} {}", stamp, to_string(entry.severity), entry.message); if (!entry.file.empty()) { 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). */ class console_sink final: public sink { 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 { - *stream_ << detail::format_record(entry); + *stream_ << detail::format_record(entry, time_format_); stream_->flush(); } private: std::ostream *stream_; + std::string time_format_; }; /** 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}; /** Keep at most this many archives, dropping the oldest first; 0 keeps them all. */ 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 * 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 - * and the old one is preserved with the timestamp before its extension, as - * `..log`. The same + * and the old one is preserved as `.` (`.log` + * when the active file has no extension), where the timestamp is rendered by + * `file_options::archive_time_format` (default ``). The same * happens mid-run once the active file passes `file_options::max_file_size`. * `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 { - 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 // 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) { @@ -237,23 +265,17 @@ export namespace ender::log { auto archive_current() -> void { if (stream_.is_open()) stream_.close(); - const auto stamp = std::format("{:%Y%m%d-%H%M%S}", - std::chrono::floor(std::chrono::system_clock::now())); - // Keep the original extension last, with the timestamp in the - // middle: `.[.]`. - const auto name = [&](const std::size_t counter) { - auto candidate = path_.parent_path() / path_.stem(); - candidate += "."; - candidate += stamp; - if (counter > 0) candidate += std::format(".{}", counter); - candidate += path_.extension(); - return candidate; - }; - auto archive = name(0); + const auto stamp = detail::format_time(std::chrono::system_clock::now(), options_.archive_time_format); + // Keep the extension last: `.`, falling + // back to `.log` when the active file has none. + const auto stem = path_.stem().string(); + const auto extension = path_.has_extension() ? path_.extension().string() : std::string{".log"}; + auto archive = path_.parent_path() / (stem + "." + stamp + extension); // Two rotations can land in the same second; disambiguate with a // counter rather than overwrite the earlier archive. 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); archives_.push_back(archive);