Skip to content
59 changes: 45 additions & 14 deletions core/foundation/inc/ROOT/RLogger.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <mutex>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>

namespace ROOT {
Expand Down Expand Up @@ -127,19 +128,28 @@ public:
A RLogHandler that multiplexes diagnostics to different client `RLogHandler`s
and keeps track of the sum of `RLogDiagCount`s for all channels.

`RLogHandler::Get()` returns the process's (static) log manager.
*/
`RLogManager::Get()` returns the process's (static) log manager.

The verbosity of individual channels can be configured at startup via the
`ROOT_LOG` environment variable. The format is a comma-separated list of
`ChannelName=Level` pairs, where `Level` is one of `Fatal`, `Error`,
`Warning`, `Info`, or `Debug` (optionally with an integer verbosity offset,
e.g. `Debug(3)`). Example:
~~~
export ROOT_LOG='ROOT.InterpreterPerf=Debug(3),ROOT.RBrowser=Error'
~~~
*/
class RLogManager : public RLogChannel, public RLogHandler {
std::mutex fMutex;
std::list<std::unique_ptr<RLogHandler>> fHandlers;

/// Verbosity overrides parsed from ROOT_LOG, keyed by channel name.
/// Applied to a channel the first time it calls GetEffectiveVerbosity().
std::unordered_map<std::string, ELogLevel> fEnvVerbosity;

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

static RLogManager &Get();

Expand All @@ -152,6 +162,16 @@ public:
/// Remove and return the given log handler. Returns `nullptr` if not found.
std::unique_ptr<RLogHandler> Remove(RLogHandler *handler);

/// Return the verbosity override for the named channel, or ELogLevel::kUnset if none.
/// Used by RLogChannel::GetEffectiveVerbosity() to apply ROOT_LOG settings lazily.
ELogLevel GetEnvVerbosity(const std::string &channelName) const
{
auto it = fEnvVerbosity.find(channelName);
if (it != fEnvVerbosity.end())
return it->second;
return ELogLevel::kUnset;
}

// Emit a `RLogEntry` to the RLogHandlers.
// Returns false if further emission of this Log should be suppressed.
bool Emit(const RLogEntry &entry) override;
Expand All @@ -171,7 +191,6 @@ struct RLogLocation {
One can construct a RLogEntry through RLogBuilder, including streaming into
the diagnostic message and automatic emission.
*/

class RLogEntry {
public:
RLogLocation fLocation;
Expand Down Expand Up @@ -206,7 +225,6 @@ namespace Detail {
~~~
This will automatically capture the current class and function name, the file and line number.
*/

class RLogBuilder : public std::ostringstream {
/// The log entry to be built.
RLogEntry fEntry;
Expand Down Expand Up @@ -267,7 +285,9 @@ public:
/// Construct the scoped count given a counter (e.g. a channel or RLogManager).
/// The counter's lifetime must exceed the lifetime of this object!
explicit RLogScopedDiagCount(RLogDiagCount &cnt)
: fCounter(&cnt), fInitialWarnings(cnt.GetNumWarnings()), fInitialErrors(cnt.GetNumErrors()),
: fCounter(&cnt),
fInitialWarnings(cnt.GetNumWarnings()),
fInitialErrors(cnt.GetNumErrors()),
fInitialFatalErrors(cnt.GetNumFatalErrors())
{
}
Expand Down Expand Up @@ -309,9 +329,20 @@ inline RLogChannel &GetChannelOrManager(RLogChannel &channel)

inline ELogLevel RLogChannel::GetEffectiveVerbosity(const RLogManager &mgr) const
{
if (fVerbosity == ELogLevel::kUnset)
return mgr.GetVerbosity();
return fVerbosity;
// If this channel has an explicit verbosity set, use it.
if (fVerbosity != ELogLevel::kUnset)
return fVerbosity;

// Check if the ROOT_LOG environment variable specified a verbosity for
// this channel by name. Named channels have a non-empty name.
if (!fName.empty()) {
ELogLevel envLevel = mgr.GetEnvVerbosity(fName);
if (envLevel != ELogLevel::kUnset)
return envLevel;
}

// Fall back to the global manager verbosity.
return mgr.GetVerbosity();
}

} // namespace ROOT
Expand Down Expand Up @@ -360,4 +391,4 @@ inline ELogLevel RLogChannel::GetEffectiveVerbosity(const RLogManager &mgr) cons
#define R__LOG_DEBUG(DEBUGLEVEL, ...) R__LOG_TO_CHANNEL(ROOT::ELogLevel::kDebug + DEBUGLEVEL, __VA_ARGS__)
///\}

#endif
#endif
102 changes: 99 additions & 3 deletions core/foundation/src/RLogger.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@

#include <algorithm>
#include <array>
#include <cstdlib>
#include <memory>
#include <sstream>
#include <string>
#include <vector>

// pin vtable
ROOT::RLogHandler::~RLogHandler() {}

namespace {

class RLogHandlerDefault : public ROOT::RLogHandler {
public:
// Returns false if further emission of this log entry should be suppressed.
Expand Down Expand Up @@ -55,8 +59,102 @@ inline bool RLogHandlerDefault::Emit(const ROOT::RLogEntry &entry)
entry.fMessage.c_str());
return true;
}

/// Trim leading and trailing whitespace from a string.
std::string_view TrimWhitespace(std::string_view s)
{
const auto begin = s.find_first_not_of(" \t\r\n");
if (begin == std::string_view::npos)
return {};
const auto end = s.find_last_not_of(" \t\r\n");
return s.substr(begin, end - begin + 1);
}

/// Parse a level string such as "Debug", "Debug(3)", "Info", "Warning", "Error", "Fatal".
/// Returns the corresponding ELogLevel. For Debug(N), the returned level is kDebug + N.
ROOT::ELogLevel ParseLogLevel(std::string_view levelStr)
{
if (levelStr.compare(0, 5, "Debug") == 0) {
int extra = 0;
auto parenOpen = levelStr.find('(');
if (parenOpen != std::string::npos) {
auto parenClose = levelStr.find(')', parenOpen);
if (parenClose != std::string::npos) {
try {
extra = std::stoi(std::string(levelStr.substr(parenOpen + 1, parenClose - parenOpen - 1)));
} catch (...) {
extra = 0;
::Warning("ROOT_LOG", "Cannot parse verbosity level in '%s', defaulting to Debug", levelStr.data());
}
}
}
return ROOT::ELogLevel::kDebug + extra;
}
if (levelStr == "Info")
return ROOT::ELogLevel::kInfo;
if (levelStr == "Warning")
return ROOT::ELogLevel::kWarning;
if (levelStr == "Error")
return ROOT::ELogLevel::kError;
if (levelStr == "Fatal")
return ROOT::ELogLevel::kFatal;

// Unrecognised string: warn the user and return kUnset so the channel falls back to global.
::Warning("ROOT_LOG", "Unrecognized log level '%s', ignoring", levelStr.data());
return ROOT::ELogLevel::kUnset;
}

/// Parse ROOT_LOG and return a map of channel-name -> verbosity level.
/// Format: "Channel1=Level1,Channel2=Debug(N),..."
std::unordered_map<std::string, ROOT::ELogLevel> ParseRootLogEnvVar()
{
std::unordered_map<std::string, ROOT::ELogLevel> result;

const char *envVal = std::getenv("ROOT_LOG");
if (!envVal)
return result;

std::stringstream ss(envVal);
std::string token;
while (std::getline(ss, token, ',')) {
token = TrimWhitespace(token);
if (token.empty())
continue;

auto eq = token.find('=');
if (eq == std::string::npos)
continue;

std::string_view channelName = TrimWhitespace(std::string_view(token).substr(0, eq));
std::string_view levelStr = TrimWhitespace(std::string_view(token).substr(eq + 1));

if (channelName.empty() || levelStr.empty())
continue;

ROOT::ELogLevel level = ParseLogLevel(levelStr);
if (level != ROOT::ELogLevel::kUnset)
result[std::string(channelName)] = level;
}
return result;
}

} // unnamed namespace

/// Construct the RLogManager, install the default handler, then apply
/// gDebug and the ROOT_LOG environment variable.
ROOT::RLogManager::RLogManager(std::unique_ptr<RLogHandler> lh) : RLogChannel(ELogLevel::kWarning)
{
fHandlers.emplace_back(std::move(lh));

// Apply gDebug as a global verbosity floor.
// gDebug == 1 maps to kDebug, gDebug == 2 to kDebug+1, etc.
if (gDebug > 0)
SetVerbosity(ELogLevel::kDebug + (gDebug - 1));

// Parse ROOT_LOG and store per-channel overrides for lazy application.
fEnvVerbosity = ParseRootLogEnvVar();
}

ROOT::RLogManager &ROOT::RLogManager::Get()
{
static RLogManager instance(std::make_unique<RLogHandlerDefault>());
Expand Down Expand Up @@ -93,10 +191,8 @@ bool ROOT::RLogManager::Emit(const ROOT::RLogEntry &entry)
// Lock-protected extraction of handlers, such that they don't get added during the
// handler iteration.
std::vector<RLogHandler *> handlers;

{
std::lock_guard<std::mutex> lock(fMutex);

handlers.resize(fHandlers.size());
std::transform(fHandlers.begin(), fHandlers.end(), handlers.begin(),
[](const std::unique_ptr<RLogHandler> &handlerUPtr) { return handlerUPtr.get(); });
Expand All @@ -106,4 +202,4 @@ bool ROOT::RLogManager::Emit(const ROOT::RLogEntry &entry)
if (!handler->Emit(entry))
return false;
return true;
}
}
7 changes: 6 additions & 1 deletion core/foundation/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
ROOT_ADD_GTEST(testMake_unique testMake_unique.cxx LIBRARIES Core)
ROOT_ADD_GTEST(testTypeTraits testTypeTraits.cxx LIBRARIES Core)
ROOT_ADD_GTEST(testNotFn testNotFn.cxx LIBRARIES Core)
ROOT_ADD_GTEST(testClassEdit testClassEdit.cxx LIBRARIES Core RIO COPY_TO_BUILDDIR ${CMAKE_CURRENT_SOURCE_DIR}/file_16199.C)
ROOT_ADD_GTEST(testClassEdit testClassEdit.cxx
LIBRARIES Core RIO
COPY_TO_BUILDDIR ${CMAKE_CURRENT_SOURCE_DIR}/file_16199.C)
ROOT_ADD_GTEST(testException testException.cxx LIBRARIES Core GTest::gmock)
ROOT_ADD_GTEST(testLogger testLogger.cxx LIBRARIES Core)
ROOT_ADD_GTEST(testRRangeCast testRRangeCast.cxx LIBRARIES Core)
ROOT_ADD_GTEST(testStringUtils testStringUtils.cxx LIBRARIES Core)
ROOT_ADD_GTEST(FoundationUtilsTests FoundationUtilsTests.cxx LIBRARIES Core INCLUDE_DIRS ../res)
ROOT_ADD_GTEST(RLoggerEnvVar RLoggerEnvVar.cxx
LIBRARIES Core
ENVIRONMENT ROOT_LOG=ROOT.TestChannel=Error)
43 changes: 43 additions & 0 deletions core/foundation/test/RLoggerEnvVar.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Test that ROOT_LOG env var correctly configures RLogger channel verbosity.
// ROOT_LOG is parsed once at RLogManager construction (process startup),
// so the env var is set via ENVIRONMENT in CMakeLists.txt.

#include "ROOT/RLogger.hxx"
#include "gtest/gtest.h"

// Declare a test channel the same way ROOT modules do
ROOT::RLogChannel &TestChannel()
{
static ROOT::RLogChannel channel("ROOT.TestChannel");
return channel;
}

// Test: channel verbosity set via ROOT_LOG is reflected in GetEnvVerbosity
TEST(RLoggerEnvVar, EnvVerbosityIsStored)
{
auto level = ROOT::RLogManager::Get().GetEnvVerbosity("ROOT.TestChannel");
EXPECT_EQ(level, ROOT::ELogLevel::kError);
}

// Test: unknown channel returns kUnset
TEST(RLoggerEnvVar, UnknownChannelReturnsUnset)
{
auto level = ROOT::RLogManager::Get().GetEnvVerbosity("ROOT.DoesNotExist");
EXPECT_EQ(level, ROOT::ELogLevel::kUnset);
}

// Test: channel effective verbosity uses env var when channel has no explicit level
TEST(RLoggerEnvVar, EffectiveVerbosityUsesEnvVar)
{
auto effective = TestChannel().GetEffectiveVerbosity(ROOT::RLogManager::Get());
EXPECT_EQ(effective, ROOT::ELogLevel::kError);
}

// Test: explicitly set verbosity on a channel takes precedence over ROOT_LOG env var.
TEST(RLoggerEnvVar, ExplicitVerbosityTakesPrecedenceOverEnvVar)
{
TestChannel().SetVerbosity(ROOT::ELogLevel::kInfo);
EXPECT_EQ(TestChannel().GetEffectiveVerbosity(ROOT::RLogManager::Get()), ROOT::ELogLevel::kInfo);
// Reset back to kUnset so other tests are not affected
TestChannel().SetVerbosity(ROOT::ELogLevel::kUnset);
}