reckless

repository·master·Indexed 19 days ago

https://github.com/mattiasflodin/reckless

An extremely low-latency, high-throughput asynchronous logging library for C++. It minimizes call-site overhead by offloading string formatting and I/O to a background thread using a lockless queue. Features include type-safe printf-style syntax, policy-based configuration via severity_log and policy_log, scoped indentation, and customizable writers. It provides mechanisms for handling writer failures through error policies and crash recovery via panic_flush.

Tokens
4.9K
Snippets
11
Records
18
Agent score
68%

What's inside reckless

  1. Understand the performance tradeoffs of reckless

    master

    When choosing reckless, be aware of its intentional design tradeoffs compared to other logging libraries:

    • Dangling Pointers: reckless allows you to pass raw pointers to the background thread. This improves performance because the log string is not prepared at the call site, but it can lead to crashes if the pointer becomes invalid before it is written to disk.
      • Best Practice: Never use raw pointers to dynamically allocated or stack-allocated memory. Pointers to global objects and string literals are generally safe. For dynamically allocated strings, use std::string.
    • Floating-Point Precision: reckless may trade some precision in floating-point output for increased performance.
    • Latency vs. Throughput: reckless aims for stable, low latency. It uses a configurable buffer (defaulting to 64 KiB) to avoid sudden hangs. If the buffer fills up, the caller may experience latency spikes as the logger performs synchronous writes.
    • Crash Safety: reckless is designed to minimize the risk of losing log messages during a crash by flushing whenever possible.
  2. Understand the `basic_log` base class

    master

    The basic_log class is the foundation for all loggers in reckless. It manages the asynchronous infrastructure, including input/output buffers and the background worker thread. It does not provide public functions for direct logging; instead, it provides a protected write<Formatter>(Args&&... args) method intended to be used by derived classes (like policy_log or severity_log).

    Key responsibilities of basic_log include:

    • Managing buffer capacities (input_buffer_capacity and output_buffer_capacity).
    • Handling error policies for temporary and permanent writer failures.
    • Providing mechanisms for panic flushing during crashes.
    • Monitoring buffer health via high-watermarks and full-count metrics.
    // #include <reckless/basic_log.hpp>
    
    class basic_log {
    public:
        basic_log();
        basic_log(writer* pwriter);
        basic_log(writer* pwriter, std::size_t input_buffer_capacity, std::size_t output_buffer_capacity);
        virtual ~basic_log();
    
        void open(writer* pwriter);
        void open(writer* pwriter, std::size_t input_buffer_capacity, std::size_t output_buffer_capacity);
        virtual void close(std::error_code& ec) noexcept;
        virtual void flush(std::error_code& ec);
        // ... other members
    
    protected:
        template <class Formatter, typename... Args>
        void write(Args&&... args);
    };
  3. How Reckless achieves low latency

    master

    Reckless is an asynchronous logging library designed for extremely low latency and high throughput.

    The Mechanism: When a logging call is made, the library only pushes the arguments onto a shared, lockless queue. The actual string formatting and I/O operations are performed asynchronously by a separate background thread. This design provides several benefits:

    • Minimal Call-site Cost: In non-contended cases, the cost is roughly equivalent to a standard function call.
    • No Kernel Transitions: By avoiding kernel calls at the call site, it prevents CPU cache pollution and allows non-logging code to run faster.
    • Lockless Synchronization: No locks are taken between threads unless the queue fills up.
    • Asynchronous Formatting & I/O: Text formatting and I/O wait times are moved off the main execution path.
    • Batching: During log bursts, multiple items can be batched into a single I/O operation to improve throughput.
  4. Important caveats of asynchronous logging in Reckless

    master

    Because formatting and I/O are handled asynchronously in a single background thread, users must be aware of the following:

    • Data Lifetime: If you pass arguments by reference or pointer, you must ensure the referenced data remains valid until the log is flushed or closed. For dynamically allocated data, use std::string, std::shared_ptr, or std::unique_ptr to ensure safety.
    • Crash Handling: In the event of a crash, log data currently in the queue may be lost. You should use the library's provided convenience functions to aid in flushing data during critical failures.
    • Scalability Limits: Since all formatting occurs in a single background thread, extremely high volumes of parallel log entries or very expensive formatting could theoretically become a bottleneck.
    • Predictability: Performance measurement may be less predictable as the OS might suspend other threads to allow the logging thread to run.
  5. Handle high-load logging bursts

    master

    In scenarios where log messages are generated as fast as possible (a "call burst"), the buffer will eventually fill up. When this happens, the caller must wait for data to be written, causing latency spikes.

    To mitigate this:

    1. Enlarge the buffer: A larger queue size takes longer to fill, though the eventual spike when it does fill will be larger.
    2. Monitor performance: Use the provided performance counters to measure how often the buffer fills up and adjust your configuration accordingly.
  6. Build Reckless using CMake

    master

    To build the library using CMake:

    mkdir build; cd build
    cmake ..
    make

    Integrating Reckless into your own CMake project:

    1. Add the Reckless directory as a subdirectory: add_subdirectory(path/to/reckless)

    2. Link your executable to reckless and pthread: target_link_libraries(your_executable reckless pthread)

  7. Build Reckless using Visual Studio (Windows)

    master

    On Windows, it is recommended to use Visual Studio:

    1. Open reckless.sln.
    2. Select "batch build" and choose "select all".
    3. Press Build.

    Library files will be placed in the build subdirectory.

    Project Configuration:

    • Include Path: Set to $(RECKLESS)/reckless/include (where RECKLESS is the path to the source directory).
    • Library Path: Point to the appropriate library build for your configuration.
  8. Build Reckless using Make (Linux/GCC)

    master

    To build the library using GNU Make, clone the git repository and run make. This requires a GCC-compatible compiler.

    Compiling a program against Reckless: Assuming RECKLESS is an environment variable pointing to the reckless root directory, use the following command structure:

    g++ -std=c++11 myprogram.cpp -I$(RECKLESS)/reckless/include -L$(RECKLESS)/reckless/lib -lreckless -lpthread
  9. Handle program crashes with `panic_flush`

    master

    Because reckless buffers data asynchronously, a crash can result in losing the most recent (and often most important) log entries. To minimize this, use panic_flush in your crash handler.

    Option 1: Using the provided convenience helpers Use install_crash_handler to automatically call panic_flush on a list of loggers when a crash occurs.

    #include <reckless/crash_handler.hpp>
    
    // In your setup code:
    void setup() {
        // This installs a handler that calls panic_flush on g_log
        static scoped_crash_handler handler(&g_log);
    }

    Option 2: Manual integration If you already have a custom crash handler, simply call log.start_panic_flush() (or panic_flush as described in the basic_log API) within it. This puts the log into a "panic" state that prevents standard cleanup in the destructor to ensure the crash-related data is prioritized.

    // #include <reckless/crash_handler.hpp>
    
    void install_crash_handler(std::initializer_list<basic_log*> log);
    void uninstall_crash_handler();
    
    class scoped_crash_handler {
    public:
        scoped_crash_handler(std::initializer_list<basic_log*> log)
        {
            install_crash_handler(log);
        }
        ~scoped_crash_handler()
        {
            uninstall_crash_handler();
        }
    };
  10. Configure error policies for writer failures

    master

    You can control how the logger behaves when the writer encounters errors using temporary_error_policy and permanent_error_policy. The classification of an error as temporary or permanent is determined by the writer implementation.

    Available Policies (error_policy):

    • ignore: Discard messages that could not be written.
    • notify_on_recovery: Discard failed messages but keep a tally. Once the writer recovers, the writer_error_callback is triggered with the count of lost messages.
    • block: Keep messages in the queue. If the queue fills up, any attempt to write to the log will block until the writer succeeds.
    • fail_immediately: As soon as a write fails, the log enters an error state. Subsequent attempts to write will throw a writer_error exception. This is a mechanism for aborting subroutines once it is known that logging is failing.
  11. Configure the reckless buffer size

    master

    The buffer size in reckless is configurable. The default size is 64 KiB.

    • Small Buffers: Provide more frequent flushes and lower latency spikes when the buffer fills, but may lead to more frequent I/O operations.
    • Large Buffers: Can handle larger bursts of data without stalling the application, but if the buffer fills up, the resulting latency spike when flushing will be higher.

    If you experience frequent latency spikes due to sporadic bursts of data, consider enlarging the buffer. The API provides performance counters to monitor how often the buffer fills.

  12. Basic usage of Reckless with severity_log

    master

    To use Reckless, you typically define a logger type using reckless::severity_log with a specific policy, then associate it with a writer (like reckless::file_writer).

    reckless::severity_log is a policy-based logger that allows you to configure fields such as indentation, separators, severity markers, and timestamps.

    Key Features:

    • Type-safe printf-style syntax: Use %p, %s, %d, %f, etc., in a way that is type-safe and extensible.
    • Scoped Indentation: Use reckless::scoped_indent to automatically indent all log lines within a specific C++ scope.
    #include <reckless/severity_log.hpp>
    #include <reckless/file_writer.hpp>
    
    // Define a logger with specific formatting policies
    using log_t = reckless::severity_log<
        reckless::indent<4>,       // 4 spaces of indent
        ' ',                       // Field separator
        reckless::severity_field,  // Show severity marker (D/I/W/E) first
        reckless::timestamp_field  // Then timestamp field
        >;
    
    reckless::file_writer writer("log.txt");
    log_t g_log(&writer);
    
    int main()
    {
        std::string s("Hello World!");
    
        // Type-safe printf-style logging
        g_log.debug("Pointer: %p", s.c_str());
        g_log.info("Info line: %s", s);
    
        for(int i=0; i!=4; ++i) {
            reckless::scoped_indent indent;  // Indents lines within this scope
            g_log.warn("Warning: %d", i);
        }
    
        g_log.error("Error: %f", 3.14);
    
        return 0;
    }