FetchContent Setup
spdlog is the most popular C++ logging library — fast, header-only capable, and built on fmt. The recommended CMake integration uses FetchContent:
# CMakeLists.txt — Basic spdlog integration
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1
)
FetchContent_MakeAvailable(spdlog)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE spdlog::spdlog)
// src/main.cpp — spdlog basic usage
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
int main() {
// Default logger (stdout, colored)
spdlog::info("Welcome to spdlog!");
spdlog::warn("Something might be wrong");
spdlog::error("An error occurred: {}", "disk full");
// Formatted output using fmt syntax
spdlog::info("Processed {} records in {:.2f}s", 1000, 3.14);
// Create named logger
auto console = spdlog::stdout_color_mt("mylogger");
console->info("Named logger message");
console->set_level(spdlog::level::debug);
console->debug("Debug message visible now");
return 0;
}
spdlog::spdlog target for compiled mode and spdlog::spdlog_header_only for header-only mode. The compiled target is preferred for projects with multiple translation units — it avoids redundant template instantiation across files.
External vs Bundled fmt
spdlog bundles its own copy of fmt. When your project also uses fmt directly, you must share a single fmt instance to avoid ODR violations:
# CMakeLists.txt — Shared fmt between spdlog and your app
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
# 1. Fetch fmt first
FetchContent_Declare(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 11.0.2
)
FetchContent_MakeAvailable(fmt)
# 2. Configure spdlog to use external fmt
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "" FORCE)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1
)
FetchContent_MakeAvailable(spdlog)
# 3. Both libraries share the same fmt
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE
spdlog::spdlog
fmt::fmt
)
SPDLOG_FMT_EXTERNAL but forget to make fmt available before spdlog's FetchContent_MakeAvailable, the build will fail with "fmt::fmt target not found." Always declare and populate fmt before spdlog.
Async vs Sync Logging
spdlog supports asynchronous logging via a thread pool and queue. Async mode prevents logging from blocking your application's hot path:
# Enable async support (compiled mode required)
# No extra CMake config needed — spdlog includes async by default
target_link_libraries(myapp PRIVATE spdlog::spdlog)
// src/async_logging.cpp — Async logger setup
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
void setupAsyncLogging() {
// Initialize thread pool: queue size 8192, 1 worker thread
spdlog::init_thread_pool(8192, 1);
// Create async logger with rotating file sink
auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", // filename
1024 * 1024 * 10, // max size: 10 MB
3 // keep 3 rotated files
);
auto async_logger = std::make_shared<spdlog::async_logger>(
"async_logger",
rotating_sink,
spdlog::thread_pool(),
spdlog::async_overflow_policy::block // block if queue full
);
spdlog::register_logger(async_logger);
spdlog::set_default_logger(async_logger);
spdlog::info("Async logging initialized");
}
int main() {
setupAsyncLogging();
for (int i = 0; i < 100000; ++i) {
spdlog::info("Message {}", i); // Non-blocking
}
// Flush before exit
spdlog::shutdown();
return 0;
}
Log Level Configuration
CMake can configure compile-time and runtime log levels. Compile-time filtering eliminates logging calls entirely in release builds:
# CMakeLists.txt — Log level configuration
option(LOG_LEVEL "Minimum log level" "info")
# Compile-time level filtering (removes calls below threshold)
if(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_definitions(myapp PRIVATE
SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_WARN
)
elseif(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(myapp PRIVATE
SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE
)
else()
target_compile_definitions(myapp PRIVATE
SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO
)
endif()
// src/main.cpp — Using active level macros
#include <spdlog/spdlog.h>
int main() {
spdlog::set_level(spdlog::level::trace); // Runtime level
// These macros respect SPDLOG_ACTIVE_LEVEL at compile time
SPDLOG_TRACE("Trace message — compiled out in Release");
SPDLOG_DEBUG("Debug message — compiled out in Release");
SPDLOG_INFO("Info message");
SPDLOG_WARN("Warning message");
SPDLOG_ERROR("Error message");
SPDLOG_CRITICAL("Critical message");
return 0;
}
Custom Sinks
spdlog's sink architecture lets you direct log output to any destination. Multi-sink loggers write to multiple outputs simultaneously:
// src/custom_sink.cpp — Multi-sink logger
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/daily_file_sink.h>
#include <spdlog/sinks/syslog_sink.h>
#include <vector>
#include <memory>
std::shared_ptr<spdlog::logger> createMultiSinkLogger() {
std::vector<spdlog::sink_ptr> sinks;
// Console sink — colored, info level
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::info);
console_sink->set_pattern("[%H:%M:%S] [%^%l%$] %v");
sinks.push_back(console_sink);
// File sink — daily rotation, debug level
auto file_sink = std::make_shared<spdlog::sinks::daily_file_sink_mt>(
"logs/app.log", 0, 0 // rotate at midnight
);
file_sink->set_level(spdlog::level::debug);
file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%s:%#] %v");
sinks.push_back(file_sink);
// Create multi-sink logger
auto logger = std::make_shared<spdlog::logger>("multi", sinks.begin(), sinks.end());
logger->set_level(spdlog::level::debug);
return logger;
}
int main() {
auto logger = createMultiSinkLogger();
spdlog::set_default_logger(logger);
SPDLOG_DEBUG("This goes to file only");
SPDLOG_INFO("This goes to both console and file");
SPDLOG_ERROR("This goes to both console and file");
return 0;
}
spdlog with Header-Only Mode
For small projects or when avoiding link dependencies, spdlog can operate in pure header-only mode:
# CMakeLists.txt — Header-only spdlog
cmake_minimum_required(VERSION 3.20)
project(SmallApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1
)
FetchContent_MakeAvailable(spdlog)
add_executable(smallapp src/main.cpp)
# Use header-only target — no compiled library
target_link_libraries(smallapp PRIVATE spdlog::spdlog_header_only)
SPDLOG_HEADER_ONLY automatically. It increases compile time for each translation unit that includes spdlog headers, but eliminates the library build step. Use compiled mode (spdlog::spdlog) for projects with more than 3-4 source files using logging.
Per-Target Log Configuration
Libraries within a larger project may need different log levels or compile-time filtering than the main application:
# CMakeLists.txt — Per-target logging configuration
cmake_minimum_required(VERSION 3.20)
project(MultiLib LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.14.1
)
FetchContent_MakeAvailable(spdlog)
# Core library — minimal logging in release
add_library(core_lib src/core.cpp)
target_link_libraries(core_lib PUBLIC spdlog::spdlog)
target_compile_definitions(core_lib PRIVATE
$<$<CONFIG:Release>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_ERROR>
$<$<CONFIG:Debug>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE>
)
# Network library — always log warnings
add_library(net_lib src/network.cpp)
target_link_libraries(net_lib PUBLIC spdlog::spdlog)
target_compile_definitions(net_lib PRIVATE
SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_WARN
)
# Application — full debug in debug, info in release
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core_lib net_lib)
target_compile_definitions(app PRIVATE
$<$<CONFIG:Release>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO>
$<$<CONFIG:Debug>:SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG>
)
Install Considerations
When packaging a library that uses spdlog, decide whether spdlog is a public or private dependency:
# CMakeLists.txt — Library with spdlog as private dependency
add_library(mylib src/mylib.cpp)
# PRIVATE — spdlog headers don't leak into consumer's include path
target_link_libraries(mylib PRIVATE spdlog::spdlog)
# Install rules — don't export spdlog requirement
install(TARGETS mylib
EXPORT mylibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include
)
# Config file for find_package(mylib)
install(EXPORT mylibTargets
FILE mylibTargets.cmake
NAMESPACE mylib::
DESTINATION lib/cmake/mylib
)
# If spdlog is PUBLIC, consumers need to find it too:
# include(CMakeFindDependencyMacro)
# find_dependency(spdlog 1.14) # In mylibConfig.cmake
<spdlog/spdlog.h>, spdlog must be a PUBLIC dependency — and your install config must call find_dependency(spdlog). Otherwise consumers get "file not found" errors. Keep spdlog PRIVATE by wrapping it behind your own logging interface.