1112 lines
41 KiB
C++
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
|