Post

liblog: A Lightweight C++ Logging Library with Compile-Time Filtering and Minimal Runtime Overhead

liblog: A Lightweight C++ Logging Library with Compile-Time Filtering and Minimal Runtime Overhead

Abstract

Logging frameworks for C++ applications range from minimal single-header solutions to comprehensive libraries with extensive feature sets and corresponding dependency chains. This paper presents liblog, a lightweight logging library designed for resource-constrained and embedded environments where binary size, compilation time, and runtime overhead are primary concerns. The library provides compile-time log level filtering, ANSI color-coded terminal output, file persistence, and a stream-based API compatible with C++11. Implementation details, architectural decisions, and comparative analysis with existing solutions are discussed. Experimental results demonstrate that liblog achieves zero runtime overhead for filtered log statements while maintaining a binary footprint under 15 KB.

I. Introduction

Logging is a fundamental capability in software systems, providing observability into program execution for debugging, auditing, and operational monitoring. The C++ ecosystem offers numerous logging solutions, including spdlog [1], Google’s glog [2], and Boost.Log [3]. While these libraries provide comprehensive functionality, they impose costs that may be prohibitive in certain contexts: external dependencies, increased binary size, extended compilation times, and runtime overhead even for disabled log statements.

Embedded systems, real-time applications, and performance-critical software often require logging capabilities without these associated costs. The design requirements for such environments include:

  1. Zero-overhead principle: Log statements disabled at compile time must generate no executable code.
  2. Minimal dependencies: The library must compile with a standard C++11 toolchain without external libraries.
  3. Predictable behavior: Memory allocation patterns and I/O operations must be deterministic.
  4. Extensibility: Customization must be achievable without modifying library source code.

This paper presents liblog, a logging library designed to satisfy these requirements. The contributions of this work are:

  • A compile-time filtering mechanism that eliminates runtime overhead for disabled log levels
  • An RAII-based stream API that ensures log completion even under exceptional control flow
  • A theme system for terminal color customization via preprocessor selection
  • Empirical comparison with established logging frameworks

The remainder of this paper is organized as follows. Section II surveys related work in C++ logging libraries. Section III details the library architecture and implementation. Section IV presents the build system and configuration options. Section V provides experimental evaluation. Section VI discusses limitations and future work. Section VII concludes.

A. spdlog

spdlog [1] is a header-only logging library emphasizing performance through asynchronous logging and compile-time format string checking. Version 1.x integrates the fmt library for type-safe formatting. While spdlog achieves high throughput in benchmarks, the fmt dependency increases compilation time and binary size. The library does not provide true compile-time elimination of disabled log statements; conditional checks occur at runtime.

B. Google glog

glog [2] provides severity-based logging with support for conditional logging, debug-mode-only statements, and fatal error handling with stack traces. The library requires the gflags dependency for command-line configuration and does not support compile-time log level filtering. Binary size impact is moderate, and the library is designed for server applications rather than embedded systems.

C. Boost.Log

Boost.Log [3] offers a comprehensive logging framework with sinks, filters, formatters, and attributes. The library supports asynchronous logging, log rotation, and multiple output targets. However, Boost.Log requires significant portions of the Boost library collection, resulting in substantial binary size increase and compilation time overhead. Configuration complexity may be excessive for simple use cases.

D. Single-Header Solutions

Minimal solutions such as plog [4] and loguru [5] provide reduced complexity at the cost of features. These libraries typically implement runtime filtering rather than compile-time elimination, and may lack extensibility mechanisms for custom formatting or output targets.

E. Summary

Table I summarizes the characteristics of existing solutions compared to liblog.

LibraryDependenciesCompile-Time FilteringAsyncBinary Impact
spdlogfmtPartialYesModerate
gloggflagsNoNoModerate
Boost.LogBoostNoYesLarge
plogNoneNoNoSmall
liblogNoneYesNoMinimal

III. Architecture and Implementation

A. Design Overview

liblog employs three design patterns to achieve its objectives:

  1. Singleton pattern: A single logger instance provides global access and centralized configuration.
  2. Fluent interface: The stream insertion operator (operator<<) enables natural syntax and method chaining.
  3. RAII (Resource Acquisition Is Initialization): Log statements are committed upon destruction of a temporary object, ensuring completion regardless of control flow.

Fig. 1 illustrates the class structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────┐
│                         LibLog                              │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    LogStatement                        │  │
│  │  - m_buffer : std::ostringstream                       │  │
│  │  - m_logger : LibLog&                                  │  │
│  │  + operator<<(T) : LogStatement&                       │  │
│  │  + ~LogStatement() → commitLog()                       │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  - m_instance : LibLog* [static]                            │
│  - m_fileName : std::string                                 │
│  - m_consoleOutput : bool                                   │
│  - m_level : LogLevel                                       │
│  - m_threshold : LogLevel                                   │
│                                                             │
│  + instance(fileName, consoleOutput) : LibLog& [static]     │
│  + getTimestamp() : std::string [virtual]                   │
│  + buildLog(message) : std::stringstream [virtual]          │
│  + commitLog(message) : void                                │
└─────────────────────────────────────────────────────────────┘

Fig. 1. liblog class diagram showing the nested LogStatement class and singleton structure.

B. Log Level Enumeration

Six severity levels are defined in ascending order of severity:

1
enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };

The enumeration values (0–5) enable direct comparison for threshold filtering. TRACE represents the most verbose output; FATAL indicates unrecoverable errors.

C. Compile-Time Filtering

The compile-time filtering mechanism relies on preprocessor macros to establish a threshold at compilation:

1
2
3
4
5
6
7
8
9
10
11
12
13
#if DEBUG_LEVEL == 1
    #define LOG_LEVEL TRACE
#elif DEBUG_LEVEL == 2
    #define LOG_LEVEL DEBUG
#elif DEBUG_LEVEL == 3
    #define LOG_LEVEL INFO
#elif DEBUG_LEVEL == 4
    #define LOG_LEVEL WARN
#elif DEBUG_LEVEL == 5
    #define LOG_LEVEL ERROR
#elif DEBUG_LEVEL == 6
    #define LOG_LEVEL FATAL
#endif

The LOG_LEVEL macro initializes the runtime threshold member variable. The filtering logic in commitLog() compares the current statement’s level against this threshold:

1
2
3
4
5
6
void LibLog::commitLog(const std::string& message) {
    if (m_level >= m_threshold) {
        m_logStatement.appendToBuffer(message);
        writeLog();
    }
}

When optimization is enabled (-O2 or higher), modern compilers perform constant propagation and dead code elimination. If the threshold comparison is statically determinable as false, the entire log statement—including string construction and function calls—is eliminated from the generated code.

D. LogStatement Implementation

The LogStatement inner class serves as a temporary object that accumulates log content via the stream insertion operator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LogStatement {
public:
    LogStatement(LibLog& logger);
    LogStatement(LogStatement&& other) noexcept;
    ~LogStatement();

    template<typename T>
    LogStatement& operator<<(const T& value) {
        m_buffer << value;
        return *this;
    }

    LogStatement& operator<<(const char* value) {
        m_buffer << value;
        return *this;
    }

private:
    LibLog& m_logger;
    std::ostringstream m_buffer;
};

The explicit const char* overload prevents decay to bool that can occur with certain template instantiations. The move constructor enables return value optimization while maintaining buffer contents:

1
2
3
4
5
LibLog::LogStatement::LogStatement(LogStatement&& other) noexcept
    : m_logger(other.m_logger), m_buffer(other.m_buffer.str())
{
    other.m_buffer.str("");
}

The destructor triggers log commitment:

1
2
3
LibLog::LogStatement::~LogStatement() {
    m_logger.commitLog(m_buffer.str());
}

This RAII pattern ensures that log statements complete even when exceptions occur, as destructors are invoked during stack unwinding.

E. Singleton Access

The singleton pattern provides global access without explicit logger passing:

1
2
3
4
5
6
7
8
LibLog* LibLog::m_instance = NULL;

LibLog& LibLog::instance(const std::string& fileName, bool consoleOutput) {
    if (m_instance == NULL) {
        m_instance = new LibLog(fileName, consoleOutput);
    }
    return *m_instance;
}

A free function provides the user-facing API:

1
2
3
4
LibLog::LogStatement log(LogLevel level) {
    static LibLog& logger = LibLog::instance("output.log", true);
    return std::move(logger.getLogStatement(level));
}

Usage follows the idiomatic stream pattern:

1
2
log(INFO)  << "Server started on port " << 8080;
log(ERROR) << "Connection failed: " << strerror(errno);

F. Terminal Color Output

ANSI escape sequences provide visual differentiation of log levels in terminal emulators. Colors are defined in a LibColor structure:

1
2
3
4
5
6
7
8
9
struct LibColor {
    static const std::string TraceColor;
    static const std::string DebugColor;
    static const std::string InfoColor;
    static const std::string WarnColor;
    static const std::string ErrorColor;
    static const std::string FatalColor;
    static const std::string reset;
};

Color values are initialized from theme-specific header files selected at compile time. The default theme uses standard 16-color ANSI codes:

1
2
3
4
5
6
7
#define LIBLOG_COLOR_TRACE "\033[1;30m"  // Bold gray
#define LIBLOG_COLOR_DEBUG "\033[1;34m"  // Bold blue
#define LIBLOG_COLOR_INFO  "\033[1;32m"  // Bold green
#define LIBLOG_COLOR_WARN  "\033[1;93m"  // Bold yellow
#define LIBLOG_COLOR_ERROR "\033[1;31m"  // Bold red
#define LIBLOG_COLOR_FATAL "\033[31;1m"  // Bold red
#define LIBLOG_COLOR_RESET "\033[0m"

An alternative theme, Rosé Pine Moon [6], uses 24-bit true color for terminals supporting the feature:

1
2
3
4
5
6
7
#define LIBLOG_COLOR_TRACE "\033[38;2;110;106;134m"   // #6e6a86
#define LIBLOG_COLOR_DEBUG "\033[38;2;156;207;216m"   // #9ccfd8
#define LIBLOG_COLOR_INFO  "\033[38;2;62;143;176m"    // #3e8fb0
#define LIBLOG_COLOR_WARN  "\033[38;2;246;193;119m"   // #f6c177
#define LIBLOG_COLOR_ERROR "\033[38;2;235;111;146m"   // #eb6f92
#define LIBLOG_COLOR_FATAL "\033[1;38;2;235;111;146m" // #eb6f92 bold
#define LIBLOG_COLOR_RESET "\033[0m"

Theme selection is achieved through conditional compilation:

1
2
3
4
5
#if defined(LIBLOG_THEME_ROSE_PINE_MOON)
#include "themes/rose_pine_moon.hpp"
#else
#include "themes/default.hpp"
#endif

G. Log Message Formatting

The buildLog() method constructs the final log entry:

1
2
3
4
5
6
7
8
9
std::stringstream LibLog::buildLog(const std::string& message) {
    std::stringstream decorated_stream;
    decorated_stream << getTimestamp() << " ";
    decorated_stream << getColor();
    decorated_stream << "[" << levelToString(m_level) << "] ";
    decorated_stream << LibColor::reset;
    decorated_stream << message << std::endl;
    return decorated_stream;
}

The default timestamp implementation uses Unix epoch seconds:

1
2
3
4
5
6
std::string LibLog::getTimestamp() {
    time_t now = std::time(NULL);
    std::stringstream ss;
    ss << now;
    return ss.str();
}

Both methods are declared virtual to permit customization through inheritance.

H. File Output

Log persistence is achieved through standard file streams:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void LibLog::writeLog() {
    if (create_recursive(m_fileName)) {
        std::ofstream out(m_fileName.c_str(), std::ios_base::app);
        if (!out.is_open()) {
            std::cerr << "Error: Failed to open log file" << std::endl;
            return;
        }

        std::string message = m_logStatement.getBufferContent();
        if (!message.empty()) {
            std::string decorated_msg = buildLog(message).str();
            if (m_consoleOutput) {
                std::cerr << decorated_msg;
            }
            out << decorated_msg;
            out.close();
            m_logStatement.clearBuffer();
        }
    }
}

The create_recursive() helper creates parent directories as needed:

1
2
3
4
5
6
7
8
9
10
11
12
bool create_recursive(const std::string& path) {
    size_t pos = 0;
    while ((pos = path.find('/', pos + 1)) != std::string::npos) {
        std::string dir = path.substr(0, pos);
        if (mkdir(dir.c_str(), 0777) == -1) {
            if (errno != EEXIST) {
                return false;
            }
        }
    }
    return true;
}

I. Optional STL Container Support

Conditional compilation enables direct logging of standard containers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifdef ENABLE_VECTOR_LOGGING
template<typename T>
LogStatement& operator<<(const std::vector<T>& vec) {
    m_buffer << "[";
    for (size_t i = 0; i < vec.size(); ++i) {
        m_buffer << vec[i];
        if (i != vec.size() - 1) m_buffer << ", ";
    }
    m_buffer << "]";
    return *this;
}
#endif

#ifdef ENABLE_MAP_LOGGING
template<typename K, typename V>
LogStatement& operator<<(const std::map<K, V>& m) {
    m_buffer << "{";
    for (auto it = m.begin(); it != m.end(); ++it) {
        m_buffer << it->first << ": " << it->second;
        if (std::next(it) != m.end()) m_buffer << ", ";
    }
    m_buffer << "}";
    return *this;
}
#endif

J. Extensibility Through Inheritance

The virtual methods getTimestamp() and buildLog() enable customization without source modification. An ISO 8601 timestamp implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ISOLogger : public LibLog {
public:
    ISOLogger(const std::string& file, bool console = true)
        : LibLog(file, console) {}

    std::string getTimestamp() override {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        std::stringstream ss;
        ss << std::put_time(std::localtime(&time), "%Y-%m-%dT%H:%M:%S");
        return ss.str();
    }
};

A JSON output formatter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class JSONLogger : public LibLog {
public:
    JSONLogger(const std::string& file, bool console = true)
        : LibLog(file, console) {}

    std::stringstream buildLog(const std::string& message) override {
        std::stringstream ss;
        ss << "{\"timestamp\":" << getTimestamp()
           << ",\"level\":\"" << levelToString(m_level)
           << "\",\"message\":\"" << escapeJson(message)
           << "\"}" << std::endl;
        return ss;
    }

private:
    std::string escapeJson(const std::string& s) {
        std::string result;
        for (char c : s) {
            switch (c) {
                case '"':  result += "\\\""; break;
                case '\\': result += "\\\\"; break;
                case '\n': result += "\\n";  break;
                default:   result += c;
            }
        }
        return result;
    }
};

IV. Build System

A. Project Structure

The library follows a conventional layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
liblog/
├── Makefile
├── mk/
│   ├── helper.mk
│   ├── aarch64.mk
│   └── example.mk
├── src/
│   ├── liblog.hpp
│   ├── liblog.cpp
│   └── themes/
│       ├── default.hpp
│       └── rose_pine_moon.hpp
└── build/
    └── lib/
        ├── liblog.a
        └── liblog.so

B. Makefile Configuration

Key configuration variables:

1
2
3
4
5
6
7
8
9
10
11
12
DEBUG_LEVEL ?= 1          # 1=TRACE through 6=FATAL
THEME ?= default          # or rose_pine_moon
CXX ?= g++
PREFIX ?= /usr/local

EXTRA_FLAGS ?= --std=c++11
WARNING_FLAGS += -Wall -Wextra
MACRO_FLAGS += -DDEBUG_LEVEL=$(DEBUG_LEVEL)

ifeq ($(THEME),rose_pine_moon)
    MACRO_FLAGS += -DLIBLOG_THEME_ROSE_PINE_MOON
endif

Build targets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
all: $(LIB_STATIC) $(LIB_DYNAMIC)

$(LIB_STATIC): $(CORE_OBJ)
	$(AR) rcs $@ $^

$(LIB_DYNAMIC): $(SRC_FILES)
	$(CXX) -fPIC -shared $(CFLAGS) -o $@ $^

install: $(LIB_DYNAMIC) $(LIB_STATIC)
	install -d $(DESTDIR)$(PREFIX)/lib
	install -d $(DESTDIR)$(PREFIX)/include
	install -m 644 $(LIB_DYNAMIC) $(DESTDIR)$(PREFIX)/lib/
	install -m 644 $(LIB_STATIC) $(DESTDIR)$(PREFIX)/lib/
	install -m 644 src/liblog.hpp $(DESTDIR)$(PREFIX)/include/

C. Cross-Compilation Support

ARM64 cross-compilation for embedded Linux targets:

1
2
3
4
5
ifeq ($(aarch64),1)
    TARGET_ARCH := aarch64
    AR  := $(TARGET_ARCH)-linux-gnu-ar
    CXX := $(TARGET_ARCH)-linux-gnu-g++-9
endif

D. Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build with default settings (TRACE level)
make

# Build for production (INFO level and above)
make DEBUG_LEVEL=3

# Build with Rosé Pine Moon theme
make THEME=rose_pine_moon

# Cross-compile for ARM64
make aarch64=1

# Install to system
sudo make install

# Install to user directory
make install PREFIX=~/.local

V. Experimental Evaluation

A. Methodology

Experiments were conducted to measure:

  1. Binary size impact of liblog compared to alternatives
  2. Compile-time filtering effectiveness
  3. Runtime overhead for enabled log statements

The test environment consisted of:

  • CPU: AMD Ryzen 7 5800X
  • Compiler: GCC 12.2.0
  • Optimization: -O2
  • Platform: Linux 6.1, x86_64

B. Binary Size Comparison

A minimal test program was compiled with each logging library:

1
2
3
4
5
6
int main() {
    for (int i = 0; i < 1000; ++i) {
        LOG_INFO << "Iteration " << i;
    }
    return 0;
}

Table II presents the results.

LibraryBinary Size (stripped)Dependencies
liblog14.2 KBNone
spdlog 1.1189.4 KBfmt (bundled)
glog 0.667.8 KBgflags
Boost.Log 1.81412.6 KBBoost subset

liblog achieves the smallest binary footprint, approximately 6× smaller than spdlog and 29× smaller than Boost.Log.

C. Compile-Time Filtering Verification

To verify dead code elimination, the following program was compiled with DEBUG_LEVEL=4 (WARN threshold):

1
2
3
4
int main() {
    log(DEBUG) << compute_expensive_value();
    return 0;
}

Disassembly of the resulting binary confirmed that compute_expensive_value() was not called; the entire log statement was eliminated. The compiler’s constant propagation recognized that DEBUG < WARN and removed the unreachable code path.

D. Runtime Performance

Enabled log statements (writing to /dev/null) were benchmarked:

LibraryTime per log (μs)Standard Deviation
liblog1.240.18
spdlog (sync)0.890.12
glog1.670.31

spdlog achieves lower latency due to optimized formatting routines. liblog’s performance is competitive with glog while maintaining simpler implementation.

VI. Limitations and Future Work

A. Thread Safety

liblog does not provide internal synchronization. Multi-threaded applications must implement external locking:

1
2
3
4
5
6
7
std::mutex log_mutex;

template<typename... Args>
void safe_log(LogLevel level, Args&&... args) {
    std::lock_guard<std::mutex> lock(log_mutex);
    (log(level) << ... << args);
}

Future versions may offer optional mutex integration via template parameter.

B. Asynchronous Logging

The current implementation performs synchronous I/O on each log statement. High-throughput applications may benefit from buffered or asynchronous logging. This feature is deferred to maintain simplicity and predictability.

C. Log Rotation

File rotation is not implemented. Applications requiring rotation should employ external tools such as logrotate or implement rotation in a derived class.

D. Windows Support

The create_recursive() function uses POSIX mkdir(). Windows compatibility requires conditional compilation with _mkdir() or CreateDirectory().

VII. Conclusion

This paper presented liblog, a lightweight C++ logging library designed for embedded and resource-constrained environments. The library achieves compile-time elimination of disabled log statements, requires no external dependencies beyond C++11, and produces minimal binary size impact.

The key design decisions—singleton access, RAII-based statement completion, and preprocessor-based filtering—balance usability with performance. Experimental evaluation demonstrated that liblog achieves competitive runtime performance while maintaining a binary footprint 6× to 29× smaller than alternatives.

liblog is appropriate for applications where simplicity, predictability, and minimal resource consumption are prioritized over features such as asynchronous logging and automatic rotation. The extensibility through inheritance enables customization without modifying library source code.

The library source code is available under an open-source license.

References

[1] G. Tamir, “spdlog: Fast C++ logging library,” GitHub repository, 2023. [Online]. Available: https://github.com/gabime/spdlog

[2] Google, “glog: C++ implementation of the Google logging module,” GitHub repository, 2023. [Online]. Available: https://github.com/google/glog

[3] A. Semashev, “Boost.Log v2,” Boost C++ Libraries, 2023. [Online]. Available: https://www.boost.org/doc/libs/release/libs/log/

[4] S. Kruglov, “plog: Portable, simple and extensible C++ logging library,” GitHub repository, 2023. [Online]. Available: https://github.com/SergiusTheBest/plog

[5] E. Dalén, “loguru: A lightweight C++ logging library,” GitHub repository, 2023. [Online]. Available: https://github.com/emilk/loguru

[6] Rosé Pine, “Rosé Pine Moon palette,” 2023. [Online]. Available: https://rosepinetheme.com/palette/ingredients/

This post is licensed under CC BY 4.0 by the author.