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

glog

glog is a logging library released and maintained by google. According to the official documentation, the library is maintained in C++14 standard, however according to its repo it is still maintained. The library is released under BSD-3-Clause license.

Disclaimer: I am not familiar with this library, and this post describes my explorations and insights on the library.

Main features of the library:

  • cout-like format (<<)

  • Conditional logging

  • Verbose logging

  • Runtime checkers

Getting ready

The glog binary package is available on Conan and vcpkg. The source code is maintained on github. If needed to build from source, both CMake and Bazel build systems are supported

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 glog_example directory.

Basic example

Let’s start logging.

The logs are stored in /tmp directory if not stated otherwise, and the format is log_name.hostname.username.severity.timestamp. Also, there is a softlink of the pattern log_name.severity that points to the real log name (we’ll see it later)

So let’s run the following snippet

void multiple_log_levels()
{
    for (const auto& index  : std::views::iota(0, 10))
    {
        if (index % 3 == 0)
        {
            LOG(INFO) << "The index is now " << index; 
        }
        else if (index % 3 == 1)
        {
            LOG(WARNING) << "The index is now  " << index;
        }
        else if (index % 3 == 2)
        {
            LOG(ERROR) << "The index is now " << index;
        }
    }
}

After running the function, the /tmp directory looks as following, with respect to the chosen name (glog-log-example)

So first of all, we have a “shortened” name that always points to the current log. This comes in hand especially when there are some logs from previous runs and you don’t want to deal with searching for the latest one.

Also, each severity is written to a separate file (to be more precise, the more verbose the level is, the more is written there, we’ll see it right on).

Also, there is a header in the file that provides some logistics like when the log was started, host machine name and run duration. A nice goodie is the log line format that explains exactly which column stands for which parameter.

Another thing worth mentioning is that the error log is logged also to console.

Let’s peek into the error log

Log file created at: 2024/11/07 22:10:02
Running on machine: WLPF46XAH6
Running duration (h:mm:ss): 0:00:00
Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
E20241107 22:10:02.204762 140363142573504 glog_functions.cpp:26] The index is now 2
E20241107 22:10:02.204874 140363142573504 glog_functions.cpp:26] The index is now 5
E20241107 22:10:02.204884 140363142573504 glog_functions.cpp:26] The index is now 8

And in the warning log

Log file created at: 2024/11/07 22:10:02
Running on machine: WLPF46XAH6
Running duration (h:mm:ss): 0:00:00
Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
W20241107 22:10:02.204667 140363142573504 glog_functions.cpp:22] The index is now  1
E20241107 22:10:02.204762 140363142573504 glog_functions.cpp:26] The index is now 2
W20241107 22:10:02.204871 140363142573504 glog_functions.cpp:22] The index is now  4
E20241107 22:10:02.204874 140363142573504 glog_functions.cpp:26] The index is now 5
W20241107 22:10:02.204881 140363142573504 glog_functions.cpp:22] The index is now  7
E20241107 22:10:02.204884 140363142573504 glog_functions.cpp:26] The index is now 8

And finally, in the info log

Log file created at: 2024/11/07 22:10:02
Running on machine: WLPF46XAH6
Running duration (h:mm:ss): 0:00:00
Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
I20241107 22:10:02.204442 140363142573504 glog_functions.cpp:18] The index is now 0
W20241107 22:10:02.204667 140363142573504 glog_functions.cpp:22] The index is now  1
E20241107 22:10:02.204762 140363142573504 glog_functions.cpp:26] The index is now 2
I20241107 22:10:02.204866 140363142573504 glog_functions.cpp:18] The index is now 3
W20241107 22:10:02.204871 140363142573504 glog_functions.cpp:22] The index is now  4
E20241107 22:10:02.204874 140363142573504 glog_functions.cpp:26] The index is now 5
I20241107 22:10:02.204879 140363142573504 glog_functions.cpp:18] The index is now 6
W20241107 22:10:02.204881 140363142573504 glog_functions.cpp:22] The index is now  7
E20241107 22:10:02.204884 140363142573504 glog_functions.cpp:26] The index is now 8
I20241107 22:10:02.204889 140363142573504 glog_functions.cpp:18] The index is now 9

Funny thing is that the default format has a closing “]” bracket without an opening one.

So, as we can see, the more verbose is the log level, the more information is recorded, and actually it makes sense to me, as if I would search for specific errors in a log that comes from the field, I would prefer the errors to be filtered right away.

And if we are already talking about format, let’s try to play with the log formatting

Log formatting

The default log formatting is pretty long, sometimes we don’t need such a prelude. glog allows to customize the formatting using a prefix formatter. This is done by calling google::InstallPrefixFormatter() function in the following way:

static void custom_formatter(std::ostream& s, const google::LogMessage& m, void* data)
{
    s << google::GetLogSeverityName(m.severity())[0]
        << "["
        << std::setw(2) << m.time().hour() << ':'
        << std::setw(2) << m.time().min()  << ':'
        << std::setw(2) << m.time().sec() << "."
        << ' '
        << std::setfill(' ') << std::setw(5)
        << ' '
        << m.basename() << ':' << m.line() << "]";
}
void custom_formatting()
{
    google::InstallPrefixFormatter(&custom_formatter);
    auto answer_to_everything = 42;
    LOG(INFO) << "Found " << num_cookies << " cookies"; 
}

So I decided that I would like to remove the microseconds counter and the thread ID from the prefix, and I got the following log content:

Log file created at: 2024/11/07 22:14:46
Running on machine: WLPF46XAH6
Running duration (h:mm:ss): 0:00:00
Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
I[22:14:46.      glog_functions.cpp:66] The answer to everything is 42

OK, nice…but what about line number 4? The nice one I mentioned earlier that describes the format? I would expect that it would be updated once I update the format!

Update - I’ve opened an issue about that here, will update how it goes\

Conditional and occasional logging

Imagine a situation where you need to log only in a certain condition (for example, if you are measuring a temperature received from a sensor, and the requirement is to log only if the temperature rises above a predefined threshold). glog provides a handy utility called LOG_IF.

Another useful feature is log every N times, for example if same event arrives, and no need to log every event, the developer can decide to log only every 25th event

The developer can also decide to log only the first N events.

And finally, the developer can decide to log each period of time, measured in milliseconds

All these features are demonstrated below

void conditional_logging()
{
    google::InstallPrefixFormatter(&custom_formatter);

    auto threshold = 5;

    LOG(INFO) << "=== Demonstrating conditional logging ===";
    for (const auto& temp : std::views::iota(1, 10))
    {
        LOG_IF(INFO, temp > threshold) << "LOG_IF: Alert, temperature is " << temp;
    }

    LOG(INFO) << "=== Demonstrating every Nth event logging ===";
    for (const auto& event : std::views::iota(1, 100))
    {
        LOG_EVERY_N(INFO, 25) << "LOG_IF_EVERY_N: Got event no. " << event;
    }

    LOG(INFO) << "=== Demonstrating first N times logging ===";
    for (const auto& event : std::views::iota(1, 10))
    {
        LOG_FIRST_N(INFO, 3) << "LOG_IF_FIRST_N: Got event no. " << event;
    }

    LOG(INFO) << "=== Demonstrating every 0.1s logging ===";
    using namespace std::literals;

    for (const auto& count : std::views::iota(1, 10))
    {
        LOG_EVERY_T(INFO, 0.1) << "The thread is still alive, count: " << count;
        std::this_thread::sleep_for(0.2s);
    }
}

And the output will be

Log file created at: 2024/11/08 00:12:10
Running on machine: WLPF46XAH6
Running duration (h:mm:ss): 0:00:00
Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
I[00:12:10.      glog_functions.cpp:76] === Demonstrating conditional logging ===
I[00:12:10.      glog_functions.cpp:79] LOG_IF: Alert, temperature is 6
I[00:12:10.      glog_functions.cpp:79] LOG_IF: Alert, temperature is 7
I[00:12:10.      glog_functions.cpp:79] LOG_IF: Alert, temperature is 8
I[00:12:10.      glog_functions.cpp:79] LOG_IF: Alert, temperature is 9
I[00:12:10.      glog_functions.cpp:82] === Demonstrating every Nth event logging ===
I[00:12:10.      glog_functions.cpp:85] LOG_IF_EVERY_N: Got event no. 1
I[00:12:10.      glog_functions.cpp:85] LOG_IF_EVERY_N: Got event no. 26
I[00:12:10.      glog_functions.cpp:85] LOG_IF_EVERY_N: Got event no. 51
I[00:12:10.      glog_functions.cpp:85] LOG_IF_EVERY_N: Got event no. 76
I[00:12:10.      glog_functions.cpp:88] === Demonstrating first N times logging ===
I[00:12:10.      glog_functions.cpp:91] LOG_IF_FIRST_N: Got event no. 1
I[00:12:10.      glog_functions.cpp:91] LOG_IF_FIRST_N: Got event no. 2
I[00:12:10.      glog_functions.cpp:91] LOG_IF_FIRST_N: Got event no. 3
I[00:12:10.      glog_functions.cpp:94] === Demonstrating every 0.1s logging ===
I[00:12:10.      glog_functions.cpp:113] The thread is still alive, count: 1
I[00:12:10.      glog_functions.cpp:113] The thread is still alive, count: 2
I[00:12:10.      glog_functions.cpp:113] The thread is still alive, count: 3
I[00:12:10.      glog_functions.cpp:113] The thread is still alive, count: 4
I[00:12:10.      glog_functions.cpp:113] The thread is still alive, count: 5
I[00:12:11.      glog_functions.cpp:113] The thread is still alive, count: 6
I[00:12:11.      glog_functions.cpp:113] The thread is still alive, count: 7
I[00:12:11.      glog_functions.cpp:113] The thread is still alive, count: 8
I[00:12:11.      glog_functions.cpp:113] The thread is still alive, count: 9

There are several more logging features and command line parameters that can be found in the documentation

Custom sinks

Sometimes, the logs should be written to destination other than a file or console.

This destination is called sink, and glog provided an interface named google::LogSink with send() method that all sinks must implement. Also, if one needs to implement a custom sink, there is a possibility to implement it.

Note: the LogSink instance cannot be destroyed until the referencing pointer is unregistered

Here is an example of logging to a custom sink

struct CustomLogSink : google::LogSink 
{  
    void send(google::LogSeverity severity, const char* filename,
        const char* base_filename, int line,
        const google::LogMessageTime& timestamp, const char* message,
        std::size_t message_len) override 
    {
        std::cout << google::GetLogSeverityName(severity) << ' ' << base_filename
            << ':' << line << ' ';
        std::copy_n(message, message_len, std::ostreambuf_iterator<char>{ std::cout });
        std::cout << '\n';
    }
};
void custom_sink_logging()
{
    CustomLogSink sink;
    google::AddLogSink(&sink);

    LOG(INFO) << "logging to CustomLogSink";

    google::RemoveLogSink(&sink);  

    LOG_TO_SINK(&sink, INFO) << "direct logging";  
    LOG_TO_SINK_BUT_NOT_TO_LOGFILE(&sink, INFO) << "direct logging but not to file";
}

There are several lines that worth to address. First of all - the CustomLogSink class that implements the send() method of LogSink. As can be seen, the output is redirected to stdout, just for the example. Any other output method can be applied here.

In the logging function, our new sink is registered, allowing the use of regular logging macros. However, even after deregistering, the sink can still be used with the LOG_TO_SINK and LOG_TO_SINK_BUT_NOT_TO_LOGFILE macros. It's important to note that file logging is enabled by default in glog, but can be turned off using flags.

The output:

INFO glog_functions.cpp:125 logging to CustomLogSink
INFO glog_functions.cpp:129 direct logging
INFO glog_functions.cpp:130 direct logging but not to file

Miscelanneous

There are several more features in glog that can be introduced, such as a log cleaner that runs at set intervals, custom failure handlers (supported only on x86, x86_64, and PowerPC, using libunwind/dbghelp libraries), and stripping log messages.

Summary

Pros

  • Easy to install and use

  • Several nice features such as conditional logging and formatting

Cons

  1. C-API - we are almost in 2025, why use macros?

  2. The formatting is not really nice - why use setw and stuff, when just a format string can be passed?

  3. No custom types logging

  4. No predefined custom sinks - the user has to implement by himself

In my opinion, when compared to spdlog - spdlog wins unequivocally.

In the next chapter I will cover the Easylogging++ library

Stay tuned

Image by Canva

Part 1 - spdlog

0
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