feat(Config): Implement configuration severity policy and logging mechanism (#23284)

This commit is contained in:
Yehonal
2025-10-25 01:16:09 +02:00
committed by GitHub
parent d58046032b
commit a05cc525f0
23 changed files with 541 additions and 124 deletions

View File

@@ -21,10 +21,14 @@
#include "StringFormat.h"
#include "Tokenize.h"
#include "Util.h"
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <locale>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
namespace
{
@@ -34,13 +38,14 @@ namespace
std::unordered_map<std::string /*name*/, std::string /*value*/> _configOptions;
std::unordered_map<std::string /*name*/, std::string /*value*/> _envVarCache;
std::mutex _configLock;
ConfigPolicy _policy;
std::vector<std::string> _fatalConfigOptions =
std::unordered_set<std::string> _criticalConfigOptions =
{
{ "RealmID" },
{ "LoginDatabaseInfo" },
{ "WorldDatabaseInfo" },
{ "CharacterDatabaseInfo" },
"RealmID",
"LoginDatabaseInfo",
"WorldDatabaseInfo",
"CharacterDatabaseInfo",
};
// Check system configs like *server.conf*
@@ -62,6 +67,29 @@ namespace
return foundAppender != std::string_view::npos || foundLogger != std::string_view::npos;
}
Optional<ConfigSeverity> ParseSeverity(std::string_view value)
{
if (value.empty())
return std::nullopt;
std::string lowered(value);
std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char c) { return std::tolower(c); });
if (lowered == "skip")
return ConfigSeverity::Skip;
if (lowered == "warn" || lowered == "warning")
return ConfigSeverity::Warn;
if (lowered == "error")
return ConfigSeverity::Error;
if (lowered == "fatal" || lowered == "abort" || lowered == "panic")
return ConfigSeverity::Fatal;
return std::nullopt;
}
template<typename Format, typename... Args>
inline void PrintError(std::string_view filename, Format&& fmt, Args&& ... args)
{
@@ -77,6 +105,138 @@ namespace
}
}
template<typename Format, typename... Args>
inline void LogWithSeverity(ConfigSeverity severity, std::string_view filename, Format&& fmt, Args&&... args)
{
std::string message = Acore::StringFormat(std::forward<Format>(fmt), std::forward<Args>(args)...);
switch (severity)
{
case ConfigSeverity::Skip:
return;
case ConfigSeverity::Warn:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_WARN("server.loading", message);
return;
}
case ConfigSeverity::Error:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_ERROR("server.loading", message);
return;
}
case ConfigSeverity::Fatal:
{
if (IsAppConfig(filename))
fmt::print("{}\n", message);
LOG_FATAL("server.loading", message);
ABORT(message);
}
}
}
ConfigPolicy ApplyPolicyString(ConfigPolicy policy, std::string_view input)
{
if (input.empty())
return policy;
std::vector<std::pair<std::string, ConfigSeverity>> overrides;
Optional<ConfigSeverity> defaultOverride;
std::string tokenBuffer(input);
for (std::string_view rawToken : Acore::Tokenize(tokenBuffer, ',', false))
{
std::string token = Acore::String::Trim(std::string(rawToken), std::locale());
if (token.empty())
continue;
auto separator = token.find('=');
if (separator == std::string::npos)
continue;
std::string key = Acore::String::Trim(token.substr(0, separator), std::locale());
std::string value = Acore::String::Trim(token.substr(separator + 1), std::locale());
if (key.empty() || value.empty())
continue;
auto severity = ParseSeverity(value);
if (!severity)
continue;
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return std::tolower(c); });
if (key == "default")
{
defaultOverride = severity;
continue;
}
overrides.emplace_back(std::move(key), *severity);
}
if (defaultOverride)
{
policy.defaultSeverity = *defaultOverride;
policy.missingFileSeverity = *defaultOverride;
policy.missingOptionSeverity = *defaultOverride;
policy.criticalOptionSeverity = *defaultOverride;
policy.unknownOptionSeverity = *defaultOverride;
policy.valueErrorSeverity = *defaultOverride;
}
for (auto const& [key, severity] : overrides)
{
if (key == "missing_file" || key == "file")
policy.missingFileSeverity = severity;
else if (key == "missing_option" || key == "option")
policy.missingOptionSeverity = severity;
else if (key == "critical_option" || key == "critical")
policy.criticalOptionSeverity = severity;
else if (key == "unknown_option" || key == "unknown")
policy.unknownOptionSeverity = severity;
else if (key == "value_error" || key == "value")
policy.valueErrorSeverity = severity;
}
return policy;
}
ConfigPolicy ApplyPolicyFromArgs(ConfigPolicy policy, std::vector<std::string> const& args)
{
for (std::size_t i = 0; i < args.size(); ++i)
{
std::string const& arg = args[i];
std::string_view value;
constexpr std::string_view shortOpt = "--config-policy";
if (arg.rfind(shortOpt, 0) == 0)
{
if (arg.size() == shortOpt.size() && (i + 1) < args.size())
{
value = args[i + 1];
++i;
}
else if (arg.size() > shortOpt.size() && arg[shortOpt.size()] == '=')
{
value = std::string_view(arg).substr(shortOpt.size() + 1);
}
if (!value.empty())
policy = ApplyPolicyString(policy, value);
}
}
return policy;
}
void AddKey(std::string const& optionName, std::string const& optionKey, std::string_view fileName, bool isOptional, [[maybe_unused]] bool isReload)
{
auto const& itr = _configOptions.find(optionName);
@@ -86,7 +246,7 @@ namespace
{
if (!IsLoggingSystemOptions(optionName) && !isReload)
{
PrintError(fileName, "> Config::LoadFile: Found incorrect option '{}' in config file '{}'. Skip", optionName, fileName);
LogWithSeverity(_policy.unknownOptionSeverity, fileName, "> Config::LoadFile: Found incorrect option '{}' in config file '{}'. Skip", optionName, fileName);
#ifdef CONFIG_ABORT_INCORRECT_OPTIONS
ABORT("> Core can't start if found incorrect options");
@@ -111,13 +271,10 @@ namespace
if (in.fail())
{
if (isOptional)
{
// No display erorr if file optional
return false;
}
throw ConfigException(Acore::StringFormat("Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file));
ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity;
LogWithSeverity(severity, file, "> Config::LoadFile: Failed open {}file '{}'", isOptional ? "optional " : "", file);
// Treat SKIP as a successful no-op so the app can proceed
return severity == ConfigSeverity::Skip;
}
uint32 count = 0;
@@ -181,13 +338,10 @@ namespace
// No lines read
if (!count)
{
if (isOptional)
{
// No display erorr if file optional
return false;
}
throw ConfigException(Acore::StringFormat("Config::LoadFile: Empty file '{}'", file));
ConfigSeverity severity = isOptional ? ConfigSeverity::Skip : _policy.missingFileSeverity;
LogWithSeverity(severity, file, "> Config::LoadFile: Empty file '{}'", file);
// Treat SKIP as a successful no-op
return severity == ConfigSeverity::Skip;
}
// Add correct keys if file load without errors
@@ -382,7 +536,6 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
std::string strValue;
auto const& itr = _configOptions.find(name);
bool fatalConfig = false;
bool notFound = itr == _configOptions.end();
auto envVarName = GetEnvVarName(name);
Optional<std::string> envVar = GetEnvFromCache(name, envVarName);
@@ -401,23 +554,23 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
{
if (showLogs)
{
for (std::string s : _fatalConfigOptions)
if (s == name)
{
fatalConfig = true;
break;
}
bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end();
ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity;
if (fatalConfig)
LOG_FATAL("server.loading", "> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable\n\nYour server cannot start without this option!",
if (isCritical)
{
LogWithSeverity(severity, _filename,
"> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable\n\nYour server cannot start without this option!",
name, _filename, name, Acore::ToString(def), envVarName);
}
else
{
std::string configs = _filename;
if (!_moduleConfigFiles.empty())
configs += " or module config";
LOG_WARN("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
LogWithSeverity(severity, _filename,
"> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
name, configs, name, def, envVarName);
}
}
@@ -433,7 +586,8 @@ T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLog
{
if (showLogs)
{
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead",
LogWithSeverity(_policy.valueErrorSeverity, _filename,
"> Config: Bad value defined for name '{}', going to use '{}' instead",
name, Acore::ToString(def));
}
@@ -447,7 +601,6 @@ template<>
std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std::string const& def, bool showLogs /*= true*/) const
{
auto const& itr = _configOptions.find(name);
bool fatalConfig = false;
bool notFound = itr == _configOptions.end();
auto envVarName = GetEnvVarName(name);
Optional<std::string> envVar = GetEnvFromCache(name, envVarName);
@@ -466,23 +619,23 @@ std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std
{
if (showLogs)
{
for (std::string s : _fatalConfigOptions)
if (s == name)
{
fatalConfig = true;
break;
}
bool isCritical = _criticalConfigOptions.find(name) != _criticalConfigOptions.end();
ConfigSeverity severity = isCritical ? _policy.criticalOptionSeverity : _policy.missingOptionSeverity;
if (fatalConfig)
LOG_FATAL("server.loading", "> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.\n\nYour server cannot start without this option!",
if (isCritical)
{
LogWithSeverity(severity, _filename,
"> Config:\n\nFATAL ERROR: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.\n\nYour server cannot start without this option!",
name, _filename, name, def, envVarName);
}
else
{
std::string configs = _filename;
if (!_moduleConfigFiles.empty())
configs += " or module config";
LOG_WARN("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
LogWithSeverity(severity, _filename,
"> Config: Missing property {} in config file {}, add \"{} = {}\" to this file or define '{}' as an environment variable.",
name, configs, name, def, envVarName);
}
}
@@ -509,7 +662,8 @@ bool ConfigMgr::GetOption<bool>(std::string const& name, bool const& def, bool s
{
if (showLogs)
{
LOG_ERROR("server.loading", "> Config: Bad value defined for name '{}', going to use '{}' instead",
LogWithSeverity(_policy.valueErrorSeverity, _filename,
"> Config: Bad value defined for name '{}', going to use '{}' instead",
name, def ? "true" : "false");
}
@@ -558,17 +712,27 @@ std::string const ConfigMgr::GetConfigPath()
#endif
}
void ConfigMgr::Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList /*= {}*/)
void ConfigMgr::Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList /*= {}*/, ConfigPolicy policy /*= {}*/)
{
_filename = initFileName;
_args = std::move(args);
_policy = policy;
if (char const* env = std::getenv("AC_CONFIG_POLICY"))
_policy = ApplyPolicyString(_policy, env);
_policy = ApplyPolicyFromArgs(_policy, _args);
_additonalFiles.clear();
_moduleConfigFiles.clear();
// Add modules config if exist
if (!modulesConfigList.empty())
{
for (auto const& itr : Acore::Tokenize(modulesConfigList, ',', false))
{
_additonalFiles.emplace_back(itr);
if (!itr.empty())
_additonalFiles.emplace_back(itr);
}
}
}

View File

@@ -18,10 +18,29 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <cstdint>
#include <stdexcept>
#include <string_view>
#include <vector>
enum class ConfigSeverity : uint8_t
{
Skip,
Warn,
Error,
Fatal
};
struct ConfigPolicy
{
ConfigSeverity defaultSeverity = ConfigSeverity::Warn;
ConfigSeverity missingFileSeverity = ConfigSeverity::Error;
ConfigSeverity missingOptionSeverity = ConfigSeverity::Warn;
ConfigSeverity criticalOptionSeverity = ConfigSeverity::Fatal;
ConfigSeverity unknownOptionSeverity = ConfigSeverity::Error;
ConfigSeverity valueErrorSeverity = ConfigSeverity::Error;
};
class ConfigMgr
{
ConfigMgr() = default;
@@ -32,7 +51,7 @@ class ConfigMgr
public:
bool LoadAppConfigs(bool isReload = false);
bool LoadModulesConfigs(bool isReload = false, bool isNeedPrintInfo = true);
void Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList = {});
void Configure(std::string const& initFileName, std::vector<std::string> args, std::string_view modulesConfigList = {}, ConfigPolicy policy = {});
static ConfigMgr* instance();

View File

@@ -211,13 +211,16 @@ void Log::ReadLoggersFromConfig()
AppenderConsole* appender = new AppenderConsole(NextAppenderId(), "Console", LOG_LEVEL_DEBUG, APPENDER_FLAGS_NONE, {});
appenders[appender->getId()].reset(appender);
Logger* rootLogger = new Logger(LOGGER_ROOT, LOG_LEVEL_ERROR);
Logger* rootLogger = new Logger(LOGGER_ROOT, LOG_LEVEL_WARN);
rootLogger->addAppender(appender->getId(), appender);
loggers[LOGGER_ROOT].reset(rootLogger);
Logger* serverLogger = new Logger("server", LOG_LEVEL_INFO);
serverLogger->addAppender(appender->getId(), appender);
loggers["server"].reset(serverLogger);
highestLogLevel = LOG_LEVEL_INFO;
return;
}
}