quill

repository·master·Indexed 25 days ago

https://github.com/odygrd/quill

A high-performance, asynchronous C++ logging library designed for low-latency, real-time applications. It features a split frontend/backend architecture to minimize caller-thread latency, supports metric publishing to Prometheus, StatsD, and OpenTelemetry, and provides advanced capabilities such as rate-limited macros, Mapped Diagnostic Context (MDC), and crash handling. Built on the {fmt} library, it supports multiple output sinks, custom formatters, and is compatible with CMake, Meson, Bazel, and Android NDK.

Tokens
30.9K
Snippets
69
Records
173
Agent score
84%

What's inside quill

  1. Overview of Quill features

    master

    Quill is a high-performance, asynchronous C++ logging library designed for ultra-low latency. Key features include:

    • Asynchronous Processing: A background thread handles formatting and I/O to keep the main thread responsive.
    • Metric Publishing: Supports publishing metric samples to Prometheus, StatsD, OpenTelemetry, or in-process collectors via the asynchronous backend.
    • Minimal Header Includes:
      • Frontend: Use only Logger.h and LogMacros.h for lightweight logging with minimal dependencies.
      • Backend: Single .cpp file inclusion; no backend code injection into other translation units.
    • Compile-Time Optimization: Ability to eliminate specific log levels at compile time.
    • Customization: Supports custom formatters, multiple output sinks (Console with color, files with rotation, JSON, and custom sinks), and log filtering.
    • Advanced Logging Capabilities:
      • Timestamp-Ordered Logs: Chronologically ordered logs for easier multithreaded debugging.
      • Flexible Timestamps: Support for rdtsc, chrono, or custom clocks.
      • Backtrace Logging: Ring buffer storage for on-demand log display.
      • Mapped Diagnostic Context (MDC): Thread-local key/value context attached to subsequent log lines.
      • Rate-Limited Macros: Use LOG_*_LIMIT or LOGV_*_LIMIT to emit at most once per configured interval per call site.
    • Reliability and Performance:
      • Crash Handling: Built-in signal handler to preserve logs during crashes.
      • Configurable Queue Modes: Options for bounded/unbounded and blocking/dropping with monitoring for dropped messages and blocked threads.
      • Huge Pages Support (Linux): Optimized performance using huge pages on the hot path.
      • Type-Safe API: Built on the {fmt} library.
      • Exception-Free Option: Configurable builds with or without exception handling.
  2. Choose between TSC, System, or User clock sources

    master

    Quill provides three primary timestamping methods for frontend log statements. Choose based on your requirements for performance versus strict ordering:

    1. TSC (Time Stamp Counter): The fastest method. It captures raw __rdtsc values. The backend uses an RdtscClock to periodically sync these with wall time (default every 500ms).

      • Pros: Extremely low latency.
      • Cons: Requires invariant TSC support on your processor. Timestamps may occasionally appear out of order by a few microseconds due to multi-core variations and periodic resyncs. The backend requires a few seconds for initial calibration.
      • Use case: High-performance logging where microsecond-level ordering is less critical than latency.
    2. System: Calls std::chrono::system_clock::now() on the frontend.

      • Pros: Most accurate and immediately usable timestamps. Provides strict chronological ordering. No backend initialization required.
      • Cons: Slower than TSC.
      • Use case: When strict chronological timestamp ordering is required.
    3. User: Uses UserClockSource to provide custom nanosecond timestamps since epoch.

      • Use case: Simulations or scenarios where you need to control the time dimension manually.
  3. Understand Quill Architecture

    master

    Quill is a header-only library consisting of two main components:

    • Frontend: Captures log arguments and metadata from LOG_* statements and places them in a thread-local SPSC (Single Producer Single Consumer) lock-free queue. This ensures no contention between different logging threads.
    • Backend: A separate thread spawned by the library that asynchronously consumes messages from all frontend queues, formats them, and writes them to the configured sinks.
  4. Benchmark Quill's performance and latency

    master

    Quill's performance is measured across several categories: latency (nanoseconds), throughput (messages per second), and compilation time.

    Latency

    Latency is measured in nanoseconds (ns) and reported by percentiles (50th, 75th, 90th, 95th, 99th, 99.9th). Quill provides different queue types that impact latency:

    • Bounded Dropping Queue: Uses a fixed-size buffer; if the buffer is full, messages are dropped.
    • Unbounded Queue: Grows as needed to prevent dropping messages.
    • Unbounded Queue (Log Functions): A mode using log functions instead of macros.

    Throughput

    Throughput is measured by the maximum number of log messages the backend thread can write to a file per second. Quill is designed to efficiently manage log messages across multiple threads.

    Compilation Time

    Quill is designed to minimize compilation overhead. It keeps call-site metadata (file, line, format string, tags) out of the frontend template identity by storing it in a MacroMetadata object. This allows multiple log statements with the same argument types to reuse the same log_statement instantiation.

  5. Understand Quill Architecture (Frontend vs Backend)

    master

    Quill uses a split architecture to minimize latency on the hot path:

    • Frontend (Caller Thread): Uses a lock-free SPSC queue. LOG_* macros binary-serialize arguments directly into the queue. This avoids shared state, contention, and formatting work on the caller thread.
    • Backend (Worker Thread): Drains the SPSC queues, reconstructs log events, performs {fmt} formatting and PatternFormatter processing, and writes to the attached Sinks.
  6. Understand the Quill backend architecture

    master

    The Quill backend operates using a single dedicated backend thread. This thread is responsible for:

    • Formatting log statements.
    • Forwarding metric samples.
    • Performing I/O operations to files.
    • Consuming events from the Single-Producer Single-Consumer (SPSC) queue.
    • Retrieving necessary metadata for each event.
    • Forwarding formatted log messages or metric samples (via Sink::write_metric) to all Sinks associated with the Logger.
  7. Publish metrics via the asynchronous pipeline

    master

    Quill allows publishing metric samples through the same backend worker used for logs. This ensures that hot threads do not pay for metric formatting or export.

    Metric-capable sinks (like the bundled PrometheusSink) receive samples via Sink::write_metric. Metric metadata is registered once and reused via pointer on each publish call to keep the hot path compact.

  8. Detailed Setup with Backend and Frontend APIs

    master

    For explicit control over backend options, logger names, sinks, or formatters, use the quill::Backend and quill::Frontend APIs. You must call quill::Backend::start() to initialize the asynchronous backend.

    #include "quill/Backend.h"
    #include "quill/Frontend.h"
    #include "quill/LogMacros.h"
    #include "quill/Logger.h"
    #include "quill/sinks/ConsoleSink.h"
    #include <string_view>
    
    int main()
    {
      quill::Backend::start();
    
      quill::Logger* logger = quill::Frontend::create_or_get_logger(
        "root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1"));
    
      LOG_INFO(logger, "Hello from {}!", std::string_view{"Quill"});
    }
  9. Log custom types and STL containers

    master

    To log user-defined types, you must provide formatting and serialization:

    • Formatting: Specialize fmtquill::formatter<T> or provide a free format_as(T) function.
    • Serialization: Provide quill::Codec<T> or use Quill's helper macros/codecs.

    Logging STL Containers: To log containers (like std::vector) containing your custom types, define the formatter/codec for the custom type and include the relevant STL container header from quill/std/.

  10. Disable character sanitization for UTF-8 logging

    master

    By default, Quill filters log messages to ensure they contain only printable ASCII characters (space to tilde, plus \n, \t, and \r). Non-printable or non-ASCII characters (like Chinese or Japanese Unicode text) are converted to their hexadecimal representation.

    To allow raw UTF-8 or non-ASCII text, disable sanitization by setting check_printable_char to an empty lambda in BackendOptions.

    quill::BackendOptions backend_options;
    backend_options.check_printable_char = {};  // Disable sanitization
    quill::Backend::start(backend_options);
  11. Create a Logger in Quill

    master

    You cannot instantiate quill::Logger objects directly. Instead, you must use the quill::Frontend to create them with a specified name, Sinks, and a Formatter. Once created, the logger's configuration is immutable due to the library's asynchronous design.

    To modify a logger (e.g., adding or removing sinks), you must remove the existing logger, wait for its removal, and then recreate it with the same name. Note that a newly created logger will have a different memory address, so you must update any stored Logger* references.

    auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
    
    quill::Logger* logger = quill::Frontend::create_or_get_logger("root", std::move(console_sink));
    
    LOG_INFO(logger, "Hello from {}", "library foo");
  12. Automatically flush backtrace logs on error

    master
    When backtrace logging is enabled via LoggerImpl::init_backtrace, Quill automatically flushes the contents of the ring buffer to the log destination whenever a high-severity log message (such as LOG_ERROR) is recorded. This is useful for capturing the sequence of low-level events leading up to a failure.