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
+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);