Skip to content

Lumberjack

Federico Sossai edited this page Dec 13, 2024 · 26 revisions

Lumberjack is a reusable log manager. It offers a single point of control for the desired verbosity.

Overview

There are only two classes you need to know: Logger and Lumberjack.

A Logger object is used to generate log messages and refers to one Lumberjack object. A Lumberjack object dictates whether messages generated by Loggers that rely on it should appear or not. Verbosity can be set to the desired level for each Logger, independently. A Lumberjack object is bound to a JSON file that represents the single point of control for the user of Noelle.

Verbosity levels

Noelle has only one globally available Lumberjack instance called NoelleLumberjack. Verbosity levels are 1 (called info) and 2 (called debug). Verbosity levels can be edited through a JSON file located at $(noelle-config --logs) that looks like this:

{
  "default_verbosity": 1,
  "separator": ": ",
  "verbosity_override": {
    "MyLogger1": 2,
    "MyLogger2": 0
  }
}

The "default_verbosity" level applies for all loggers that are not mentioned in the "verbosity_override" dictionary.

  • 0: Logs are disabled. It correspondes to LOG_DISABLED
  • 1: Only log.info() are enabled. It correspondes to LOG_INFO
  • 2: Both log.debug() and log.info() are enabled. It correspondes to LOG_DEBUG

Initialization

Logger log(NoelleLumberjack, "JohnDoe");

// now, you can edit the JSON file and override the verbosity for "JohnDoe"

Messages

log.level(LOG_BYPASS) << "This message is printed no matter what\n";
log.level(LOG_INFO) << "Info message\n";
log.level(LOG_DEBUG) << "Debug message\n";

// or more concisely:

log.bypass() << "This message is printed no matter what\n";
log.info() << "Info message\n";
log.debug() << "Debug message\n";

will produce:

JohnDoe: This message is printed no matter what
JohnDoe: Debug message
JohnDoe: Info message

Sections and Guards

Sections objects follow lexical scope, that is, are closed by their destructors. A guard let you defer a log to the end of the object lifetime. Sections provide the onExit() method too.

log.info() << "Start\n";

auto g = log.guard();
g.onExit(LOG_INFO, "End\n"); // called by dtor

log.info() << "Message 1\n";
{
  auto s1 = log.namedSection("Details");
  log.debug() << "Message 2\n";
  {
    auto s2 = log.indentedSection();
    s2.onExit("Goodbye");
    log.debug() << "Message 3\n";
    log.debug() << *V->getType() << "\n";
    log.debug().noPrefix() << *V->getType() << "\n";
  }
  log.debug() << "Message 4\n";
}
// log.info() << "End\n"; // avoid this, use a guard instead

with verbosity level 2 (debug+info) will produce:

JohnDoe: Start
JohnDoe: Message 1
JohnDoe: Details: Message 2
JohnDoe: Details:   Message 3
JohnDoe: Details:   i32
JohnDoe: Details: Goodbye
i32
JohnDoe: Details: Message 4
JohnDoe: End

and with verbosity level 1 (info only) will produce:

JohnDoe: Start
JohnDoe: Message 1
JohnDoe: End

Notice how the Guard object g will print Exit irrespective of where we leave its lexical scope.

Objects with custom print() methods

Some objects (e.g. arcana::noelle:SCC) cannot be directly printed through raw_ostream::operator<<() because they are not native LLVM types, but they might be equipped with one or both of the following methods:

  • void print(llvm::raw_ostream &stream)
  • void print(llvm::raw_ostream &stream, std::string prefix)

Lumberjack detects these possibilities, making objects printable with no additional API complications. When calling, say, log.info() << obj, the logger will automatically invoke obj.print(...) passing an appropriate prefix.

scc->print(errs(), "JohnDoe: ");  // avoid this if possible
log.info() << *scc;               // use this instead

For printing multi-line objects that don't have a dedicated method for printing with a prefix (e.g. BasicBlock) it is recommended to hide Lumberjack automatic prefix:

BasicBlock &BB = ...;
log.info().noPrefix() << BB << "\n";

Lambdas

Lambdas are conditinally invoked. The return value of a lambda will be computed only if the message will be displayed.

int i = 0;
auto heavyLambda = [&i]() {
  i++;
  return "This string took long to compute";
};

log.debug() << "Result: " << heavyLambda << "\n";

In the example above, if debug messages are disabled, i will be zero after the call. Notice that object being passed is the lambda variable and not it's return value.

Note: Don't write side-effectful lambda to be used with the logger! You don't want the logic of the program to change with the verbosity, do you..

When building on Noelle ...

.. you should instantiate a new Lumberjack, so that Loggers belonging to different codebases are kept seperate and controlled through different JSON files.

Lumberjack MyProjectLumberjack("path/to/Lumberjack.json", llvm::errs());