Hermes
Logging

Table of Contents

Hermes provides 4 streams of log messages: info, warn, error and critical. In practice, they just have different console colors (although in the future I may actually add some features here). The class taking care of logging is hermes::Log, but the easiest and recommended way to perform logging is by using the macros.

HERMES_LOG(FMT, ...);
HERMES_LOG_ERROR(FMT, ...);
#define HERMES_LOG_ERROR(FMT,...)
Logs into error log stream.
Definition: logging.h:385
#define HERMES_LOG_CRITICAL(FMT,...)
Logs into critical log stream.
Definition: logging.h:396
#define HERMES_LOG(FMT,...)
Logs into info log stream.
Definition: logging.h:363
#define HERMES_LOG_WARNING(FMT,...)
Logs into warning log stream.
Definition: logging.h:374

FMT is your logging message that can be formatted to include variable values - just like printf. However, it does not follow printf's directives. hermes::Log functions format strings in a more simple way assuming that every argument will accept std::stringstream << operator (i.e., your variables must work with std::cout for example). To put an argument inside your message, you use {}, like this:

HERMES_LOG_ERROR("{} errors in {}", 3, "foo");
// this will log the message
// "3 errors in foo"

By using the macros above, Hermes will prefix your messages with a label containig file location, function name, line number, time and log stream. So the message above would probably appear like this:

[2022-01-18 14:23:26] [error] [../my_code.cpp][35][foo] 3 errors in foo

You can customize the label and the colors by configuring hermes::Log variables and options:

int main() {
// tell hermes to abbreviate file path locations and
// output colored messages
hermes::Log::addOptions(hermes::log_options::abbreviate |
hermes::log_options::use_colors);
// choose warn message colors
hermes::Log::warn_color = 123; // value in [0,255]
HERMES_LOG_WARNING("warning message!");
return 0;
}
static u8 warn_color
warn stream messages color
Definition: logging.h:263
static void addOptions(logging_options options_to_add)
Enables logging options.
Definition: logging.h:253
Debug, logging and assertion macros.

You can choose among 256 colors, commonly used in terminals. You can consult them here in the 88/256 Colors section.

Sometimes you just want to log a code location while debugging to check if the coding is getting there, or simply log variables. Here is what you can do in those situations:

// log just the code location
// log a variable like: variable_name = value
HERMES_LOG_VARIABLE(variable_name);
// log multiple variable values in the same line
#define HERMES_PING
Logs into info stream code location.
Definition: logging.h:352
#define HERMES_LOG_VARIABLES(...)
Logs multiple variables into info log stream.
Definition: logging.h:455
#define HERMES_LOG_VARIABLE(A)
Logs variable name and value into info log stream.
Definition: logging.h:404

In case of logging in a CUDA code, you will not be able to use any of the macros above, you will have to use printf. The following macros do that for you:

// outputs to stdout
HERMES_C_LOG(FMT,...);
// outputs to stderr
// for convenience, the following macros do the same
// outputs to stdout
// outputs to stderr
#define HERMES_C_DEVICE_ERROR(FMT,...)
Logs into stderr from device code.
Definition: logging.h:506
#define HERMES_C_LOG(FMT,...)
Logs into stdout in printf style.
Definition: logging.h:467
#define HERMES_C_DEVICE_LOG(FMT,...)
Logs into info stdout from device code.
Definition: logging.h:493
#define HERMES_C_LOG_ERROR(FMT,...)
Logs into stderr in printf style.
Definition: logging.h:480

Note that in FMT now follows printf format options!

Finally, you may also intercept the log output as well. hermes::Log allows you to register callbacks to intercept log messages:

int main() {
// register a warn stream callback
hermes::Log::warn_callback = [](const hermes::Str& message) {
// handle message
};
// you can also fully redirect messages to your callbacks this way
hermes::log::addOptions(hermes::log_options::callback_only);
HERMES_LOG_WARNING("this message will not appear in console!");
return 0;
}
static std::function< void(const Str &)> warn_callback
warn stream redirection callback
Definition: logging.h:276
String class and set of string functions.
Definition: str.h:53

Memory Prints

Sometimes you want to actually analyse how data is arranged in memory, with addresses, alignments, and etc. hermes::MemoryDumper is a very useful tool for printing memory footprints like this:

0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 13 14 15 16 17
0x4C139960 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00
0x4C139978 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00
0x4C139990 06 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00

You can reproduce the result above with this code:

int main() {
u64 v[] = {0, 1, 2, 3, 4, 5, 6, 7};
return 0;
}
static std::string dump(const T *data, std::size_t size, u32 bytes_per_row=8, const RegionLayout &region=RegionLayout(), memory_dumper_options options=memory_dumper_options::none)
Dumps memory region.
Definition: memory_dump.h:239
uint64_t u64
64 bit size unsigned integer type
Definition: defs.h:89

For complex data arrangements you can define memory regions with colors to help your visualization. This is done by creating a hermes::MemoryDumper::RegionLayout instance and passing it to the dump function used in the example above. Here is an example of how to describe memory regions:

// imagine your data type is a struct like this:
struct S {
hermes::vec3 v; // floats {x, y, z}
hermes::point2 p; // floats {x, y, z}
};
//
// imagine now that you have an array with 3 instances of S
// and the following elements
S v[3] = {{{1, 2, 3}, {4, 5}},
{{10, 20, 30}, {40, 50}},
{{100, 200, 300}, {400, 500}}};

As you can see, we could color all bytes related to field S::v and S::p to help our visualization. Here is how we can define our memory layout:

using namespace hermes;
// we can construct sequentially our regions, from top to bottom
// so we first start by creating a big region layout containing the entire
// array, by using its size: sizeof(S) * 3
auto layout = MemoryDumper::RegionLayout().withSizeOf<S>(3)
// then we can define the first sub-region, the field S::v
// hermes::vec3 provides a base region layout for us, so we may use it and
// just choose a color, blue
// the same goes with field S::p, which receives the color yellow
static char blue[6]
"\e[34m"
Definition: console_colors.h:66
static char yellow[6]
"\e[33m"
Definition: console_colors.h:65
static MemoryDumper::RegionLayout memoryDumpLayout()
Gets memory layout.
Definition: math_element.h:56
RegionLayout & withColor(const std::string &console_color)
Modifies layout color.
Definition: memory_dump.h:110

Colors are provided by hermes::ConsoleColors.

A region layout may contain sub-region layouts, recursively. This way you can define memory layouts for complex classes and structures. hermes::MemoryDumper also lets you customize its output, then you can choose how it shows values for example. A cool feature is to print on the side of the memory footprint the values based on their data type. Let's put it all together in a single example:

struct S {
};
int main() {
// setup data
S v[3] = {{{1, 2, 3}, {4, 5}},
{{10, 20, 30}, {40, 50}},
{{100, 200, 300}, {400, 500}}};
// describe data
auto layout = MemoryDumper::RegionLayout().withSizeOf<S>(3)
.withSubRegion(vec3::memoryDumpLayout().withColor(ConsoleColors::blue))
.withSubRegion(point2::memoryDumpLayout().withColor(ConsoleColors::yellow));
// print out memory
MemoryDumper::dump(v, 3, 8, layout,
// note the options for printing actual values on the side
// and to color the output
memory_dumper_options::type_values | memory_dumper_options::colored_output);
return 0;
}
Geometric point classes.
Geometric vector classes.

Here are some screenshots of other types of outputs: