#ifndef SPFLOGCONFIG_H #define SPFLOGCONFIG_H #include #include #include #include #include #include #include #include #include #include #include #include "spdlogconfig/SPDLOGConfigError.h" #if __has_include() #include "spdlog/sinks/kafka_sink.h" #endif #if __has_include() #include "spdlog/sinks/mongo_sink.h" #endif #ifndef _WIN32 #include "spdlog/sinks/syslog_sink.h" #include #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 #include #include #include #include #include #include #include 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, rfl::Maximum<59>>; using vHour = rfl::Validator, rfl::Maximum<23>>; using vThreadCount = rfl::Validator, rfl::Maximum<100>>; using vThreadQueue = rfl::Validator, rfl::Maximum<409600>>; using vMaxSize = rfl::Validator< std::string, rfl::PatternValidator>; using vRotationTime = rfl::Validator< std::string, rfl::PatternValidator>; // --------------------------------------------------------------------------- // 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> m_file_name; rfl::Rename<"base_filename", std::optional> m_base_file_name; rfl::Rename<"level", std::optional> m_level; rfl::Rename<"truncate", std::optional> m_truncate = defaults::TRUNCATE; rfl::Rename<"max_size", std::optional> m_max_size = defaults::MAX_SIZE; rfl::Rename<"max_files", std::optional> m_max_files = defaults::MAX_FILES; rfl::Rename<"rotation_time", std::optional>> m_rotation_time; rfl::Rename<"create_parent_dir", std::optional> m_create_parent_dir = defaults::CREATE_PARENT_DIR; #ifndef _WIN32 rfl::Rename<"ident", std::optional> m_syslog_ident = defaults::IDENT; rfl::Rename<"syslog_option", std::optional> m_syslog_option = defaults::SYSLOG_OPTION; rfl::Rename<"syslog_facility", std::optional> m_syslog_facility = LOG_USER; #endif }; struct spdlog_patterns_t { rfl::Rename<"name", std::optional> m_pattern_name; rfl::Rename<"value", std::optional> 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> m_logger_level; rfl::Rename<"name", std::string> m_logger_name; rfl::Rename<"sinks", std::vector> m_logger_sinks; rfl::Rename<"pattern", std::optional> m_logger_pattern; rfl::Rename<"type", std::optional> m_logger_type = defaults::DEFAULT_LOGGER_TYPE; rfl::Rename<"overflow_policy", std::optional> m_overflow_policy = defaults::OVERFLOW_POLICY; rfl::Rename<"thread_pool", std::optional> 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> m_sinks; rfl::Rename<"pattern", std::vector> m_patterns; rfl::Rename<"logger", std::vector> m_loggers; rfl::Rename<"thread_pools", std::vector> 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 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 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 m_initialLevels; std::unordered_map 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( 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{}; 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( sink_log_item.m_level.value().value()) : static_cast( 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()) ->set_level(effectiveLevel()); break; case stdout_sink_mt: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stderr_sink_st: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stderr_sink_mt: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stdout_color_sink_st: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stdout_color_sink_mt: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stderr_color_sink_st: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case stderr_color_sink_mt: sinkList .emplace_back( std::make_shared()) ->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( 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( 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( 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( 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()) ->set_level(effectiveLevel()); break; case null_sink_mt: sinkList .emplace_back(std::make_shared()) ->set_level(effectiveLevel()); break; #ifdef __ANDROID__ case android_sink_st: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; case android_sink_mt: sinkList .emplace_back( std::make_shared()) ->set_level(effectiveLevel()); break; #endif #ifndef _WIN32 case syslog_sink_st: sinkList .emplace_back(std::make_shared( 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( 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()) ->set_level(effectiveLevel()); break; case msvc_sink_mt: sinkList .emplace_back(std::make_shared()) ->set_level(effectiveLevel()); break; #endif default: break; } } } } // Build the logger (sync or async) std::shared_ptr logger; if (logger_item.m_logger_type.value() == names::SYNC) { logger = std::make_shared( 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 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( 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( 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( 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( 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(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( 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( 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