Skip to content
Open
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
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;
}
}
4 changes: 4 additions & 0 deletions core/foundation/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ 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)
set_tests_properties(RLoggerEnvVar PROPERTIES
ENVIRONMENT "ROOT_LOG=ROOT.TestChannel=Error")
52 changes: 52 additions & 0 deletions core/foundation/test/RLoggerEnvVar.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Test that ROOT_LOG env var correctly configures RLogger channel verbosity.
// ROOT_LOG is parsed once at RLogManager construction (process startup),
// so each test case is a separate executable launched with the env var set.

#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)
{
// ROOT_LOG was set to 'ROOT.TestChannel=Error' before process start
// (set in CMakeLists via set_tests_properties)
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)
{
// Channel has no explicit verbosity set, so should use env var value
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.
// ROOT_LOG sets ROOT.TestChannel=Error, but we explicitly set it to kInfo here.
// The explicit setting should win.
TEST(RLoggerEnvVar, ExplicitVerbosityTakesPrecedenceOverEnvVar)
{
// ROOT_LOG set ROOT.TestChannel=Error via environment
// Now explicitly override it to kInfo
TestChannel().SetVerbosity(ROOT::ELogLevel::kInfo);

// Explicit verbosity should win over env var
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);
}