|
StormByte C++ Library: Logger module 1.2.0
StormByte-Logger is the logging module of the StormByte C++ suite.
|
This repository is StormByte Logger: stream logging for the StormByte C++ suite.
It depends on StormByte Base 1.2.0 or newer. Public headers live under StormByte/logger/ and cover Log, ThreadedLog, header formats, hierarchical components, Scope facades, groups, colors, temporary formats, human-readable numbers, redaction, hex dumps and binary payloads.
The suite is split on purpose. Base, Buffer, Config, Crypto, Database, Network and System are other repositories. This one does not implement them.
operator<< facade with a minimum print Level. Copies and Scope facades share the same backend.LowLevel, Debug, Warning, Notice, Info, Error, Fatal. Warning, Error and Fatal are always emitted.L level, T timestamp, i thread id, c component path, g group, %% literal %.component("A") pushes a segment; nested calls join with / (Multimedia/Decoder). pop_component pops one segment. reset_component clears the stack. component("") does not push.log.Scope("Multimedia/Decoder") returns a std::shared_ptr<Log> facade with a sticky path. Nested Scope("Encoder") joins relative to the parent. Config methods without a component argument bind to that sticky path (root facade = global). The facade shares the backend and, on ThreadedLog, the line lock.group("name") labels, cleared by a newline.color, color(Color::X) and nocolor content manipulators. Disabled by default.push_format("...") / pop_format.humanreadable_number, humanreadable_bytes, nohumanreadable.redact / redact(N) keep last N, redact_first(N) keep first N, noredact.hex / hex(N) dumps payload bytes as 0xAA with N bytes per row (default 16). nohex restores the default. Applies to every subsequent payload, including numbers (text bytes, not numeric hex).std::span<const std::byte> (and std::vector<std::byte>) print as Base64 by default, or as a hex dump when hex is active.Log is single-threaded. Share a logger across threads only via ThreadedLog.| Module | Role | API |
|---|---|---|
| Base | Exceptions, Expected, serialization, strings, UUID, concepts | /StormByte |
| Buffer | FIFO, SharedFIFO, Ring, Producer/Consumer and multi-stage pipelines | /StormByte-Buffer |
| Config | Human-readable text and versioned binary documents (groups, lists, raw bytes) | /StormByte-Config |
| Crypto | Hash, compress, encrypt, sign and key agreement — Crypto++ never leaves the private tree | /StormByte-Crypto |
| Database | One API over SQLite, PostgreSQL and MariaDB | /StormByte-Database |
| Logger | This repository | /StormByte-Logger |
| Network | Framed packets, Client/Server, IPv4/IPv6 TCP and Buffer pipelines (compress/encrypt) | /StormByte-Network |
| System | Processes, pipes and environment variables across Linux, Windows and macOS | /StormByte-System |
StormByte/logger/): https://dev.stormbyte.org/StormByte-Logger/.Other modules that take a std::shared_ptr<StormByte::Logger::Log> should point here rather than re-document the logger. The print floor is chosen by the application, not by the library that logs.
StormByte::Logger::Level is the only severity type. It is used twice:
Warning, Error and Fatal are always written.operator<<(Level) on a line. That sets the level of everything until the next Level or the end of the line.Order is least severe → most severe (this is not syslog):
| Level | Value | When to use |
|---|---|---|
LowLevel | 0 | High-volume diagnostics: per-unit PTS/DTS, wait/wake, hopper chatter. Expect slowness if the floor is this low. |
Debug | 1 | Useful but quieter: binds, reserves, work summaries, codec open. |
Warning | 2 | Recoverable problems that did not fail the job. |
Notice | 3 | Significant normal events: created, opened path, eof, closed. Keep this quiet. |
Info | 4 | Job-level information, such as a completed operation. |
Error | 5 | Error conditions. |
Fatal | 6 | Unrecoverable errors. |
A message is emitted when message_level >= print_floor, or when its level is Warning, Error or Fatal.
Examples:
Info prints Info, Warning, Error and Fatal. It does not print Notice, Debug or LowLevel.Debug prints Debug, Warning, Error and Fatal. It does not print Notice or LowLevel.LowLevel prints everything.LevelToString(Level) returns the short name used in L ("LowLevel", "Debug", "Notice", …). The header pads the name to 8 characters.
Payload operator<< for ordinary filtered levels returns immediately below the floor. Warning, Error and Fatal remain enabled. Setting a Level, applying a manipulator, or writing std::endl is still forwarded so logger state stays consistent.
Enabled(Level) asks whether that level would pass the print floor (including the Warning/Error/Fatal exception). It does not open a line and does not consult throttle. Use it to skip building a payload. Throttle still runs later on PrepareLine if you do write.
Third constructor argument. Specifiers:
| Token | Meaning |
|---|---|
L | Current message level, padded to 8 characters |
T | Local timestamp dd/mm/YYYY HH:MM:SS |
i | std::this_thread::get_id() |
c | Component path for the current line (stack join or Scope sticky path) |
g | Group for the current line, if any |
%% | A literal % |
Default format is "[%L] %T". A typical multi-thread format is "[%L] %T" or "[%L %i] %T". Components and groups are opt-in: use "[%L] %T %c %g" when you want them.
The logger writes the header once per line, then the payload, then the newline manipulator.
A component format override can introduce c / g even when the general format does not contain them. Longest matching path wins; then the general format.
Needs a C++26 compiler, CMake 3.28 or newer, and StormByte Base 1.2.0 or newer.
Link StormByte-Logger (and Base). Include path: the public install prefix, headers as #include <StormByte/logger/….hxx>.
Headers are #include <StormByte/logger/….hxx>. Namespace root is StormByte::Logger.
operator<< unpacks std::shared_ptr / std::unique_ptr whose element type derives from Log (Log and ThreadedLog). *tlog << still works.
Log and ThreadedLog accept any std::ostream (std::cout, a file stream, a string stream).
Streamed payload types: bool, the standard integer and floating types, char / unsigned char / wchar_t, const char*, const wchar_t*, std::string_view, std::wstring_view, std::span<const std::byte>. std::string and std::wstring convert to those views. std::vector<std::byte> converts to the span. There is no separate operator<<(const std::string&). There is no std::format overload on the logger itself; format first, then stream the view or string.
A line is:
<< Level selects the message level and starts (or restarts) the line.std::endl (or any stream manipulator that writes a newline) ends the line, prints the header if needed, and is the point at which ThreadedLog drops the line lock.Do not start a line without a Level if you care about the filter. Do not omit the newline: ThreadedLog holds the line lock until one is seen.
Copy and copy-assignment of Log / ThreadedLog share the same Implementation (shared_ptr). That is the intended way to hand one logger to several objects on one thread.
Across threads, construct a ThreadedLog (or std::make_shared<ThreadedLog>) and pass that pointer. Log has no line lock; concurrent operator<< will interleave characters.
The objects store std::shared_ptr<Log>. ThreadedLog is-a Log, so the same pointer type works. Scope facades also share that backend.
State stays until another of these manipulators is applied.
Applies to strings and numbers (numbers are converted first). Stays on until noredact.
| Manipulator | Effect |
|---|---|
redact / redact(0) | Every character becomes * |
redact(N) | Keep the last N characters |
redact_first(N) | Keep the first N characters |
noredact | Disable |
Same contract on ThreadedLog. Hex encoding runs before redaction.
hex dumps every subsequent payload as space-separated 0xHH bytes. hex(N) wraps every N bytes with a raw newline (no new header, line stays open). Default N is 16. hex(0) is the same as nohex.
Numbers are converted to text first, then those text bytes are dumped. It is not a numeric hex printer.
std::span<const std::byte> (and std::vector<std::byte>) is Base64 by default:
On ThreadedLog the Base64 / hex string is built before the line lock, then written as a prepared payload.
ANSI color output is disabled by default. Configure a general color per level with Color(level, color). A component-path rule has priority while that path (or a child of it) is active. Longest matching prefix wins; then the general level color.
color re-enables the configured color for the current level. color(Color::X) temporarily selects an explicit content color, and nocolor suppresses color for subsequent content. The header remains configured and every line ends with an ANSI reset when a color was active.
push_format saves the current format and activates a temporary one. Calls nest, and pop_format restores the most recent saved format. An empty pop is a no-op, and the stack persists across lines until explicitly popped.
Changing the format while a line is active closes that line before the new format is used.
Formats can also be configured persistently per component path:
Effective precedence: push_format first, then the longest matching component path, then the general format. Format("Path", "") removes that override.
On a Scope facade, Format("mask") with no path argument binds to the facade's sticky path. On the root logger it changes the global format.
group("name") labels one line and is rendered by g. A newline clears the group automatically; group("") also selects no group. Without g, the group is intentionally not added to the payload.
component("name") pushes a segment onto a thread-local stack. Nested pushes join with / for c and for config lookup:
component("") does not push. Use reset_component to return to root, or pop_component to drop one segment. The stack is not cleared by endl.
The stack is thread-local, not tied to a Log instance. Two Log objects used by the same thread share it. Start tests and job boundaries with reset_component if a previous caller may have left segments.
To switch to a sibling path, reset (or pop) first. Otherwise component("Media") then component("Other") becomes Media/Other.
Scope is the API intended for libraries that should not touch the TLS stack.
Rules:
nullptr.Scope("Child") joins onto the parent's sticky path. A path that already contains / is joined as given.component on the original logger does not change a Scope facade, and a Scope write does not push onto the TLS stack.std::out, file, throttle table, formats, colors).ThreadedLog facades share the same line lock.Format, Color and Throttle without a component argument bind to the facade path. On the root logger (empty path) they remain global.Format("Multimedia/Decoder", mask) on any logger still sets that path explicitly.Child rules win over parent rules. If the leaf has no format/color, the longest matching ancestor is used, then the general setting.
Throttle is disabled by default. It limits complete logical lines, not payload fragments.
Rules are selected by the most specific matching key. Component matching uses path prefixes (Multimedia matches Multimedia/Decoder). When two rules match, the longer component string wins.
Error and Fatal are never throttled. Warning can be throttled even though it is always visible with respect to the print floor.
Policies are Drop, Sample and Window. Sample(n) admits the first line and then one of every n attempts. Window(keep, period) admits the first keep lines of each count window. Rate/burst can additionally limit the admitted lines by time. rate == 0 && burst == 0 disables the time ceiling; rate > 0 requires burst >= 1, while rate == 0 && burst > 0 allows exactly that initial burst with no refill.
When lines are dropped, the next admitted line is preceded by dropped N messages using the same level, component, group, effective format and color. Call FlushThrottle() at a job boundary to emit pending summaries. FlushThrottle(spec) limits the flush to matching selectors.
Install rules before concurrent writers start.
Other suite modules log through this module. A useful convention is:
Scope("Multimedia") (or a nested Scope("Decoder")), not by repeating component(...) on every line.LowLevel — per-packet / per-frame / wait-wake. Sparse-sample if the volume would drown the log.Debug — binds, reserves, work n/min/max.Notice — created, open path, eof, closed. Must stay low-noise.Info — job close or other application-level completion events.The application chooses the floor. A user who sets LowLevel is asking for noise and the cost that comes with it.
ThreadedLog serializes logical lines, not individual << tokens from one thread.
<< Level starts a line).std::endl).<< Level always updates the current message level. If that level is an ordinary filtered level below the floor, the lock is released immediately after the update; Warning, Error and Fatal remain enabled.Scope facades of a ThreadedLog share that same lock.endl must drop the lock even if another thread just changed the current level. That is required so a filtered LowLevel line cannot leave the lock held and stall every other writer.
Implementation current-level / enabled flags are still process-wide, not thread_local. Do not interleave two unfinished lines on the same logger from two threads without finishing each line with a newline. The supported pattern is: one thread writes a complete line (Level … endl) at a time; ThreadedLog only prevents those complete lines from mixing characters.
The component stack is thread-local. Scope paths are per-facade and do not use that stack. group remains line-scoped.
Issues only on this repository. Fork and open a pull request against master.
GNU Lesser General Public License version 3 or later. See [LICENSE](LICENSE) and https://www.gnu.org/licenses/lgpl-3.0.html.
StormByte is developed in spare time. Sponsorship is optional and does not buy features, priority or support.