Compare commits
2
Commits
2c348cb590
...
2e284a16e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e284a16e5 | ||
|
|
2f6ff74670 |
Vendored
+13
-6
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user