Compare commits
2
Commits
v0.10.0
...
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.
|
(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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user