diff --git a/core/foundation/inc/ROOT/RLogger.hxx b/core/foundation/inc/ROOT/RLogger.hxx index dd6e020688619..0916a83015379 100644 --- a/core/foundation/inc/ROOT/RLogger.hxx +++ b/core/foundation/inc/ROOT/RLogger.hxx @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include namespace ROOT { @@ -132,13 +134,12 @@ public: class RLogManager : public RLogChannel, public RLogHandler { std::mutex fMutex; std::list> fHandlers; + mutable std::mutex fChannelVerbosityMutex; + std::unordered_map fChannelVerbosity; public: /// Initialize taking a RLogHandler. - RLogManager(std::unique_ptr lh) : RLogChannel(ELogLevel::kWarning) - { - fHandlers.emplace_back(std::move(lh)); - } + RLogManager(std::unique_ptr lh); static RLogManager &Get(); @@ -151,6 +152,28 @@ public: /// Remove and return the given log handler. Returns `nullptr` if not found. std::unique_ptr Remove(RLogHandler *handler); + /// Get the verbosity configured for a specific channel name. Returns ELogLevel::kUnset if not configured. + ELogLevel GetChannelVerbosity(const std::string &name) const; + + /// Set the verbosity for a specific channel name. + void SetChannelVerbosity(const std::string &name, ELogLevel level); + + /// Reset the verbosity for a specific channel name (reverting to unconfigured). + void ResetChannelVerbosity(const std::string &name); + + /// Reset all channel-specific verbosities. + void ResetAllChannelVerbosities(); + + /// Configure channel and global verbosities from the ROOT_LOG (or ROOT_LOGGERS) environment variable. + void ConfigureFromEnv(); + + /// Configure channel and global verbosities from a configuration string. + /// Format: "Channel1=Level1,Channel2=Level2" (e.g. "ROOT.InterpreterPerf=Debug(3),ROOT.RBrowser=Error") + void ConfigureFromString(std::string_view config); + + /// Parse a string representing a log level (e.g. "Debug(3)", "Debug+3", "Error", "Warning", "Info", "Fatal", "Unset"). + static ELogLevel ParseLogLevel(std::string_view str); + // Emit a `RLogEntry` to the RLogHandlers. // Returns false if further emission of this Log should be suppressed. bool Emit(const RLogEntry &entry) override; @@ -308,9 +331,14 @@ inline RLogChannel &GetChannelOrManager(RLogChannel &channel) inline ELogLevel RLogChannel::GetEffectiveVerbosity(const RLogManager &mgr) const { - if (fVerbosity == ELogLevel::kUnset) - return mgr.GetVerbosity(); - return fVerbosity; + if (fVerbosity != ELogLevel::kUnset) + return fVerbosity; + if (!fName.empty()) { + auto channelLevel = mgr.GetChannelVerbosity(fName); + if (channelLevel != ELogLevel::kUnset) + return channelLevel; + } + return mgr.GetVerbosity(); } } // namespace ROOT diff --git a/core/foundation/src/RLogger.cxx b/core/foundation/src/RLogger.cxx index 9c0c39ba2123c..7b4c54e3fb046 100644 --- a/core/foundation/src/RLogger.cxx +++ b/core/foundation/src/RLogger.cxx @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include @@ -54,8 +56,23 @@ inline bool RLogHandlerDefault::Emit(const ROOT::RLogEntry &entry) entry.fMessage.c_str()); return true; } + +std::string_view TrimWhitespace(std::string_view sv) +{ + while (!sv.empty() && (sv.front() == ' ' || sv.front() == '\t' || sv.front() == '\r' || sv.front() == '\n')) + sv.remove_prefix(1); + while (!sv.empty() && (sv.back() == ' ' || sv.back() == '\t' || sv.back() == '\r' || sv.back() == '\n')) + sv.remove_suffix(1); + return sv; +} } // unnamed namespace +ROOT::RLogManager::RLogManager(std::unique_ptr lh) : RLogChannel(ELogLevel::kWarning) +{ + fHandlers.emplace_back(std::move(lh)); + ConfigureFromEnv(); +} + ROOT::RLogManager &ROOT::RLogManager::Get() { static RLogManager instance(std::make_unique()); @@ -106,3 +123,151 @@ bool ROOT::RLogManager::Emit(const ROOT::RLogEntry &entry) return false; return true; } + +ROOT::ELogLevel ROOT::RLogManager::GetChannelVerbosity(const std::string &name) const +{ + std::lock_guard lock(fChannelVerbosityMutex); + auto it = fChannelVerbosity.find(name); + if (it != fChannelVerbosity.end()) + return it->second; + return ELogLevel::kUnset; +} + +void ROOT::RLogManager::SetChannelVerbosity(const std::string &name, ELogLevel level) +{ + std::lock_guard lock(fChannelVerbosityMutex); + if (level == ELogLevel::kUnset) { + fChannelVerbosity.erase(name); + } else { + fChannelVerbosity[name] = level; + } +} + +void ROOT::RLogManager::ResetChannelVerbosity(const std::string &name) +{ + std::lock_guard lock(fChannelVerbosityMutex); + fChannelVerbosity.erase(name); +} + +void ROOT::RLogManager::ResetAllChannelVerbosities() +{ + std::lock_guard lock(fChannelVerbosityMutex); + fChannelVerbosity.clear(); +} + +ROOT::ELogLevel ROOT::RLogManager::ParseLogLevel(std::string_view str) +{ + str = TrimWhitespace(str); + if (str.empty()) + return ELogLevel::kUnset; + + int offset = 0; + std::string_view base = str; + + auto openParen = str.find('('); + auto closeParen = str.find(')', openParen); + if (openParen != std::string_view::npos && closeParen != std::string_view::npos && closeParen > openParen) { + base = TrimWhitespace(str.substr(0, openParen)); + std::string_view numStr = TrimWhitespace(str.substr(openParen + 1, closeParen - openParen - 1)); + try { + offset = std::stoi(std::string(numStr)); + } catch (...) { + offset = 0; + } + } else { + auto plusPos = str.find('+'); + if (plusPos != std::string_view::npos) { + base = TrimWhitespace(str.substr(0, plusPos)); + std::string_view numStr = TrimWhitespace(str.substr(plusPos + 1)); + try { + offset = std::stoi(std::string(numStr)); + } catch (...) { + offset = 0; + } + } else { + auto colonPos = str.find(':'); + if (colonPos != std::string_view::npos) { + base = TrimWhitespace(str.substr(0, colonPos)); + std::string_view numStr = TrimWhitespace(str.substr(colonPos + 1)); + try { + offset = std::stoi(std::string(numStr)); + } catch (...) { + offset = 0; + } + } + } + } + + std::string lowerBase; + lowerBase.reserve(base.size()); + for (char c : base) + lowerBase.push_back(static_cast(std::tolower(static_cast(c)))); + + if (lowerBase.size() > 1 && lowerBase.front() == 'k') { + std::string_view stripped(lowerBase.c_str() + 1, lowerBase.size() - 1); + if (stripped == "fatal" || stripped == "error" || stripped == "warning" || + stripped == "warn" || stripped == "info" || stripped == "debug" || stripped == "unset") + lowerBase = std::string(stripped); + } + + if (lowerBase == "fatal") + return ELogLevel::kFatal; + if (lowerBase == "error") + return ELogLevel::kError; + if (lowerBase == "warning" || lowerBase == "warn") + return ELogLevel::kWarning; + if (lowerBase == "info") + return ELogLevel::kInfo; + if (lowerBase == "debug") { + int lvl = static_cast(ELogLevel::kDebug) + offset; + if (lvl > 255) + lvl = 255; + if (lvl < 0) + lvl = 0; + return static_cast(lvl); + } + if (lowerBase == "unset") + return ELogLevel::kUnset; + + return ELogLevel::kUnset; +} + +void ROOT::RLogManager::ConfigureFromString(std::string_view config) +{ + while (!config.empty()) { + auto commaPos = config.find(','); + std::string_view entry = (commaPos == std::string_view::npos) ? config : config.substr(0, commaPos); + if (commaPos == std::string_view::npos) + config = {}; + else + config = config.substr(commaPos + 1); + + entry = TrimWhitespace(entry); + if (entry.empty()) + continue; + + auto eqPos = entry.find('='); + if (eqPos != std::string_view::npos) { + std::string_view key = TrimWhitespace(entry.substr(0, eqPos)); + std::string_view val = TrimWhitespace(entry.substr(eqPos + 1)); + ELogLevel level = ParseLogLevel(val); + if (key.empty() || key == "*" || key == "default" || key == "ROOT") { + SetVerbosity(level); + } else { + SetChannelVerbosity(std::string(key), level); + } + } else { + ELogLevel level = ParseLogLevel(entry); + SetVerbosity(level); + } + } +} + +void ROOT::RLogManager::ConfigureFromEnv() +{ + const char *env = std::getenv("ROOT_LOG"); + if (!env || !*env) + env = std::getenv("ROOT_LOGGERS"); + if (env && *env) + ConfigureFromString(env); +} diff --git a/core/foundation/test/testLogger.cxx b/core/foundation/test/testLogger.cxx index 0c9be1260d364..4503b7adf411e 100644 --- a/core/foundation/test/testLogger.cxx +++ b/core/foundation/test/testLogger.cxx @@ -305,3 +305,101 @@ TEST(Logger, ROOTErrorHandlerDiagString) SetErrorHandler(prevErrorHandler); } + +TEST(Logger, ParseLogLevel) +{ + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Debug(3)"), ROOT::ELogLevel::kDebug + 3); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Debug+5"), ROOT::ELogLevel::kDebug + 5); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Debug:2"), ROOT::ELogLevel::kDebug + 2); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Debug"), ROOT::ELogLevel::kDebug); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("kDebug"), ROOT::ELogLevel::kDebug); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Error"), ROOT::ELogLevel::kError); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("kError"), ROOT::ELogLevel::kError); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Warning"), ROOT::ELogLevel::kWarning); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("warn"), ROOT::ELogLevel::kWarning); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Info"), ROOT::ELogLevel::kInfo); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("FATAL"), ROOT::ELogLevel::kFatal); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("Unset"), ROOT::ELogLevel::kUnset); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel(" kInfo "), ROOT::ELogLevel::kInfo); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel(""), ROOT::ELogLevel::kUnset); + EXPECT_EQ(ROOT::RLogManager::ParseLogLevel("invalid_level"), ROOT::ELogLevel::kUnset); +} + +TEST(Logger, ConfigureFromString) +{ + auto &mgr = ROOT::RLogManager::Get(); + auto initialGlobal = mgr.GetVerbosity(); + + mgr.ConfigureFromString("ROOT.InterpreterPerf=Debug(3), ROOT.RBrowser=Error, default=Info"); + + ROOT::RLogChannel perf("ROOT.InterpreterPerf"); + ROOT::RLogChannel browser("ROOT.RBrowser"); + ROOT::RLogChannel other("ROOT.UnspecifiedChannel"); + + EXPECT_EQ(perf.GetEffectiveVerbosity(mgr), ROOT::ELogLevel::kDebug + 3); + EXPECT_EQ(browser.GetEffectiveVerbosity(mgr), ROOT::ELogLevel::kError); + EXPECT_EQ(other.GetEffectiveVerbosity(mgr), ROOT::ELogLevel::kInfo); + + // Clean up + mgr.ResetAllChannelVerbosities(); + mgr.SetVerbosity(initialGlobal); +} + +TEST(Logger, ConfigureFromEnv) +{ + auto &mgr = ROOT::RLogManager::Get(); + auto initialGlobal = mgr.GetVerbosity(); + + const char *prevEnv = std::getenv("ROOT_LOG"); + std::string savedEnv = prevEnv ? prevEnv : ""; + +#if defined(_WIN32) + _putenv("ROOT_LOG=ROOT.InterpreterPerf=Debug(3),ROOT.RBrowser=Error"); +#else + setenv("ROOT_LOG", "ROOT.InterpreterPerf=Debug(3),ROOT.RBrowser=Error", 1); +#endif + + mgr.ConfigureFromEnv(); + + ROOT::RLogChannel perf("ROOT.InterpreterPerf"); + ROOT::RLogChannel browser("ROOT.RBrowser"); + + EXPECT_EQ(perf.GetEffectiveVerbosity(mgr), ROOT::ELogLevel::kDebug + 3); + EXPECT_EQ(browser.GetEffectiveVerbosity(mgr), ROOT::ELogLevel::kError); + + { + TestLogger testLogger; + + // Debug(2) should be emitted for perf (effective level is Debug(3)) + R__LOG_DEBUG(2, perf) << "perf debug 2 emitted"; + EXPECT_EQ(testLogger.size(), 1); + + // Debug(4) should be suppressed for perf + R__LOG_DEBUG(4, perf) << "perf debug 4 suppressed"; + EXPECT_EQ(testLogger.size(), 1); + + // Warning should be suppressed for browser (effective level is Error) + R__LOG_WARNING(browser) << "browser warning suppressed"; + EXPECT_EQ(testLogger.size(), 1); + + // Error should be emitted for browser + R__LOG_ERROR(browser) << "browser error emitted"; + EXPECT_EQ(testLogger.size(), 2); + } + + // Restore environment and manager state +#if defined(_WIN32) + if (prevEnv) + _putenv(("ROOT_LOG=" + savedEnv).c_str()); + else + _putenv("ROOT_LOG="); +#else + if (prevEnv) + setenv("ROOT_LOG", savedEnv.c_str(), 1); + else + unsetenv("ROOT_LOG"); +#endif + + mgr.ResetAllChannelVerbosities(); + mgr.SetVerbosity(initialGlobal); +}