You.i Engine
Logging

Detailed Description

Allows information to be output to a log.

You.i Engine's logging system provides a method for outputting, at run-time, information relating to the application.

Warning
The You.i Engine logging system is built on top of the spdlog logging engine library, and is purposely not entirely encapsulated, in order to achieve the maximum logging performance and to avoid unnecessary overhead. Please note however that using spdlog directly and bypassing the You.i Engine logging API may cause undefined behavior, is not supported and is strongly discouraged, unless explicitly allowed in certain places.

Generating Logs

By default logging is enabled in You.i Engine and all messages are output using the default logging sink for the target platform.

Macros are provided to simplify writing log messages to the logging system. These are the macros for each of the log severities supported by the logger:

Log Macro Description
YI_LOGF Logs a fatal error and immediately aborts the application.
YI_LOGE Logs an error.
YI_LOGW Logs a warning.
YI_LOGI Logs information.
YI_LOGD Logs debug information.

The log macros listed in the table above are listed in order of severity, from highest to lowest. All log macros have the same syntax and support variadic arguments for the message.

The following code snippet displays the use of a log macro in its simplest form:

// Logs a simple debugging message without arguments
YI_LOGD("MyExampleClass", "Something is about to happen.");
// Log output: "Something is about to happen."

All of the logging macros support dynamic arguments injected into a specified format string, for instance:

// Logs an informational message with the tag MyExampleClass.
int32_t value = 10;
YI_LOGI("MyExampleClass", "The value is: %d.", value);
// Log output: "The value is: 10."

While the spdlog library supports fmt::print-style argument format specifiers by default, for compatibility with existing code the macros above use legacy printf-style format API as implemented by the fmt library. See fmt::printf API for precise format details.

Warning
Unlike their standard counterparts, the fmt functions are type-safe and throw an exception if an argument type does not match its format specification. In the previous implementation of the You.i logging engine certain format specifiers were less strict in terms of accepted argument types. The current implementation requires enum classes to be explicitly cast to the int type and pointers must be explicitly cast to the void pointer type to avoid compilation failures:
// enum values must be cast to int explicitly:
enum class MyEnum
{
ELEMENT1,
ELEMENT2,
ELEMENT3
} e = MyEnum::ELEMENT2;
// YI_LOGD("MyClass", "The enum value is %d.", e); // Build failure (type mismatch, enum class != int)
YI_LOGD("MyClass", "The enum value is %d.", static_cast<int>(e)); // OK
// pointers must be cast to void * explicitly:
struct MyStruct
{
} myStruct;
MyStruct *pStruct = &myStruct;
// YI_LOGD("MyClass", "The pointer points to %p.", pStruct); // Build failure, pointer type mismatch
YI_LOGD("MyClass", "The pointer points to %p.", static_cast<void *>(pStruct)); // OK

Log Message Presentation

The appearance of log messages can be customized. Use CYILogger::SetPattern to specify a different format string to use.

The pattern flags are in the form of "%flag" and are modeled after the strftime() function:

flag meaning
"%v" The actual message text to log
"%t" Thread id
"%P" Process id
"%n" Logger's name
"%l" The full log level of the message
"%L" Short log level of the message
"%a" Abbreviated weekday name
"%A" Full weekday name
"%b" Abbreviated month name
"%B" Full month name
"%c" Date and time representation
"%C" Year in 2 last digits
"%Y" Year in 4 full digits
"%D" or "%x" Short MM/DD/YY date
"%m" Month (01-12)
"%d" Day of month (01-31)
"%H" Hours in 24-hour format (00-23)
"%I" Hours in 12-hour format (01-12)
"%M" Minutes (00-59)
"%S" Seconds (00-59)
"%e" Millisecond part of the current second (000-999)
"%f" Microsecond part of the current second (000000-999999)
"%F" Nanosecond part of the current second (000000000-999999999)
"%p" AM/PM string
"%r" Time using 12-hour clock
"%R" Time using 24-hour clock (HH:MM), equivalent to "%H:%M"
"%T" or "%X" ISO 8601 time format (HH:MM:SS), equivalent to "%H:%M:%S"
"%z" ISO 8601 offset from UTC in timezone ([+/-]HH:MM)
"%E" Seconds since the epoch
"%%" The % sign
"%o" Elapsed time in milliseconds since previous message
"%i" Elapsed time in microseconds since previous message
"%u" Elapsed time in nanoseconds since previous message
"%O" Elapsed time in seconds since previous message

For more format specifiers, see the spdlog formatting documentation

The default format string used by the logger depends on whether the code is compiled in release or debug mode. The debug mode format string presents log messages more verbosely, providing more context for easier debugging. On the other hand, the release mode log messages are slimmer, to reduce the amount of logging traffic and to prevent a performance impact.

Passing Log Tags

The best way to pass a log tag to a YI_LOGx macro is to pass a string literal like so:

YI_LOGI("MyExampleClass", "An example log message.");

To avoid repeating the same string in multiple log calls, a macro constant named LOG_TAG could be used:

#define LOG_TAG "MyExampleClass"
YI_LOGI(LOG_TAG, "An example log message.");
YI_LOGW(LOG_TAG, "Another example log message.");

String instances (CYIString and std::string) can also be passed in:

CYIString logTag("MyExampleClass");
YI_LOGI(logTag, "An example log.");
Warning
Non-static char arrays should never be passed to the logger functions. Passing a non-static char array may result in runtime crashes.

Filtering Ouput

By default messages of all severities are output, including logs generated by You.i Engine. However, the application developer may not want to output all log messages, and to accommodate this the logging system has a built-in mechanism for log filtering.

The EYILogLevel is an enumeration representing the severity of a log message. Here is the full list sorted by severity (highest to lowest):

Severity Meaning
EYILogLevel::critical A critical failure message (application execution is aborted)
EYILogLevel::err An error message
EYILogLevel::warn A warning message
EYILogLevel::info An information message
EYILogLevel::debug A debugging message

To configure the minimum level of messages to be emitted, call CYILogger::SetLevel with the desired value. All messages below the specified threshold will be ignored and not added to the log. Passing the special level value of EYILogLevel::off suppresses all emitted messages.

// this will only let through information messages and above
CYILogger::SetLevel(EYILogLevel::info);
// this will suppress all logging messages
CYILogger::SetLevel(EYILogLevel::off);

In case multiple logging sinks are used (see below), each sink can have its own logging level specified. Thus, it's possible, for instance, to do verbose logging to a file but to only send warnings and errors to the console.

In addition to the global level-based filtering, there's a way to provide custom log message filtering based on arbitrary criteria. The logging subsystem supports a stack of log message filters chained together in a way that selectively removes only certain messages from the logging feed and leaves others intact. Each such filter in the stack reduces the logging traffic in a way similar to how photo camera filters filter out light that reaches the camera sensor/film. The pruned log messages are then sent to all of the installed logging sinks (where each sink can have its own minimum pass-through level, as mentioned above)

To add a log message filter, use the CYILogger::PushFilter method with an std::function predicate of your choice to perform per-message based filtering. The predicate accepts the message to be logged and decides whether to keep it or to discard it. If the predicate function returns true, the message is kept, otherwise it gets rejected. For example:

// this is effectively the reverse of the CYILogger::SetLevel() behaviour, only allowing verbose messages through
CYILogger::PushFilter([](const CYILogMessage &message) {
return message.level < EYILogLevel::info; // keep only debug and trace messages (just to illustrate a point)
});
// another artificial example showing that filtering can be done based on any criteria of your choosing
CYILogger::PushFilter([](const CYILogMessage &message) {
// reject any messages except those containing "oops" in them
return CYIString(message.payload.data(), message.payload.size()).Contains("oops");
});

As is the case with any std::function, the predicate can be a lambda, a regular function or method, or any other callable object.

To remove filters from the stack (the last one added is removed first), use CYILogger::PopFilter. To remove all installed filters, invoke CYILogger::RemoveAllFilters.

A common use case for log filtering is reducing logs based on tags, where certain log tags get selectively suppressed or made to stand out more than the rest. To facilitate this scenario, CYILogger::CreateFilter method takes a map of log tags as keys and minimum levels as values. As a result, if a log message with one of the given tags is encountered, the matching log level will be used as a cut-off threshold. If the tag is not found in the map, by default the message is allowed through and normal sink levels (if configured) are applied as the next step. The optional elseMinLevel argument provides a way to specify the default minimum level in the fallback case. This can be used to perform an "opt-in" style of logging, where most/all of the log messages are ignored, except those that are specified in the map with their respective baseline levels. One useful application of this is in automated tests, where code is only interested in specific messages to be emitted to the log. It usually makes sense to use this mode for the last filter of the filter stack.

Example 1:

// this filter will only pass through warning and error messages for tag "Tag1"
// and information and above for "Tag2", the rest will come through unchanged
// (and may be filtered out by individual sinks, if configured)
CYILogger::PushFilter(CYILogger::CreateFilter({{"Tag1", EYILogLevel::warn},
{"Tag2", EYILogLevel::info}}));

Example 2:

// we specify a special fallback minimum logging level: only Tag1 and Tag2 messages will come through
// (if their levels are higher than specified), the rest of messages will be all suppressed.
CYILogger::PushFilter(CYILogger::CreateFilter({{"Tag1", EYILogLevel::debug},
{"Tag2", EYILogLevel::trace}},
EYILogLevel::off));

Custom Log Sinks

The logging system supports simultaneous output to multiple logging destinations like console, log file or other custom targets called "logging sinks". The default output sink depends on the target platform and can use platform-specific logging facilities. For instance, Android logs are emitted in a way that allows them to be analyzed using the stock Android logcat tool, and Apple logs use the Unified Logging subsystem.

In order to provide additional logging targets like special log files or to implement logging over the network, custom sinks can be implemented by extending the CYILogSink class or the spdlog::sinks::sink class template directly. Third-party custom spdlog sinks are supported as well.

An example of a simple custom sink implementation:

class CustomLogSink : public CYILogSink
{
public:
CustomLogSink();
protected:
virtual void sink_it_(const CYILogMessage &message) override;
};
CustomLogSink::CustomLogSink()
{
set_pattern("[%n] %#:%!: %v");
}
void CustomLogSink::sink_it_(const CYILogMessage &message)
{
(message.level > EYILogLevel::info ? std::cerr : std::cout)
<< "Custom Logger: "
<< std::endl;
}

To add a custom sink, call CYILogger::AddSink, and to remove it, use CYILogger::RemoveSink. If you want to completely avoid using the stock logger sinks and reconfigure the logger to redirect your logs elsewhere, it's possible to remove all installed sinks by calling CYILogger::RemoveAllSinks and add the desired ones from scratch.

To send log output to a file, use CYILogger::CreateFileSink to create the logging sink to do so. You can have multiple file sinks at the same time, perhaps with different log levels applied to them. (Remember that the log filtering stack is applied to all of the installed sinks.)

Example:

// this will install a new sink to additionally send the logs to custom.log file, discarding its old contents
CYILogger::AddSink(CYILogger::CreateFileSink("custom.log", true /* truncate old log files */));

Note that unlike the stock spdlog engine, the You.i logging engine currently only supports a single logger (but with multiple output sinks).

Classes

class  CYILogger
 
class  CYILogSink
 A parent class for all logging sinks, supported by the spdlog logging engine. More...
 

Macros

#define YI_LOG_PURE(tag, message, ...)   _YI_LOG_AT_LEVEL(EYILogLevel::critical, (tag), message, ##__VA_ARGS__)
 
#define YI_LOGF(tag, message, ...)
 
#define YI_LOGE(tag, message, ...)   _YI_LOG_AT_LEVEL(EYILogLevel::err, (tag), message, ##__VA_ARGS__)
 
#define YI_LOGW(tag, message, ...)   _YI_LOG_AT_LEVEL(EYILogLevel::warn, (tag), message, ##__VA_ARGS__)
 
#define YI_LOGI(tag, message, ...)   _YI_LOG_AT_LEVEL(EYILogLevel::info, (tag), message, ##__VA_ARGS__)
 
#define YI_LOGD(tag, message, ...)   _YI_LOG_AT_LEVEL(EYILogLevel::debug, (tag), message, ##__VA_ARGS__)
 

Typedefs

using EYILogLevel = spdlog::level::level_enum
 
using CYILogMessage = spdlog::details::log_msg
 

Macro Definition Documentation

#define YI_LOG_PURE (   tag,
  message,
  ... 
)    _YI_LOG_AT_LEVEL(EYILogLevel::critical, (tag), message, ##__VA_ARGS__)

Macro to log without a severity tag. This will always print even in a release build.

#define YI_LOGD (   tag,
  message,
  ... 
)    _YI_LOG_AT_LEVEL(EYILogLevel::debug, (tag), message, ##__VA_ARGS__)

Use this macro to log a debug message.

#define YI_LOGE (   tag,
  message,
  ... 
)    _YI_LOG_AT_LEVEL(EYILogLevel::err, (tag), message, ##__VA_ARGS__)

Use this macro to log an error.

#define YI_LOGF (   tag,
  message,
  ... 
)
Value:
do \
{ \
_YI_LOG_AT_LEVEL(EYILogLevel::critical, (tag), message, ##__VA_ARGS__); \
abort(); \
} while ((void)0, false)
static void Flush()
#define _YI_LOG_AT_LEVEL(level, tag, message,...)
Definition: YiLogger.h:48

Use this macro to log a fatal error.

Warning
This log macro will abort the application.
#define YI_LOGI (   tag,
  message,
  ... 
)    _YI_LOG_AT_LEVEL(EYILogLevel::info, (tag), message, ##__VA_ARGS__)

Use this macro to log some information.

#define YI_LOGW (   tag,
  message,
  ... 
)    _YI_LOG_AT_LEVEL(EYILogLevel::warn, (tag), message, ##__VA_ARGS__)

Use this macro to log a warning.

Typedef Documentation

using CYILogMessage = spdlog::details::log_msg

A structure containing a log message with additional attributes like source file and line.

using EYILogLevel = spdlog::level::level_enum

An enum representing a logging level/message severity.