Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions core/foundation/inc/ROOT/RLogger.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include <mutex>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>

namespace ROOT {
Expand Down Expand Up @@ -132,13 +134,12 @@ public:
class RLogManager : public RLogChannel, public RLogHandler {
std::mutex fMutex;
std::list<std::unique_ptr<RLogHandler>> fHandlers;
mutable std::mutex fChannelVerbosityMutex;
std::unordered_map<std::string, ELogLevel> fChannelVerbosity;

public:
/// Initialize taking a RLogHandler.
RLogManager(std::unique_ptr<RLogHandler> lh) : RLogChannel(ELogLevel::kWarning)
{
fHandlers.emplace_back(std::move(lh));
}
RLogManager(std::unique_ptr<RLogHandler> lh);

static RLogManager &Get();

Expand All @@ -151,6 +152,28 @@ public:
/// Remove and return the given log handler. Returns `nullptr` if not found.
std::unique_ptr<RLogHandler> 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;
Expand Down Expand Up @@ -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
Expand Down
165 changes: 165 additions & 0 deletions core/foundation/src/RLogger.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <memory>
#include <vector>

Expand Down Expand Up @@ -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<RLogHandler> lh) : RLogChannel(ELogLevel::kWarning)
{
fHandlers.emplace_back(std::move(lh));
ConfigureFromEnv();
}

ROOT::RLogManager &ROOT::RLogManager::Get()
{
static RLogManager instance(std::make_unique<RLogHandlerDefault>());
Expand Down Expand Up @@ -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<std::mutex> 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<std::mutex> lock(fChannelVerbosityMutex);
if (level == ELogLevel::kUnset) {
fChannelVerbosity.erase(name);
} else {
fChannelVerbosity[name] = level;
}
}

void ROOT::RLogManager::ResetChannelVerbosity(const std::string &name)
{
std::lock_guard<std::mutex> lock(fChannelVerbosityMutex);
fChannelVerbosity.erase(name);
}

void ROOT::RLogManager::ResetAllChannelVerbosities()
{
std::lock_guard<std::mutex> 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<char>(std::tolower(static_cast<unsigned char>(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<int>(ELogLevel::kDebug) + offset;
if (lvl > 255)
lvl = 255;
if (lvl < 0)
lvl = 0;
return static_cast<ELogLevel>(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);
}
98 changes: 98 additions & 0 deletions core/foundation/test/testLogger.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}