Compile-Time Logrus-Style Logging Interface with spdlog in C++

To achieve structured logging similar to Go's logrus using C++, we can build a wrapper around the efficient spdlog libray that constructs log messages at compile time without dynamic memory allocation.

Core Requirements

The implemantation should provide:

  • Key-value pairs kept together for clarity
  • Compile-time string construction
  • Literals-only message and key support
  • Zero runtime allocations

String Literal Concatenation

We need to concatenate string literals at compile time. A template class handles this by storing character arrays and using index sequences:

template <size_t N>
struct StringLiteral {
    char data[N];

    constexpr StringLiteral(const char (&str)[N])
        : StringLiteral(str, std::make_index_sequence<N>{}) {}

    template <size_t... Indices>
    constexpr StringLiteral(const char (&str)[N], std::index_sequence<Indices...>)
        : data{str[Indices]...} {}
};

template <size_t N1, size_t N2>
constexpr auto concat_literals(const char (&s1)[N1], const char (&s2)[N2]) {
    return StringLiteral<N1 + N2 - 1>(s1, s2);
}

Field Representation

Each key-value pair is represented as a field template:

template <size_t KeySize, typename ValueType>
struct LogField {
    StringLiteral<KeySize> key;
    ValueType value;

    constexpr LogField(const char (&k)[KeySize], ValueType&& v)
        : key(k), value(std::forward<ValueType>(v)) {}
};

Log Entry Construction

The entry class accumulates fields and generates formatted output:

template <typename... Fields>
class LogEntry {
    std::tuple<Fields...> field_data;

public:
    constexpr LogEntry(Fields&&... fields)
        : field_data(std::forward<Fields>(fields)...) {}

    template <size_t N, typename T>
    constexpr auto add_field(const char (&key)[N], T&& value) const {
        return LogEntry<Fields..., LogField<N, T>>(
            std::get<Fields>(field_data)..., 
            LogField<N, T>(key, std::forward<T>(value))
        );
    }

    template <size_t MsgLen>
    void write(spdlog::level::level_enum level, const char (&message)[MsgLen]) const {
        format_and_log(level, message, std::make_index_sequence<sizeof...(Fields)>{});
    }

private:
    template <size_t MsgLen, size_t... Indices>
    void format_and_log(spdlog::level::level_enum lvl, 
                       const char (&msg)[MsgLen], 
                       std::index_sequence<Indices...>) const {
        
        auto formatter = build_formatter(
            LogField<4, const char*>("msg", msg),
            std::get<Indices>(field_data)...
        );
        
        std::apply([&](auto&&... args) {
            spdlog::log(lvl, formatter.pattern.data(), std::forward<decltype(args)>(args)...);
        }, std::move(formatter.values));
    }
};

Formatter Generation

The formatting system builds the final pattern and argument list:

template <typename... Values>
struct MessageFormatter {
    StringLiteral<100> pattern;  // Simplified size
    std::tuple<Values...> values;
};

template <size_t KeyLen, typename T>
constexpr auto create_segment(const LogField<KeyLen, T>& field) {
    return MessageFormatter<T>{
        concat_literals(field.key.data, "='{}' "),
        std::make_tuple(field.value)
    };
}

template <typename First, typename Second>
constexpr auto combine_formatters(First&& f1, Second&& f2) {
    return MessageFormatter<decltype(f1.values), decltype(f2.values)>{
        concat_literals(f1.pattern.data, f2.pattern.data),
        std::tuple_cat(std::move(f1.values), std::move(f2.values))
    };
}

Usage Examples

With this infrastructure, usage becomes straightforward:

// Simple message
logger::info("System started");

// Single field
logger::with("address", "127.0.0.1:80").info("Connection established");

// Multiple fields
logger::with("ip", "192.168.1.1")
       .add("port", 8080)
       .info("Service listening");

// Reusable context
auto task_ctx = logger::with("task_id", 42);
task_ctx.add("status", "running").info("Task processing");
task_ctx.add("result", "success").info("Task completed");

Macro Interface

Convenience macros simplify common patterns:

#define LOG_INFO(msg, ...) \
    logger::entry(__VA_ARGS__).write(spdlog::level::info, msg)

#define KV(key, value) \
    LogField(sizeof(key), key, value)

Tags: C++ spdlog logging compile-time Templates

Posted on Mon, 01 Jun 2026 02:20:51 +0000 by realjumper