spdlog Backtrace Feature and Ring Buffer Usage: Implementation Guide

spdlog's backtrace feature stores recent log messages in a lock-protected ring buffer and emits them on demand via dump_backtrace(), using spdlog::details::backtracer and circular_q components.

The gabime/spdlog library provides a sophisticated mechanism to capture and replay recent log history through its backtrace feature, which utilizes a thread-safe ring buffer to temporarily store messages. This capability is essential for debugging difficult-to-reproduce failures by preserving contextual log messages leading up to an error. Understanding how spdlog implements this backtrace feature and ring buffer usage reveals the internal architecture of the backtracer and circular_q classes that enable this functionality.

Core Architecture Components

The backtracer Class

The spdlog::details::backtracer class, defined in include/spdlog/details/backtracer.h, serves as the primary manager for the backtrace buffer. According to the source code, this class maintains three critical members:

  • std::atomic<bool> enabled_ (line 20): Controls whether message capture is active, allowing lock-free status checks
  • std::mutex mutex_ (line 19): Protects all buffer operations from concurrent access
  • circular_q<log_msg_buffer> messages_ (line 21): The ring buffer instance that stores log message copies

The class provides the foreach_pop routine that flushes stored messages to sinks when dumping the backtrace.

The circular_q Ring Buffer Implementation

The circular_q<T> template in include/spdlog/details/circular_q.h implements the lock-free ring buffer mechanics used by the backtracer. Key implementation details include:

  • Allocation strategy: The constructor (lines 28-31) allocates a vector sized max_items_ + 1 to keep one slot as a "full" marker
  • Overwrite behavior: When the queue reaches capacity, push_back (lines 45-55) silently overwrites the oldest entry
  • FIFO access: Provides front(), pop_front(), size(), and empty() methods (lines 59-85) for ordered traversal

This generic circular queue is instantiated with log_msg_buffer to store formatted log messages.

How Backtrace Capture Works

The backtrace feature operates through a four-phase lifecycle:

  1. Enable: Calling logger.enable_backtrace(N) invokes backtracer::enable, which constructs a circular_q<log_msg_buffer> with capacity N (lines 30-34 of backtracer-inl.h) and sets enabled_ to true

  2. Capture: Every log call invokes backtracer::push_back, which wraps messages in log_msg_buffer objects and pushes them into the ring buffer. If the buffer is full, the oldest entry is dropped

  3. Dump: When logger.dump_backtrace() is called, it invokes backtracer::foreach_pop (lines 53-61 of backtracer-inl.h), which iterates over the queue while holding the mutex, emitting "Backtrace Start" markers, each buffered message, and "Backtrace End" markers

  4. Disable: Calling logger.disable_backtrace() clears the flag and releases the allocated buffer

Thread Safety Mechanisms

All modifications to the backtrace buffer are guarded by the mutex_ member, while the enabled_ atomic flag allows concurrent enable/disable checks without locking. This design ensures that the push_back operation—which occurs on every log call—minimizes contention by only holding the lock during the brief ring buffer update. The foreach_pop method also acquires the mutex during the entire dump operation to ensure consistent message ordering.

Usage Examples

Basic Synchronous Backtrace

The following example demonstrates backtrace usage with a standard synchronous logger:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    spdlog::logger logger("example", console);

    logger.set_pattern("%v");           // only message text
    logger.enable_backtrace(5);         // keep last 5 messages

    logger.info("startup");             // printed immediately
    for (int i = 0; i < 100; ++i)
        logger.debug("debug {}", i);    // stored in backtrace buffer

    logger.dump_backtrace();            // prints backtrace block
}

Asynchronous Logger Backtrace

The same API works with spdlog::async_logger, with the backtracer's internal mutex ensuring correct ordering even when the surrounding logger operates on a thread pool:

#include "spdlog/async.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    spdlog::init_thread_pool(120, 1);               // 120 queue slots, 1 thread
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto async_logger = std::make_shared<spdlog::async_logger>(
        "async", console, spdlog::thread_pool());

    async_logger->set_pattern("%v");
    async_logger->enable_backtrace(5);

    async_logger->info("startup");
    for (int i = 0; i < 100; ++i)
        async_logger->debug("debug {}", i);

    async_logger->dump_backtrace();                 // flushed asynchronously
}

Disabling Backtrace

To stop buffering and release resources:

logger.disable_backtrace();   // stops buffering further messages

Summary

  • spdlog's backtrace feature uses a circular_q ring buffer to store recent log messages in memory
  • spdlog::details::backtracer manages the buffer with a mutex-protected, atomic-enabled design for thread safety
  • Enable backtrace by calling logger.enable_backtrace(size_t) to allocate the ring buffer with a specific capacity
  • Dump backtrace by calling logger.dump_backtrace() to emit buffered messages between start/end markers
  • Works with both synchronous and asynchronous loggers, with the internal mutex ensuring correct ordering across thread pools
  • Oldest messages are overwritten when the ring buffer reaches capacity, implementing a true circular buffer behavior

Frequently Asked Questions

How do I enable backtrace in spdlog?

Call logger.enable_backtrace(N) where N is the number of messages to retain. This allocates a circular_q of that capacity and sets the atomic enabled_ flag to true, causing subsequent log calls to push messages into the buffer while holding the internal mutex.

Is spdlog backtrace thread-safe?

Yes. The backtracer class uses a std::mutex to protect all operations on the circular_q buffer, and an std::atomic<bool> for the enabled flag. This ensures that concurrent logging threads can safely push messages, and dump operations see a consistent view of the buffer.

What happens when the backtrace buffer is full?

When the ring buffer reaches capacity, the push_back method in circular_q automatically overwrites the oldest entry (lines 45-55 of circular_q.h). This silent overrun behavior ensures that the most recent N messages are always retained, which is typically the desired behavior for debugging the events leading to a crash.

Can I use backtrace with asynchronous loggers?

Yes. The backtrace API works identically for spdlog::async_logger instances. The backtracer's internal mutex ensures correct ordering even when the surrounding logger operates on a thread pool with multiple worker threads, as verified in the test suite at tests/test_backtrace.cpp.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →