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.
- **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 `<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.
@@ -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 <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
`enderlog.log` is renamed to `enderlog.<YYYYmmdd-HHMMSS>.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.<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`.
+46 -24
View File
@@ -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<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
* 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<std::chrono::seconds>(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
* `<stem>.<YYYYmmdd-HHMMSS>.log`. The same
* and the old one is preserved as `<stem>.<timestamp><extension>` (`.log`
* 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`.
* `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::seconds>(std::chrono::system_clock::now()));
// Keep the original extension last, with the timestamp in the
// middle: `<stem>.<stamp>[.<n>]<ext>`.
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: `<stem>.<timestamp><extension>`, 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);