Binary and encoded logs¶
BinarySink writes raw bytes; EncodedFileSink writes entries in binary and leaves the
formatting to a decoder. Both share the lock-free queue and the writer thread with the
text sinks described under Sinks.
| To capture | Sink | Read the file with |
|---|---|---|
| Packet or application payload bytes | BinarySink | Your application's parser |
| Log entries with timestamps and arguments | EncodedFileSink | slick_log_decode or EncodedLogReader |
Binary logging¶
BinarySink writes raw bytes to a file exactly as given, so an application can capture packets, wire messages, or serialized records through the same asynchronous queue as its text logs, with no second I/O path and no formatting on the writer thread.
Wrap bytes in as_binary() and log them like any other argument:
#include <slick/logger.hpp>
using namespace slick::logger;
#pragma pack(push, 1)
struct Trade {
uint64_t timestamp_ns;
uint32_t quantity;
double price;
char symbol[8];
};
#pragma pack(pop)
int main() {
auto& logger = Logger::instance();
auto capture = logger.add_binary_sink("trades.bin", "capture");
logger.init(1 << 16);
Trade trade{1'725'000'000'000'000'000ULL, 100, 431.25, {'A','A','P','L'}};
// A POD, by address and size
LOG_SINK_INFO(capture, "{}", as_binary(&trade, sizeof(trade)));
// Or any contiguous range: vector, array, span
std::vector<uint8_t> frame{0xFE, 0xED, 0x00, 0x0A};
LOG_SINK_INFO(capture, "{}", as_binary(frame));
logger.shutdown(); // trades.bin is exactly 28 + 4 bytes
}
add_binary_sink() returns the sink so it can go straight into LOG_SINK_*.
What lands in the file¶
Only the binary payloads. Timestamp, level, format string and non-binary arguments are all ignored, and the file is exactly the concatenation of what you encoded. Entries with no binary argument write nothing. The file has no framing of its own, so if you need to walk it back record by record, encode that yourself or override write_payload():
class LengthPrefixedBinarySink : public BinarySink {
public:
using BinarySink::BinarySink;
protected:
void write_payload(const std::byte* data, size_t size) override {
const auto length = static_cast<uint16_t>(size);
BinarySink::write_payload(reinterpret_cast<const std::byte*>(&length), sizeof(length));
BinarySink::write_payload(data, size);
}
};
Text sinks¶
Text sinks render payloads as hex, so the same call can feed a binary capture and a readable log. {} gives lowercase hex, {:X} uppercase, and {:.N} renders at most N bytes. The default cap is 64 bytes, after which the total byte count is appended (0a1bff...(100 bytes)).
Payloads larger than 64KB¶
A single payload is capped at slick::logger::kMaxPayloadBytes (65534) and truncated beyond it, because slick-queue packs its reservation size into 16 bits. To log more, pass the buffer as several arguments of a single log call:
constexpr auto kCap = kMaxPayloadBytes;
LOG_SINK_INFO(capture, "{}{}{}",
as_binary(p, kCap),
as_binary(p + kCap, kCap),
as_binary(p + 2 * kCap, size - 2 * kCap));
BinarySink writes an entry's payloads back to back with nothing between them, so the file is byte-identical to the unsplit buffer.
Do not spread the chunks over separate log calls. The queue is multi-producer, so another thread's entry can land between them and interleave the two payloads in the file. One entry is the unit of atomicity, which caps a single payload at SLICK_LOGGER_MAX_ARGS * kMaxPayloadBytes (~1.28 MB by default; raise SLICK_LOGGER_MAX_ARGS for more).
Notes¶
- The bytes are copied into the string ring while the log call runs, so the source buffer only has to outlive that call.
as_binary()over a range copies the element bytes as they are, padding included.- Binary payloads cross process boundaries like any other argument, so a shared-memory collector can own the
BinarySink. - A
BinarySinkis dedicated by default, so broadcastLOG_*entries do not reach it. Callset_dedicated(false)to change that.
Runnable example: examples/binary_sink_example.cpp.
Encoded log files¶
EncodedFileSink writes entries in binary and leaves the formatting for later. A text sink spends most of the writer thread's time turning entries into text. This one writes what the entry holds: timestamp, level, thread, pid and tag, source location, and each argument as its bytes. A format string or file name goes in once, and entries refer to it by id after that.
auto& logger = Logger::instance();
logger.add_encoded_file_sink("app.slog");
logger.init(1 << 16);
LOG_INFO("order {} filled {} @ {}", id, qty, price);
slick_log_decode renders the file, each line exactly as FileSink would have written it. Build it with -DBUILD_SLICK_LOGGER_DECODER=ON.
slick_log_decode app.slog -o app.log # stdout without -o
slick_log_decode app.slog --pattern "%T.%e %l %v"
It exits with 1 when a crash cut the last record short, after writing everything before it. In code, EncodedLogReader hands back each LogEntry for any sink to render:
EncodedLogReader reader("app.slog");
FileSink text("app.log");
while (const LogEntry* entry = reader.next()) text.write(*entry);
Writer thread cost per entry on Windows/MSVC /O2, with the entries already queued when it starts. About 50-100 ns of each is the writer reading and merging the rings.
| Entry | FileSink | EncodedFileSink | Speed-up, 1 / 4 / 8 producers | Bytes per entry, text / encoded |
|---|---|---|---|---|
| No arguments | 292 ns | 133 ns | 2.2x / 2.0x / 2.5x | 70 / 28 |
| Four scalars | 513 ns | 150 ns | 3.4x / 3.2x / 3.5x | 93 / 45 |
| Three scalars and a string | 611 ns | 141 ns | 4.3x / 3.6x / 3.4x | 100 / 64 |
| Eight scalars | 888 ns | 172 ns | 5.2x / 4.6x / 4.4x | 95 / 80 |
Each sink starts its session with a header, so appending to an existing file is fine. It keeps up to 16 MiB of format strings and file names to refer to by id (set_text_table_limit()), and writes newer text inline in each entry. A collector's entries arrive with their text copied, so the sink matches it by content, which costs a little more than a LOG_* literal matched by its address. The collector writes one with --encoded <path>.
RotatingEncodedFileSink rotates by size and DailyEncodedFileSink by date, with the same RotationConfig and file names as RotatingFileSink and DailyFileSink. Add them with add_rotating_encoded_file_sink() and add_daily_encoded_file_sink(), or with --rotating-encoded and --daily-encoded on the collector. Every file starts with a header and defines the texts it uses, so each one decodes alone. Pass several to decode them together, oldest first:
The daily sink switches files when an entry's timestamp reaches local midnight, so each entry lands in its own date's file. The check is one comparison per entry.