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)