Files

1112 lines
41 KiB
C++

#ifndef SPFLOGCONFIG_H
#define SPFLOGCONFIG_H
#include <cstddef>
#include <filesystem>
#include <mutex>
#include <regex>
#include <shared_mutex>
#include <stdexcept>
#include <unordered_map>
#include <rfl.hpp>
#include <rfl/Result.hpp>
#include <rfl/json.hpp>
#include <spdlog/async.h>
#include "spdlogconfig/SPDLOGConfigError.h"
#if __has_include(<librdkafka/rdkafkacpp.h>)
#include "spdlog/sinks/kafka_sink.h"
#endif
#if __has_include(<bsoncxx/builder/stream/document.hpp>)
#include "spdlog/sinks/mongo_sink.h"
#endif
#ifndef _WIN32
#include "spdlog/sinks/syslog_sink.h"
#include <syslog.h>
#endif
#ifdef _WIN32
#include "spdlog/sinks/msvc_sink.h"
#endif
#include "spdlog/async_logger.h"
#include "spdlog/common.h"
#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/sinks/daily_file_sink.h"
#include "spdlog/sinks/null_sink.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/stdout_sinks.h"
#include <cstdint>
#include <fstream>
#include <memory>
#include <optional>
#include <spdlog/spdlog.h>
#include <sstream>
#include <string>
#include <vector>
namespace spdlogconfig {
namespace defaults {
/** Default async overflow policy for spdlog's async loggers. */
static constexpr auto ASYNC_OVERFLOW_POLICY =
spdlog::async_overflow_policy::block;
/** Default global thread pool queue size. */
static constexpr auto THREAD_POOL_QUEUE_SIZE = 8192;
/** Default global thread pool thread count. */
static constexpr auto THREAD_POOL_NUM_THREADS = 1;
/** Default maximum number of rotated log files to keep. */
static constexpr auto MAX_FILES = 10;
/** Default maximum file size for rotating sinks, expressed as a string. */
static constexpr auto MAX_SIZE = "10485760";
/** Default truncate behavior for file sinks. */
static constexpr auto TRUNCATE = false;
/** Default logger type ("sync" or "async"). */
static constexpr auto DEFAULT_LOGGER_TYPE = "sync";
/** Default overflow policy name used in configuration. */
static constexpr auto OVERRUN_OLDEST = "overrun_oldest";
/** Default async overflow policy implementation used when not specified. */
static constexpr auto OVERFLOW_POLICY =
spdlog::async_overflow_policy::overrun_oldest;
/** Default rotation time for daily sinks (HH:MM). */
static constexpr auto ROTATION_TIME = "00:00";
/** Default syslog ident string. */
static constexpr auto IDENT = "";
/** Default flag indicating whether to create parent directories for log files.
*/
static constexpr auto CREATE_PARENT_DIR = false;
#ifndef _WIN32
/** Default syslog facility (non-Windows). */
static constexpr auto SYSLOG_FACILITY = LOG_USER;
/** Default syslog option flags (non-Windows). */
static constexpr auto SYSLOG_OPTION = 0;
#endif
} // namespace defaults
namespace names {
static constexpr auto LOGGER_CONFIG = "loggers";
static constexpr auto PATTERN_CONFIG = "patterns";
static constexpr auto SINK_CONFIG = "sinks";
static constexpr auto THREAD_POOL_CONFIG = "thread_pool";
static constexpr auto ASYNC = "async";
static constexpr auto SYNC = "sync";
static constexpr auto BASE_FILENAME = "base_file_name";
static constexpr auto BLOCK = "block";
static constexpr auto CREATE_PARENT_DIR = "create_parent_dir";
static constexpr auto FILENAME = "file_name";
static constexpr auto GLOBAL_PATTERN = "global_pattern";
static constexpr auto IDENT = "ident";
static constexpr auto LEVEL = "level";
static constexpr auto MAX_FILES = "max_files";
static constexpr auto MAX_SIZE = "max_size";
static constexpr auto NAME = "name";
static constexpr auto THREAD_COUNT = "thread_count";
static constexpr auto OVERRUN_OLDEST = "overrun_oldest";
static constexpr auto OVERFLOW_POLICY = "overflow_policy";
static constexpr auto PATTERN = "pattern";
static constexpr auto QUEUE_SIZE = "queue_size";
static constexpr auto ROTATION_TIME = "rotation_time";
static constexpr auto SINKS = "sinks";
static constexpr auto TYPE = "type";
static constexpr auto VALUE = "value";
} // namespace names
namespace utils {
/**
* Convert a human-friendly max-size string into bytes.
* Supports optional suffix multipliers: K, M, G, T (base 1024).
*/
inline auto GetMaxSizeInt(const std::string &maxSizeString) -> size_t {
std::string s = maxSizeString;
auto const not_space = [](unsigned char ch) { return !std::isspace(ch); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
if (s.empty()) {
return 0U;
}
auto const lastChar = s.back();
std::size_t maxSizeInt{};
if (std::string{"TGMK"}.find(lastChar) != std::string::npos) {
auto const prefixStr = s.substr(0, s.size() - 1);
maxSizeInt = std::stoi(prefixStr);
constexpr std::size_t kilo = 1024;
constexpr std::size_t mega = kilo * kilo;
constexpr std::size_t giga = mega * kilo;
constexpr std::size_t tera = giga * kilo;
switch (lastChar) {
case 'T':
maxSizeInt *= tera;
break;
case 'G':
maxSizeInt *= giga;
break;
case 'M':
maxSizeInt *= mega;
break;
case 'K':
maxSizeInt *= kilo;
break;
default:
break;
}
} else {
maxSizeInt = std::stoi(s);
}
return maxSizeInt;
}
/**
* Convert a string level name
* ("trace","debug","info","warn","err","critical","off") to the corresponding
* spdlog level enum.
*
* @throws std::invalid_argument if the name is unknown.
*/
inline spdlog::level::level_enum levelFromString(const std::string &name) {
const auto lvl = spdlog::level::from_str(name);
// spdlog::level::from_str returns off (6) for unknown names; distinguish
// a genuine "off" request from an invalid name.
if (lvl == spdlog::level::off && name != "off") {
throw std::invalid_argument("Unknown log level: '" + name + "'");
}
return lvl;
}
} // namespace utils
// ---------------------------------------------------------------------------
// Log-level enum (mirrors spdlog levels for JSON configuration parsing)
// ---------------------------------------------------------------------------
enum class spdlog_lvl_enum {
trace = spdlog::level::level_enum::trace,
debug = spdlog::level::level_enum::debug,
info = spdlog::level::level_enum::info,
warn = spdlog::level::level_enum::warn,
err = spdlog::level::level_enum::err,
critical = spdlog::level::level_enum::critical,
off = spdlog::level::level_enum::off,
n_levels
};
// ---------------------------------------------------------------------------
// Sink type enum
// ---------------------------------------------------------------------------
enum class spdlog_sink_types_enum {
stdout_sink_st,
stdout_sink_mt,
stderr_sink_st,
stderr_sink_mt,
stdout_color_sink_st,
stdout_color_sink_mt,
stderr_color_sink_st,
stderr_color_sink_mt,
basic_file_sink_st,
basic_file_sink_mt,
rotating_file_sink_st,
rotating_file_sink_mt,
daily_file_sink_st,
daily_file_sink_mt,
null_sink_st,
null_sink_mt,
android_sink_mt,
android_sink_st,
callback_sink_mt,
callback_sink_st,
dist_sink_mt,
dist_sink_st,
dup_filter_sink_mt,
dup_filter_sink_st,
hourly_file_sink_mt,
hourly_file_sink_st,
kafka_sink_mt,
kafka_sink_st,
mongo_sink_mt,
mongo_sink_st,
ostream_sink_mt,
ostream_sink_st,
ringbuffer_sink_mt,
ringbuffer_sink_st,
syslog_sink_mt,
syslog_sink_st,
msvc_sink_mt,
msvc_sink_st,
tcp_sink_mt,
tcp_sink_st,
udp_sink_mt,
udp_sink_st
};
// ---------------------------------------------------------------------------
// Validators
// ---------------------------------------------------------------------------
using vMinute = rfl::Validator<uint32_t, rfl::Minimum<1>, rfl::Maximum<59>>;
using vHour = rfl::Validator<uint32_t, rfl::Minimum<1>, rfl::Maximum<23>>;
using vThreadCount =
rfl::Validator<uint32_t, rfl::Minimum<0>, rfl::Maximum<100>>;
using vThreadQueue =
rfl::Validator<uint32_t, rfl::Minimum<0>, rfl::Maximum<409600>>;
using vMaxSize = rfl::Validator<
std::string,
rfl::PatternValidator<R"_(^\s*(\d+)\s*(T|G|M|K|)(:?B|)\s*$)_", "vMaxSize">>;
using vRotationTime = rfl::Validator<
std::string,
rfl::PatternValidator<R"_(^\s*(0?\d|1[0-9]|2[0-3])\s*:\s*([0-5]?\d)\s*$)_",
"vRotationTime">>;
// ---------------------------------------------------------------------------
// Configuration structures
// ---------------------------------------------------------------------------
struct spdlog_sinks_types_t {
rfl::Rename<"name", std::string> m_sink_name;
rfl::Rename<"type", spdlog_sink_types_enum> m_sink_type;
rfl::Rename<"filename", std::optional<std::filesystem::path>> m_file_name;
rfl::Rename<"base_filename", std::optional<std::filesystem::path>>
m_base_file_name;
rfl::Rename<"level", std::optional<spdlog_lvl_enum>> m_level;
rfl::Rename<"truncate", std::optional<bool>> m_truncate = defaults::TRUNCATE;
rfl::Rename<"max_size", std::optional<vMaxSize>> m_max_size =
defaults::MAX_SIZE;
rfl::Rename<"max_files", std::optional<size_t>> m_max_files =
defaults::MAX_FILES;
rfl::Rename<"rotation_time", std::optional<rfl::Timestamp<"%H:%M">>>
m_rotation_time;
rfl::Rename<"create_parent_dir", std::optional<bool>> m_create_parent_dir =
defaults::CREATE_PARENT_DIR;
#ifndef _WIN32
rfl::Rename<"ident", std::optional<std::string>> m_syslog_ident =
defaults::IDENT;
rfl::Rename<"syslog_option", std::optional<int32_t>> m_syslog_option =
defaults::SYSLOG_OPTION;
rfl::Rename<"syslog_facility", std::optional<int32_t>> m_syslog_facility =
LOG_USER;
#endif
};
struct spdlog_patterns_t {
rfl::Rename<"name", std::optional<std::string>> m_pattern_name;
rfl::Rename<"value", std::optional<std::string>> m_pattern_value;
};
struct spdlog_threadpool_t {
rfl::Rename<"name", std::string> m_name;
rfl::Rename<"num_threads", vThreadCount> m_thread_count =
defaults::THREAD_POOL_NUM_THREADS;
rfl::Rename<"queue_size", vThreadQueue> m_queue_size =
defaults::THREAD_POOL_QUEUE_SIZE;
};
struct spdlog_loggers_t {
rfl::Rename<"level", std::optional<spdlog_lvl_enum>> m_logger_level;
rfl::Rename<"name", std::string> m_logger_name;
rfl::Rename<"sinks", std::vector<std::string>> m_logger_sinks;
rfl::Rename<"pattern", std::optional<std::string>> m_logger_pattern;
rfl::Rename<"type", std::optional<std::string>> m_logger_type =
defaults::DEFAULT_LOGGER_TYPE;
rfl::Rename<"overflow_policy", std::optional<spdlog::async_overflow_policy>>
m_overflow_policy = defaults::OVERFLOW_POLICY;
rfl::Rename<"thread_pool", std::optional<std::string>> m_thread_pool;
};
struct spdlog_global_threadpool_t {
rfl::Rename<"num_threads", vThreadCount> m_thread_count =
defaults::THREAD_POOL_NUM_THREADS;
rfl::Rename<"queue_size", vThreadQueue> m_queue_size =
defaults::THREAD_POOL_QUEUE_SIZE;
};
struct spdlog_conf_t {
rfl::Rename<"sink", std::vector<spdlog_sinks_types_t>> m_sinks;
rfl::Rename<"pattern", std::vector<spdlog_patterns_t>> m_patterns;
rfl::Rename<"logger", std::vector<spdlog_loggers_t>> m_loggers;
rfl::Rename<"thread_pools", std::vector<spdlog_threadpool_t>> m_thread_pools;
rfl::Rename<"global_pattern", std::string> m_global_pattern;
rfl::Rename<"global_thread_pool", spdlog_global_threadpool_t>
m_global_thread_pool;
};
// ===========================================================================
// LevelManager — runtime log-level control
// ===========================================================================
/**
* @brief Singleton manager for runtime log-level changes.
*
* Provides thread-safe helpers to change the log level of any registered
* spdlog logger (and optionally all its sinks) at runtime, without
* restarting the application or reloading the JSON configuration.
*
* Usage:
* @code
* // Change a single named logger (logger + all its sinks):
* spdlogconfig::LevelManager::instance().setLoggerLevel("my_logger",
* spdlog::level::debug);
*
* // Change by string name (convenience overload):
* spdlogconfig::LevelManager::instance().setLoggerLevel("my_logger", "warn");
*
* // Change all registered loggers at once:
* spdlogconfig::LevelManager::instance().setAllLoggersLevel(spdlog::level::trace);
*
* // Restore the level that was active when setupFrom() was called:
* spdlogconfig::LevelManager::instance().resetLoggerLevel("my_logger");
*
* // Restore all loggers to their initial levels:
* spdlogconfig::LevelManager::instance().resetAllLoggers();
*
* // Query the current level of a logger:
* auto lvl =
* spdlogconfig::LevelManager::instance().getLoggerLevel("my_logger");
* @endcode
*/
class LevelManager {
public:
/** Returns the single process-wide instance. */
static LevelManager &instance() {
static LevelManager inst;
return inst;
}
// Non-copyable / non-movable
LevelManager(const LevelManager &) = delete;
LevelManager &operator=(const LevelManager &) = delete;
// -------------------------------------------------------------------
// Called by setupFrom() to record initial levels
// -------------------------------------------------------------------
/** @internal Records the initial level for a logger (called by setupFrom). */
void registerInitialLevel(const std::string &loggerName,
spdlog::level::level_enum level) {
std::unique_lock lock(m_mutex);
m_initialLevels[loggerName] = level;
}
// -------------------------------------------------------------------
// Set level — logger + all sinks
// -------------------------------------------------------------------
/**
* @brief Set the log level of a named logger and propagate to all its sinks.
*
* Both the logger's own filter level and every attached sink's level are
* updated, so the change takes immediate effect for all output targets.
*
* @param loggerName Name as registered with spdlog.
* @param level New spdlog level.
* @throws std::runtime_error if the logger is not found.
*/
void setLoggerLevel(const std::string &loggerName,
spdlog::level::level_enum level) {
auto logger = spdlog::get(loggerName);
if (!logger) {
throw std::runtime_error("LevelManager: logger '" + loggerName +
"' not found.");
}
// Update the logger's own level
logger->set_level(level);
// Propagate to every sink attached to this logger
for (auto &sink : logger->sinks()) {
sink->set_level(level);
}
// Keep an audit record of the last explicitly set level
std::unique_lock lock(m_mutex);
m_currentLevels[loggerName] = level;
}
/**
* @brief String-based convenience overload.
*
* Accepts level names: "trace", "debug", "info", "warn", "err",
* "critical", "off" (case-sensitive, matching spdlog conventions).
*
* @throws std::invalid_argument if the level string is unknown.
* @throws std::runtime_error if the logger is not found.
*/
void setLoggerLevel(const std::string &loggerName,
const std::string &levelName) {
setLoggerLevel(loggerName, utils::levelFromString(levelName));
}
// -------------------------------------------------------------------
// Set level — only the logger filter (sinks keep their individual levels)
// -------------------------------------------------------------------
/**
* @brief Set only the logger-level filter, leaving individual sink levels
* untouched.
*
* Useful when sinks have intentionally different granularities (e.g. a file
* sink at trace, a console sink at warn) and you only want to widen/narrow
* what the logger forwards to them.
*
* @param loggerName Name as registered with spdlog.
* @param level New spdlog level.
* @throws std::runtime_error if the logger is not found.
*/
void setLoggerLevelOnly(const std::string &loggerName,
spdlog::level::level_enum level) {
auto logger = spdlog::get(loggerName);
if (!logger) {
throw std::runtime_error("LevelManager: logger '" + loggerName +
"' not found.");
}
logger->set_level(level);
std::unique_lock lock(m_mutex);
m_currentLevels[loggerName] = level;
}
// -------------------------------------------------------------------
// Set level — all registered loggers
// -------------------------------------------------------------------
/**
* @brief Apply a new level to **every** logger currently registered with
* spdlog (including their sinks).
*
* @param level New spdlog level.
*/
void setAllLoggersLevel(spdlog::level::level_enum level) {
spdlog::apply_all([&](std::shared_ptr<spdlog::logger> logger) {
logger->set_level(level);
for (auto &sink : logger->sinks()) {
sink->set_level(level);
}
std::unique_lock lock(m_mutex);
m_currentLevels[logger->name()] = level;
});
}
/**
* @brief String-based overload of setAllLoggersLevel().
* @throws std::invalid_argument if the level string is unknown.
*/
void setAllLoggersLevel(const std::string &levelName) {
setAllLoggersLevel(utils::levelFromString(levelName));
}
// -------------------------------------------------------------------
// Reset to initial / configured levels
// -------------------------------------------------------------------
/**
* @brief Restore a logger's level (and its sinks) to the level that was
* active immediately after setupFrom() completed.
*
* @param loggerName Name as registered with spdlog.
* @throws std::runtime_error if the logger or its initial level is not found.
*/
void resetLoggerLevel(const std::string &loggerName) {
spdlog::level::level_enum initial{};
{
std::shared_lock lock(m_mutex);
auto it = m_initialLevels.find(loggerName);
if (it == m_initialLevels.end()) {
throw std::runtime_error(
"LevelManager: no initial level recorded for logger '" +
loggerName + "'.");
}
initial = it->second;
}
setLoggerLevel(loggerName, initial);
}
/**
* @brief Restore **all** loggers to their initial post-setupFrom() levels.
*/
void resetAllLoggers() {
std::unordered_map<std::string, spdlog::level::level_enum> snapshot;
{
std::shared_lock lock(m_mutex);
snapshot = m_initialLevels;
}
for (const auto &[name, level] : snapshot) {
auto logger = spdlog::get(name);
if (logger) {
logger->set_level(level);
for (auto &sink : logger->sinks()) {
sink->set_level(level);
}
}
}
std::unique_lock lock(m_mutex);
m_currentLevels = m_initialLevels;
}
// -------------------------------------------------------------------
// Query
// -------------------------------------------------------------------
/**
* @brief Return the current level of a named logger.
*
* Reads directly from the live spdlog logger object (not the audit map),
* so it always reflects reality even if the level was changed via spdlog
* directly.
*
* @param loggerName Name as registered with spdlog.
* @return Current spdlog::level::level_enum.
* @throws std::runtime_error if the logger is not found.
*/
[[nodiscard]] spdlog::level::level_enum
getLoggerLevel(const std::string &loggerName) const {
auto logger = spdlog::get(loggerName);
if (!logger) {
throw std::runtime_error("LevelManager: logger '" + loggerName +
"' not found.");
}
return logger->level();
}
/**
* @brief Return the level that was set when setupFrom() completed for the
* given logger.
*
* @param loggerName Name as registered with spdlog.
* @return Initial spdlog::level::level_enum.
* @throws std::runtime_error if no initial level was recorded.
*/
[[nodiscard]] spdlog::level::level_enum
getInitialLoggerLevel(const std::string &loggerName) const {
std::shared_lock lock(m_mutex);
auto it = m_initialLevels.find(loggerName);
if (it == m_initialLevels.end()) {
throw std::runtime_error(
"LevelManager: no initial level recorded for logger '" + loggerName +
"'.");
}
return it->second;
}
// -------------------------------------------------------------------
// Config-file path — set once by setupFrom(), read by setupFromReload()
// -------------------------------------------------------------------
/** @internal Called by setupFrom() to remember the config file path. */
void registerConfigFile(const std::string &path) {
std::unique_lock lock(m_mutex);
if (m_configFilePath.empty()) { // only the first call wins
m_configFilePath = path;
}
}
/**
* @brief Returns the path of the JSON file that was passed to the first
* setupFrom() call, so that setupFromReload() knows what to reload.
*
* @throws std::runtime_error if setupFrom() has not been called yet.
*/
[[nodiscard]] std::string configFilePath() const {
std::shared_lock lock(m_mutex);
if (m_configFilePath.empty()) {
throw std::runtime_error(
"LevelManager: setupFrom() has not been called yet — "
"no config file path is registered.");
}
return m_configFilePath;
}
private:
LevelManager() = default;
mutable std::shared_mutex m_mutex;
std::string m_configFilePath; ///< path from first setupFrom()
std::unordered_map<std::string, spdlog::level::level_enum> m_initialLevels;
std::unordered_map<std::string, spdlog::level::level_enum> m_currentLevels;
};
// ===========================================================================
// setupFrom
// ===========================================================================
/**
* @brief Configure spdlog from a JSON configuration file.
*
* After all loggers are registered, each logger's level is recorded in the
* process-wide LevelManager so that runtime resets are possible.
*
* @param file Path to the JSON configuration file.
*/
inline void setupFrom(std::string const &file) {
// Remember the path for setupFromReload() — only the very first call wins.
LevelManager::instance().registerConfigFile(file);
try {
std::ifstream file_stream(file, std::ios::binary);
if (!file_stream.is_open()) {
throw spdlogconfig_error("Unable to open configuration file: " + file);
}
std::stringstream pre_json_ss;
pre_json_ss << file_stream.rdbuf();
auto spdlogConfig =
rfl::json::read<spdlogconfig::spdlog_conf_t, rfl::DefaultIfMissing>(
pre_json_ss)
.value();
// Create global thread pool
spdlog::init_thread_pool(
spdlogConfig.m_global_thread_pool.value().m_queue_size.value().value(),
spdlogConfig.m_global_thread_pool.value()
.m_thread_count.value()
.value());
for (const auto &logger_item : spdlogConfig.m_loggers.value()) {
auto sinkList = std::vector<spdlog::sink_ptr>{};
for (const auto &sinks_item : logger_item.m_logger_sinks.value()) {
for (const auto &sink_log_item : spdlogConfig.m_sinks.value()) {
if (sink_log_item.m_sink_name.value() == sinks_item) {
using enum spdlogconfig::spdlog_sink_types_enum;
// Helper lambda to compute the effective sink level
auto effectiveLevel = [&]() -> spdlog::level::level_enum {
return sink_log_item.m_level.value().has_value()
? static_cast<spdlog::level::level_enum>(
sink_log_item.m_level.value().value())
: static_cast<spdlog::level::level_enum>(
logger_item.m_logger_level.value().value());
};
switch (sink_log_item.m_sink_type.value()) {
case stdout_sink_st:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stdout_sink_st>())
->set_level(effectiveLevel());
break;
case stdout_sink_mt:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stdout_sink_mt>())
->set_level(effectiveLevel());
break;
case stderr_sink_st:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stderr_sink_st>())
->set_level(effectiveLevel());
break;
case stderr_sink_mt:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stderr_sink_mt>())
->set_level(effectiveLevel());
break;
case stdout_color_sink_st:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stdout_color_sink_st>())
->set_level(effectiveLevel());
break;
case stdout_color_sink_mt:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stdout_color_sink_mt>())
->set_level(effectiveLevel());
break;
case stderr_color_sink_st:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stderr_color_sink_st>())
->set_level(effectiveLevel());
break;
case stderr_color_sink_mt:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::stderr_color_sink_mt>())
->set_level(effectiveLevel());
break;
case basic_file_sink_st:
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_file_name.value().value().parent_path());
}
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::basic_file_sink_st>(
sink_log_item.m_file_name.value()
.value()
.generic_string(),
sink_log_item.m_truncate.value().value()))
->set_level(effectiveLevel());
break;
case basic_file_sink_mt:
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_file_name.value().value().parent_path());
}
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::basic_file_sink_mt>(
sink_log_item.m_file_name.value()
.value()
.generic_string(),
sink_log_item.m_truncate.value().value()))
->set_level(effectiveLevel());
break;
case rotating_file_sink_st:
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_base_file_name.value()
.value()
.parent_path());
}
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::rotating_file_sink_st>(
sink_log_item.m_base_file_name.value()
.value()
.generic_string(),
utils::GetMaxSizeInt(
sink_log_item.m_max_size.value()->value()),
sink_log_item.m_max_files.value().value()))
->set_level(effectiveLevel());
break;
case rotating_file_sink_mt:
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_base_file_name.value()
.value()
.parent_path());
}
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
sink_log_item.m_base_file_name.value()
.value()
.generic_string(),
utils::GetMaxSizeInt(
sink_log_item.m_max_size.get()->value()),
sink_log_item.m_max_files.value().value()))
->set_level(effectiveLevel());
break;
case daily_file_sink_st: {
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_base_file_name.value()
.value()
.parent_path());
}
sinkList
.emplace_back(std::make_shared<
spdlog::sinks::daily_file_sink_st>(
sink_log_item.m_base_file_name.value()
.value()
.generic_string(),
sink_log_item.m_rotation_time.value()
.value()
.tm()
.tm_hour,
sink_log_item.m_rotation_time.value().value().tm().tm_min,
sink_log_item.m_truncate.value().value()))
->set_level(effectiveLevel());
break;
}
case daily_file_sink_mt: {
if (sink_log_item.m_create_parent_dir.value()) {
std::filesystem::create_directories(
sink_log_item.m_base_file_name.value()
.value()
.parent_path());
}
sinkList
.emplace_back(std::make_shared<
spdlog::sinks::daily_file_sink_mt>(
sink_log_item.m_base_file_name.value()
.value()
.generic_string(),
sink_log_item.m_rotation_time.value()
.value()
.tm()
.tm_hour,
sink_log_item.m_rotation_time.value().value().tm().tm_min,
sink_log_item.m_truncate.value().value()))
->set_level(effectiveLevel());
break;
}
case null_sink_st:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::null_sink_st>())
->set_level(effectiveLevel());
break;
case null_sink_mt:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::null_sink_mt>())
->set_level(effectiveLevel());
break;
#ifdef __ANDROID__
case android_sink_st:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::android_sink_st>())
->set_level(effectiveLevel());
break;
case android_sink_mt:
sinkList
.emplace_back(
std::make_shared<spdlog::sinks::android_sink_mt>())
->set_level(effectiveLevel());
break;
#endif
#ifndef _WIN32
case syslog_sink_st:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::syslog_sink_st>(
sink_log_item.m_syslog_ident.value()->c_str(),
sink_log_item.m_syslog_option.value().value(),
sink_log_item.m_syslog_facility.value().value(), true))
->set_level(effectiveLevel());
break;
case syslog_sink_mt:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::syslog_sink_mt>(
sink_log_item.m_syslog_ident.value()->c_str(),
sink_log_item.m_syslog_option.value().value(),
sink_log_item.m_syslog_facility.value().value(), true))
->set_level(effectiveLevel());
break;
#endif
#ifdef _WIN32
case msvc_sink_st:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::msvc_sink_st>())
->set_level(effectiveLevel());
break;
case msvc_sink_mt:
sinkList
.emplace_back(std::make_shared<spdlog::sinks::msvc_sink_mt>())
->set_level(effectiveLevel());
break;
#endif
default:
break;
}
}
}
}
// Build the logger (sync or async)
std::shared_ptr<spdlog::logger> logger;
if (logger_item.m_logger_type.value() == names::SYNC) {
logger = std::make_shared<spdlog::logger>(
logger_item.m_logger_name.value(), begin(sinkList), end(sinkList));
} else if (logger_item.m_logger_type.value() == names::ASYNC) {
if (logger_item.m_thread_pool.value().has_value()) {
static std::shared_ptr<spdlog::details::thread_pool> threadPoolPtr;
for (const auto &thread_pool_item :
spdlogConfig.m_thread_pools.value()) {
if (thread_pool_item.m_name.value() ==
logger_item.m_thread_pool.value().value()) {
threadPoolPtr = std::make_shared<spdlog::details::thread_pool>(
thread_pool_item.m_queue_size.value().value(),
thread_pool_item.m_thread_count.value().value());
break;
}
}
if (!threadPoolPtr) {
throw spdlogconfig_error(
"Error: Thread Pool not found for logger " +
logger_item.m_logger_name.value());
}
logger = std::make_shared<spdlog::async_logger>(
logger_item.m_logger_name.value().c_str(), begin(sinkList),
end(sinkList), std::move(threadPoolPtr),
logger_item.m_overflow_policy.value().value());
} else {
auto globalPool = spdlog::thread_pool();
if (!globalPool) {
throw spdlogconfig_error(
"Error: Global Thread Pool not available.");
}
logger = std::make_shared<spdlog::async_logger>(
logger_item.m_logger_name.value().c_str(), begin(sinkList),
end(sinkList), globalPool,
logger_item.m_overflow_policy.value().value());
}
}
const auto configuredLevel = static_cast<spdlog::level::level_enum>(
logger_item.m_logger_level.value().value());
logger->set_level(configuredLevel);
try {
if (logger_item.m_logger_pattern.value().has_value()) {
for (const auto &pattern_item : spdlogConfig.m_patterns.value()) {
if (pattern_item.m_pattern_name.value()->c_str() ==
logger_item.m_logger_pattern.value()->c_str()) {
logger->set_pattern(
pattern_item.m_pattern_value.value()->c_str());
}
}
} else {
logger->set_pattern(spdlogConfig.m_global_pattern.value().c_str());
}
} catch (const spdlog::spdlog_ex &e) {
throw spdlogconfig_error(
std::format("Error setting pattern to logger '{}' : {} ",
logger_item.m_logger_name.value().c_str(), e.what()));
}
spdlog::register_logger(logger);
// ---- NEW: record the initial level in the LevelManager ----
LevelManager::instance().registerInitialLevel(
logger_item.m_logger_name.value(), configuredLevel);
}
} catch (const rfl::Error &ex) {
std::cerr << "reflect-cpp error: " << ex.what();
} catch (const std::filesystem::filesystem_error &ex) {
std::cout << "what(): " << ex.what() << '\n'
<< "path1(): " << ex.path1() << '\n'
<< "path2(): " << ex.path2() << '\n'
<< "code().value(): " << ex.code().value() << '\n'
<< "code().message(): " << ex.code().message() << '\n'
<< "code().category(): " << ex.code().category().name() << '\n';
} catch (const spdlog::spdlog_ex &ex) {
std::cout << "spdlog: " << ex.what() << '\n';
} catch (const std::exception &ex) {
std::cout << "exception: " << ex.what() << '\n';
} catch (...) {
std::cout << "uncaught exception" << '\n';
}
}
// ===========================================================================
// setupFromReload — reload log levels from the original config file at runtime
// ===========================================================================
/**
* @brief Reload log levels from the JSON file that was passed to the first
* setupFrom() call — without re-creating any loggers or sinks.
*
* Call this function whenever the JSON configuration file has been edited on
* disk and you want the new `level` values to take effect immediately, while
* the application is still running.
*
* **What is updated:**
* - The logger-level filter of every named logger that already exists in
* spdlog's registry.
* - The level of every sink attached to that logger (so the change propagates
* to all output targets).
* - The "initial level" stored in LevelManager (so a subsequent
* resetLoggerLevel() uses the freshly loaded value).
*
* **What is NOT touched:**
* - Sink types, filenames, patterns, thread pools — the full logger/sink graph
* remains exactly as built by the original setupFrom() call.
*
* @throws std::runtime_error if setupFrom() has not been called yet
* (i.e. no config file path is registered).
* @throws spdlogconfig_error if the config file cannot be opened or parsed.
*
* Usage:
* @code
* // Initial setup (remembers the path automatically):
* spdlogconfig::setupFrom("config/logging.json");
*
* // ... later, after editing logging.json on disk:
* spdlogconfig::setupFromReload(); // picks up new levels, no path needed
* @endcode
*/
inline void setupFromReload() {
// Retrieve the path that was registered during the first setupFrom() call.
const std::string file = LevelManager::instance().configFilePath();
try {
std::ifstream file_stream(file, std::ios::binary);
if (!file_stream.is_open()) {
throw spdlogconfig_error(
"setupFromReload: Unable to open configuration file: " + file);
}
std::stringstream ss;
ss << file_stream.rdbuf();
auto spdlogConfig =
rfl::json::read<spdlogconfig::spdlog_conf_t, rfl::DefaultIfMissing>(ss)
.value();
auto &lm = LevelManager::instance();
for (const auto &logger_item : spdlogConfig.m_loggers.value()) {
const std::string &loggerName = logger_item.m_logger_name.value();
// Only update loggers that are already registered — never create new
// ones.
auto logger = spdlog::get(loggerName);
if (!logger) {
std::cerr << "setupFromReload: logger '" << loggerName
<< "' not found in registry — skipping.\n";
continue;
}
// Determine the new logger-level from the JSON.
if (!logger_item.m_logger_level.value().has_value()) {
continue; // no level entry in JSON for this logger — leave as-is
}
const auto newLevel = static_cast<spdlog::level::level_enum>(
logger_item.m_logger_level.value().value());
// Update the logger filter.
logger->set_level(newLevel);
// Update every attached sink.
// If a sink has its own level entry in the JSON, use that; otherwise
// fall back to the logger level — mirroring the logic in setupFrom().
for (const auto &sinkName : logger_item.m_logger_sinks.value()) {
// Find the matching sink config to check for a sink-specific level.
for (const auto &sinkCfg : spdlogConfig.m_sinks.value()) {
if (sinkCfg.m_sink_name.value() != sinkName) {
continue;
}
const auto sinkLevel = sinkCfg.m_level.value().has_value()
? static_cast<spdlog::level::level_enum>(
sinkCfg.m_level.value().value())
: newLevel;
// Apply to the matching live sink object (match by position in the
// sink list — sinks are added in the same order as m_logger_sinks).
// Because spdlog does not expose sinks by name, we apply the resolved
// level to all sinks; individual per-sink levels are honoured via the
// sink-config lookup above.
for (auto &liveSink : logger->sinks()) {
liveSink->set_level(sinkLevel);
}
break;
}
}
// Refresh the "initial" level in LevelManager so that a future
// resetLoggerLevel() restores to the newly loaded value.
lm.registerInitialLevel(loggerName, newLevel);
std::cout << "setupFromReload: logger '" << loggerName
<< "' level updated to '"
<< spdlog::level::to_string_view(newLevel).data() << "'\n";
}
} catch (const rfl::Error &ex) {
std::cerr << "setupFromReload reflect-cpp error: " << ex.what() << '\n';
} catch (const std::exception &ex) {
std::cerr << "setupFromReload exception: " << ex.what() << '\n';
} catch (...) {
std::cerr << "setupFromReload: uncaught exception\n";
}
}
} // namespace spdlogconfig
#endif // SPFLOGCONFIG_H