Top C++ Logging Libraries Compared: How to Choose the Best One - part 1

spdlog

spdlog is a veteran (first release in 2016) C++ logging library by Gabi Melman. It uses libfmt for formatting(pre-C++20). It is released under MIT license.

Several highlights of the library:

  • Header-only or compiled

  • Formatting

  • Single- or multi-threaded loggers

  • Log rotation

  • Colored console logging

  • Synchronous and asynchronous logging

  • Filtering by log level at compile time and runtime

Getting ready

The spdlog binary package is available from all popular package managers (apt, brew, dnf, vcpkg, etc.). It is also available via Conan and the source code is maintained on github.

I used CMake FetchContent plugin, so that the package code will be available for me.

For the demo code and picking the right function, I also used cxxopts library for parsing the command line options.

The source code is stored in this github repo, in spdlog_example directory

Basic example

I started with a basic example that was provided in README.md

I had some hard time to get the basic example to work.

Long story short, master branch cannot be used for this example, I used branch v2.x and used the library version (not header-only) in order to make it work (browse CMakeLists.txt here)

Update: the author purged branch v2.x, use v1.x instead, repo updated.

#include "spdlog/spdlog.h"

void basic_usage_example()
{
    spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
    spdlog::info("Welcome to spdlog!");
    spdlog::error("Some error message with arg: {}", 1);
    spdlog::warn("Easy padding in numbers like {:08d}", 12);
    spdlog::critical("Support for int: {0:d};  hex: {0:x} oct: {0:o} bin: {0:b}", 42);
    spdlog::info("Support for floats {:03.2f}", 1.23456);
    spdlog::info("Positional args are {1} {0}..", "too", "supported");
    spdlog::info("{:<30}", "left aligned");
    spdlog::set_level(spdlog::level::debug); // Set global log level to debug
    spdlog::debug("This message should be displayed..");    
    spdlog::trace("This message shouldn't be displayed..");    
}

This is the output I got when I run this function

As can be seen, since the log level is set to debug, the last line was not displayed indeed.

Note: When setting the log format, the syntax is based on the libfmt syntax.

Also, when no logger object is explicitly created, the default logger object methods are invoked, which in this case is a console logger.

Actually a call to spdlog::X() is an alias for spdlog::default_logger()→X()

Nice, so we can log to console, and even with colors.

Now, let’s dive into how the logging works

How the logging works

Let’s create a logger step by step

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

#include <memory>

void handmade_logger()
{
    auto logger = std::make_shared<spdlog::logger>("handmade-logger");
    logger->sinks().push_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
    logger->sinks().push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/basic_sink.txt"));
    logger->set_level(spdlog::level::debug);

    logger->log(spdlog::level::trace, "TRACE");
    logger->log(spdlog::level::debug, "DEBUG");
    logger->log(spdlog::level::info, "INFO");
    logger->log(spdlog::level::warn, "WARNING");
    logger->log(spdlog::level::err, "ERROR");
    logger->log(spdlog::level::critical, "CRITICAL");
}Let’s explore what happens line by line.

Let’s explore what happens line by line.

The first line creates a pointer to a logger object that will serve us. The logger type is of spdlog::logger type. The constructor parameter is the logger ID, it is stored in the global log registry as the key for retrieval of this specific logger object, and also will appear in the log.

The second and third line are adding sinks for the logger. The sinks can be seen as output type (console, file, tcp, rotating, etc.). A pointer to sink object is created and added to the logger sinks. The mt suffix stands for multi-threaded (i.e. thread-safe), each sink has also single-threaded variant, marked with st suffix. While the stdout sink has no constructor parameters, obviously the file sink has to receive the file name.

Next line is setting the log level, meaning that only messages that are greater or equal to the level will be displayed

The lines after that are pretty self-explanatory, logging according to the desired level.

The output:

Nice…but a lot of boilerplate code.

Luckily, spdlog provides us with a nice shortcuts to save the log creation, sink creation, and even mentioning the log level as argument to the log() function, let’s see an example for the stdout logger

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

void console_logger()
{
    auto logger = spdlog::stdout_color_mt("stdout");

    logger->info("This is an info log message");
}

The first line saves us the cumbersome process of creating a logger, a sink, and binding them one to another.

The second line is actually an alias for logger→log(spdlog::level::info, ““);

There are several more sinks, such as timed flushing(daily, hourly), network (TCP/UDP), mongo and kafka and more.

Stopwatch

Another handy feature of spdlog is the stopwatch. It is not directly related to logging, but it is a nice utility to measure the elapsed time. It is somehow similar to boost::progress_timer, except the fact that progress_timer stops and prints on destruction, i.e. going out of scope, and the spdlog stopwatch allows to log the stopwatch state at any time. Let’s see an example

#include "spdlog/spdlog.h"
#include "spdlog/stopwatch.h"

#include <thread>

void stopwatch()
{
    using namespace std::literals;

    spdlog::stopwatch sw;
    spdlog::info("Elapsed {:.3}", sw);
    std::this_thread::sleep_for(3.5s);
    spdlog::info("Elapsed {}", sw);
}

Very simple, just create the stopwatch object, and log the object state easily, the output is given below

[2024-10-26 13:18:17.567] [info] Elapsed 1.3e-05
[2024-10-26 13:18:21.068] [info] Elapsed 3.500265971

Async logging

spdlog allows to create a logger without a dedicated thread, which relies on the system thread pool. It can be used to isolate the application that uses the logger from unexpected spikes, such as slow disk I/O or network latency. In general, the creator recommends to use the regular synchronous logging mechanism, unless there is a specific need.

A simple example is shown below

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

void async_logger()
{
    auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");
    for (auto i = 1; i < 42; ++i)
    {
        async_file->info("Async message #{}", i);
    }

    // Under VisualStudio, this must be called before main finishes to workaround a known VS issue
    spdlog::drop_all(); 
}

Logging user-defined objects

The last feature of spdlog that I would like to demonstrate is logging a user-defined object. Suppose we have a class in our application or library, a class that in some point we need to log its’ state (or part of it).

A specialized std::formatter for our class should be defined (if C++20 std::format is used, otherwise fmt::formatter) as shown in the code snippet below

#include "spdlog/spdlog.h"
#include "spdlog/fmt/fmt.h"

#include <string>
#include <vector>
#include <iostream>

struct custom_loggable
{
    explicit custom_loggable(const std::string& input_str, const std::vector<std::uint32_t>& input_values)
        : str{ input_str }
        , values{ input_values }
    {}

    std::string str;
    std::vector<std::uint32_t> values;
};

template<>
struct fmt::formatter<custom_loggable> : fmt::formatter<std::string>
{
    auto format(custom_loggable loggable, format_context &ctx) const -> decltype(ctx.out())
    {
        std::string vector_str;
        vector_str  = "[ ";
        for (const auto& entry : loggable.values)
        {
            vector_str += std::format("{} ", entry);
        }

        vector_str += "]";

        return fmt::format_to(ctx.out(), "custom_loggable(str: '{}', values: {})", loggable.str, vector_str);
    }
};

void user_defined_object_logger()
{
    custom_loggable cl{ "Lost", { 4, 8, 15, 16, 23, 42 } };
    spdlog::info("user defined type: {}", cl);
}

And the output is as below

[2024-10-27 00:14:08.966] [info] user defined type: custom_loggable: str = Lost, values = [ 4 8 15 16 23 42 ]

Benchmarks

The creator of spdlog provides benchmarks and code for several loggers, can be built using SPDLOG_BUILD_BENCH flag in CMakeLists.txt

Conclusion

The spdlog is a nice library, providing many useful features, with a good modern interface. The main drawback of the library in my opinion is that the README.md and the documentation at all is not detailed enough, however the basic usage is pretty intuitive.

In the next chapter I will cover glog by Google.

Stay tuned

1
Subscribe to my newsletter

Read articles from Alexander Kushnir directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alexander Kushnir
Alexander Kushnir