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 checksstd::mutex mutex_(line 19): Protects all buffer operations from concurrent accesscircular_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_ + 1to 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(), andempty()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:
-
Enable: Calling
logger.enable_backtrace(N)invokesbacktracer::enable, which constructs acircular_q<log_msg_buffer>with capacityN(lines 30-34 ofbacktracer-inl.h) and setsenabled_totrue -
Capture: Every log call invokes
backtracer::push_back, which wraps messages inlog_msg_bufferobjects and pushes them into the ring buffer. If the buffer is full, the oldest entry is dropped -
Dump: When
logger.dump_backtrace()is called, it invokesbacktracer::foreach_pop(lines 53-61 ofbacktracer-inl.h), which iterates over the queue while holding the mutex, emitting "Backtrace Start" markers, each buffered message, and "Backtrace End" markers -
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_qring buffer to store recent log messages in memory spdlog::details::backtracermanages 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →