Omniverse Logging
Fast, feature rich, and easy to extend
Overview
Omniverse’s logging system is designed around the concept of a “channel”. A channel is a stream of related messages. For example, the audio subsystem may contain channels named audio.mp3.decode, audio.mp3.encode, and audio.spatial. There can be any number of channels within the process and channels can span multiple shared objects (e.g. .dll/.so).
Each message logged is associated with a single channel. Additionally, each message has an associated level (i.e. severity). The available levels are as follows:
- Fatal
Unrecoverable error. An error has ocurred and the program cannot continue.
- Error
Error message. An error has occurred but the program can continue.
- Warn
Warning message. Something could be wrong but not necessarily an error.
- Info
Informational message. Informational messages are usually triggered on state changes and typically not seen every frame.
- Verbose
Detailed diagnostics message. A channel may produce many verbose messages per-frame.
The levels above are sorted by severity. Fatal message are considered the most severe message while Verbose messages have the least severity.
To give a concrete example of channels and logging levels at work, a Python programmer would log an informational message as follows:
omni.log.info(f"loading {filename}", channel="audio.vorbis.decode")
Logging is a helpful debugging tool for not only users but also developers. However, logging every message in the system would not only slow down the application but also provide of mountain of mostly useless data. Because of this, the logging system supports per-channel filtering of messages. Each channel has the following properties to support this filtering:
- Level Threshold
Minimum level at which messages should be logged. For example, if a channel’s level is Warn, messages with lower severity (e.g. Info and Verbose) will be ignored (i.e. not logged).
- Enabled
If disabled, the channel will ignore all messages sent to it.
By storing these properties per-channel, the user is able to control which channels are able to write messages to the log. For example, the user can enable all messages for the audio.mp3 channels and only fatal messages for the audio.spatial channel as follows:
example_app.exe --/log/channels/audio.mp3.*=verbose --/log/channels/audio.spatial=fatal
In addition to the per-channel settings, the logging system itself has an enabled flag and a level threshold. By default, channels will respect the global enabled flag and global level threshold. However, per-channel, the user is able to override (i.e. ignore) the global settings. In the command-line example above, when we set the level thresholds, we implicitly told each channel to: be enabled, log only messages at the given severity or above, and override (i.e. ignore) the global enabled flag and level threshold.
Tip
The global log’s default enabled flag is true and its default level threshold is Warn. Channels, by default, are set to inherit (i.e. respect) the global log’s flags.
This means, that by default, an Omniverse application will log all channels’ messages that are of Warn, Error, or Fatal severity.
Above, we covered the basic concepts of the logging system. In the following sections we’ll cover:
How users can configure logging via the command-line and config files.
How programmers can write log messages, configure settings, add channels, etc.
How programmers can be notified of logging events such as a new message, new channels, etc.
Logging for Users
Logging can be configured by the user via the command-line and/or configuration files.
Command-Line
Log settings are specified on the command-line with the --/log/
key. For example, to disable the log:
example_app.exe --/log/enabled=false
Multiple log settings can be supplied on the command-line. For example, to enable all messages (i.e. enable Verbose) across all channels, but disable all audio channels:
example_app.exe --/log/enabled=true --/log/level=verbose --/log/channels/audio.*=disable
Supported command-line flags can be found in Settings Reference.
Config Files
Omniverse supports two configuration file formats, JSON and TOML. If an application’s name is example_app.exe, the application will attempt to read its configuration from example_app.config.json or example_app.config.toml.
An example JSON config file is as follows:
{
"log": {
"enabled" : true,
"channels": {
"test.unit.logtest" : "verbose",
"omni.core.*" : "error"
}
}
}
The equivalent in TOML:
[log]
enabled = true
[log.channels]
"test.unit.logtest" = "verbose"
"omni.core.*" = "error"
While each file type has a different format, their settings follow the same structure. Available log settings can be found in Settings Reference.
Settings Reference
The following settings are supported by the logging system:
- /log/level
The global logging level. Can be one of verbose, info, warn, error, or fatal. Defaults to warn.
Example:
/log/level = verbose
- /log/enabled
The global log enabled flag level. Can be true or false. Defaults to true.
Example:
/log/enabled = true
- /log/channels
Sets the level of all channels matching the given wildcard. The wildcard support * and ?. Values for can be one of verbose, info, warn, error, fatal, or disable. The setting supplied will override the global log settings.
More than one filter can be specified. Multiple filters are resolved in order, provided that they are set in the
carb::settings::ISettings
system in that order (this may be dependent on the type of serializer used).The log system subscribes to these
carb::settings::ISettings
keys and changes the log levels immediately when they are changed in thecarb::settings::ISettings
system.Example:
/log/channels/audio.* = info
- /log/async
If true, logging a message does not block the thread calling the logging function. This can increase application performance at the cost of log message appearing slightly out-of-order in the log. Defaults to false.
- /log/file
Filename to which to log. Defaults to the empty string (does not log to file).
Example:
/log/file = log.txt
- /log/fileFlushLevel
Severity at which a message should be immediately flushed to disk. Accepted values are verbose, info, warn, error, or fatal. The default value is verbose.
Can have a performance penalty.
Example:
/log/fileFlushLevel = error
- /log/flushStandardStreamOutput
If true, immediately flush each message to
stdout
. Enabling has a performance penalty. Default to false./log/enableStandardStreamOutput
must be enabled.Example:
/log/fileFlushStandardStreamOutput = true
- /log/enableStandardStreamOutput
Log each message to
stdout
. Defaults to true.Example:
/log/enableStandardStreamOutput = false
See also:
/log/flushStandardStreamOutput
,/log/outputStream
,/log/outputStreamLevel
.- /log/enableDebugConsoleOutput
Log each message to the attached (if any) debugger’s output console. Defaults to true.
Example:
/log/enableDebugConsoleOutput = false
See also:
/log/debugConsoleLevel
- /log/enableColorOutput
Taste the rainbow, in text form on the console. Messages will be colored based on severity (e.g. error message are red). Defaults to true.
Example:
/log/enableColorOutput = false
- /log/processGroupId
Sets the process group ID for the logger. If a non-zero identifier is given, inter-process locking will be enabled on both the log file and the stdout/stderr streams. This will prevent simultaneous messages from multiple processes in the logs from becoming interleaved within each other. If a zero identifier is given, inter-process locking will be disabled. Defaults to 0.
Example:
/log/processGroupId = 12009
- /log/includeChannel
Writes the channel name as a part of the message. Defaults to true.
Example:
/log/includeChannel = false
- /log/includeSource
Deprecated: Alias for
/log/includeChannel
.Example:
/log/includeSource = false
- /log/includeFilename
Writes the name of the file that logged the message. Defaults to false.
Example:
/log/includeFilename = true
- /log/includeLineNumber
Writes the line number within the file that logged the message. Defaults to false.
Example:
/log/includeLineNumber = true
- /log/includeFunctionName
Writes the name of the function that logged the message. Defaults to false.
Example:
/log/includeLineNumber = true
- /log/includeTimeStamp
Writes the time at which the message was logged. Defaults to false.
Example:
/log/includeTimeStamp = true
Can be enabled in addition to
/log/setElapsedTimeUnits
.- /log/includeThreadId
Writes the id of the thread that logged the message. Defaults to false.
Example:
/log/includeThreadId = true
- /log/setElapsedTimeUnits
Writes the elapsed time since the first message was logged. The time printed will use the system’s high-resolution clock. Defaults to none.
Valid values are as follows:
“” or “none”: The time index printing is disabled (default state).
“ms”, “milli”, or “milliseconds”: Print the time index in milliseconds.
“us”, “µs”, “micro”, or “microseconds”: Print the time index in microseconds.
“ns”, “nano”, or “nanoseconds”: Print the time index in nanoseconds.
Example:
/log/setElapsedTimeUnits = ms
Can be enabled in addition to
/log/includeTimeStamp
.- /log/includeProcessId
Writes the id of the process that logged the message. Defaults to false.
Example:
/log/includeProcessId = true
- /log/outputStream
Stream to which to log messages. Options are stdout and stderr. Defaults to stdout.
/log/enableStandardStreamOutput
must be enabled.Example:
/log/outputStream = stderr
- /log/outputStreamLevel
Severity level at which to log to the output stream. Accepted values are verbose, info, warn, error, or fatal. The default value is verbose.
/log/enableStandardStreamOutput
must be enabled.Example:
/log/outputStreamLevel = info
- /log/debugConsoleLevel
Severity level at which to log to the debugger’s output console. Accepted values are verbose, info, warn, error, or fatal. The default value is verbose.
/log/enableDebugConsoleOutput
must be enabled.Example:
/log/outputStreamLevel = info
- /log/fileLogLevel
Severity level at which to log to file. Accepted values are verbose, info, warn, error, or fatal. The default value is verbose.
/log/file
must be set.Example:
/log/fileLogLevel = info
- /log/detail
Enable most options: channel names, line number, function name, time stamp, thread id, and elapsed time. Default to false.
Example:
/log/detail = true
- /log/fullDetail
Enable all of the flags enabled by
/log/detail
along with the filename and process id.Example:
/log/fullDetail = true
- /log/fileAppend
Indicates whether opening the file should append to it. If false, file is overwritten. Default is false.
Example:
/log/fileAppend = true
Basic Logging for Programmers
The Omniverse logging system is designed to filter messages generated by native code at low cost. In fact, the runtime
cost of ignoring a message is a single comparison of an int32_t
. The cheap cost of filtering messages allows
developers to litter their codebase with log messages. Additionally, thoughtful use of logging channels allows both
users and developers to quickly get to the information they desire.
A message can be logged with one of the following macros:
#include <omni/log/ILog.h>
void logAtEachLevel()
{
OMNI_LOG_VERBOSE("This is verbose");
OMNI_LOG_INFO("Þetta eru upplýsingar");
OMNI_LOG_WARN("Սա նախազգուշացում է");
OMNI_LOG_ERROR("这是一个错误");
OMNI_LOG_FATAL("This is %s", "fatal");
}
By default, log messages are sent to a “default” channel. The name of this default channel can be specified per shared object or per translation unit. See Default Channel for further details.
Note
Older Omniverse code uses logging macros like CARB_LOG_ERROR(...)
. When linking to newer versions of the
logging system, these macros log to the default logging channel.
While logging to the default channel is easy, the developer will quickly want to log to custom channels. To do this, the developer must first create the channel:
OMNI_LOG_ADD_CHANNEL(kMp3EncodeChannel, "audio.vorbis.encode", "Vorbis audio encoding.")
OMNI_LOG_ADD_CHANNEL(kMp3DecodeChannel, "audio.vorbis.decode", "Vorbis audio decoding.")
OMNI_LOG_ADD_CHANNEL(kSpatialChannel, "audio.spatial", "Spatial (3D) audio.")
The OMNI_LOG_ADD_CHANNEL
macro not only defines necessary storage for the channels book-keeping, it also
registers the channel with the logging system. Because of this storage allocation and registration, it is important
this macro be called only once per-channel (else you’ll get compiler errors).
There’s subtlety to the statement above. OMNI_LOG_ADD_CHANNEL
should only be called once per-channel, but that’s
once per shared object. For example, if you have a DLL named audio-mp3.dll and another named audio-flac.dll,
both of which contain a channel named audio.settings, each DLL will contain a call to OMNI_LOG_ADD_CHANNEL
for the
channel.
OMNI_LOG_ADD_CHANNEL
should be called within a .cpp file and at global namespace scope.
To log to a channel, the developer simply passes its identifier as the first argument to the logging macros covered above:
OMNI_LOG_ERROR(kMp3DecodeChannel, "failed to load '%s'", filename);
// ...
OMNI_LOG_VERBOSE(kSpatialChannel, "sound source is %f meters from listener", distance);
If the developer wishes to use the channel identifier in a .cpp outside of the file in which OMNI_LOG_ADD_CHANNEL
was called, the following macro can be used to “forward-declare” the channel identifier:
OMNI_LOG_DECLARE_CHANNEL(kMp3EncodeChannel)
OMNI_LOG_DECLARE_CHANNEL
can be called multiple times within a shared object and even multiple times within a
translation unit.
In the following sections, we cover additional considerations for plugin authors, application authors, and Python developers.
Python Programmers
Logging is available to Python modules via the omni.log
module:
import omni.log
omni.log.verbose("This is verbose")
omni.log.info("Þetta eru upplýsingar")
omni.log.warn("Սա նախազգուշացում է")
omni.log.error("这是一个错误")
status = "bad"
omni.log.fatal(f"This is {status}")
By default, the module’s name is used as the channel name. However, the channel can be specified via the channel=
argument:
omni.log.info(f"loading {filename}", channel="audio.vorbis.decode")
The global log can be accessed via omni.log.get_log()
. For example:
log = omni.log.get_log()
# disable logging except for the "audio.vorbis.decode" channel
log.enabled = False
log.set_channel_enabled("audio.vorbis.decode", True, omni.log.SettingBehavior.OVERRIDE)
log.set_channel_level("audio.vorbis.decode", omni.log.Level.VERBOSE, omni.log.SettingBehavior.OVERRIDE)
# add a callback that can see non-ignored message
def on_log(channel, level, module, filename, func, line_no, msg, pid, tid, timestamp):
print(f"received a message on {channel} from {module} at {filename}:{line_no}: {msg}")
consumer = log.add_message_consumer(on_log)
omni.log.error("this message is ignored because the default channel is disabled")
omni.log.fatal("this message is ignored because the channel is disabled", channel="audio.spatial")
omni.log.verbose("this message will be seen", channel="audio.vorbis.decode")
log.remove_message_consumer(consumer)
Warning
Logging in Python is significantly slower than logging in native code. 🤷
Advanced Logging for Programmers
Default Channel
When using CARB_LOG_*
, g_carbClientName
is used as the name of the channel to which to log.
When using OMNI_LOG_*
, if no channel identifier is specified, the message is logged to the “default” channel. By
default, the identifier for the default channel is kDefaultChannel
. However, this can be overridden by defining the
OMNI_LOG_DEFAULT_CHANNEL
preprocessor symbol. One way to do this is via the compiler’s command-line:
g++ -DOMNI_LOG_DEFAULT_CHANNEL=kAnotherChannel MyPlugin.cpp -o MyPlugin.o
Another way is to simply define the macro before including omni/log/ILog.h:
#define OMNI_LOG_DEFAULT_CHANNEL kAnotherChannel
#include <omni/log/ILog.h>
OMNI_LOG_ADD_CHANNEL(kAnotherChannel, "example.log.another", "Another logging channel.")
void writeToAnotherChannel()
{
// kAnotherChannel (i.e. example.log.another) is the default channel
OMNI_LOG_INFO("writing message to 'example.log.another'");
}
The default channel can be redefined multiple times in a translation unit. For example:
OMNI_LOG_ERROR("i'm logged to the previous default channel");
#undef OMNI_LOG_DEFAULT_CHANNEL
#define OMNI_LOG_DEFAULT_CHANNEL kAlphaTestChannel
OMNI_LOG_ERROR("i'm logged to the alpha %s", "channel");
#undef OMNI_LOG_DEFAULT_CHANNEL
#define OMNI_LOG_DEFAULT_CHANNEL kBetaTestChannel
OMNI_LOG_ERROR("i'm logged to the beta channel");
Logging in Public Headers
The Omniverse SDK consist of many utility headers which contain nothing but inline code. Logging from within these headers can be advantageous. Header authors have two broad approaches to log from within public headers.
Log to the default channel. This is the simplest approach. Here all messages logged by the header will be logged to the default channel of the module that included the header.
Something to consider is that such an approach may pollute a module’s default channel. For example, given the example.audio.dll module, whose default channel is named example.audio, if a message from carb/extras/Path.h logs to the default channel, that message will be logged to the example.audio channel. If that header is included in another module, say example.filesystem.dll, the header’s messages would also appear in example.filesystem.dll’s default channel.
Based on what the header is doing, this behavior can be a benefit or an annoyance. If a header is found to be an annoyance, the default channel can be changed before including the header and restored after the include. See Default Channel for more details.
Log to header specific channels. Here public header authors create channels specific to their functionality. An example of such a header:
// Copyright (c) 2020-2021, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once
#include <omni/log/ILog.h>
// Forward declare the channel identifiers so they can be used in this header.
OMNI_LOG_DECLARE_CHANNEL(kExampleUsefulChannel);
OMNI_LOG_DECLARE_CHANNEL(kExampleUselessChannel);
namespace example
{
inline void myUsefulFunction()
{
OMNI_LOG_INFO(kExampleUsefulChannel, "i'm helping"); // logs to "example.useful"
}
inline void myUselessFunction()
{
OMNI_LOG_INFO(kExampleUselessChannel, "i'm not helping"); // logs to "example.useless"
}
} // namespace example
// This macro allows consumers of the header to allocate storage for this header's channels. It also adds the channels
// to the channel list of the module that included this header. This macro should be called once per module that
// includes this header.
#define EXAMPLE_ADD_CHANNELS() \
OMNI_LOG_ADD_CHANNEL(kExampleUsefulChannel, "example.useful", "Output from myUsefulFunction"); \
OMNI_LOG_ADD_CHANNEL(kExampleUselessChannel, "example.useless", "Output from myUselessFunction")
Above we see this approach requires three steps:
Forward declare your channels with
OMNI_LOG_DECLARE_CHANNEL
.Specific the new channels when logging.
Provide a utility macro consumers of your header can call to add your header’s channels.
The macro defined by the last step should be called once, and only once, in each module that consumes the header. Failure to call the macro will result in link errors. As such, it’s recommended the variable names used to identify the channels are descriptive in order to help guide the user to the missing macro call.
Message Consumers
The logging system allows for clients to register an interface that will be notified each time a message is to be written to the log (i.e. a message that has not been filtered out).
In C++:
auto log = omniGetLogWithoutAcquire();
bool sawFatalMessage = false;
auto consumer = log->addMessageConsumer(
[&sawFatalMessage](const char* channel, omni::log::Level level, const char* moduleName, const char* fileName,
const char* functionName, uint32_t lineNumber, const char* msg, uint32_t pid, uint32_t tid,
uint64_t timestamp) noexcept {
if (omni::log::Level::eFatal == level)
{
sawFatalMessage = true;
}
});
OMNI_LOG_FATAL("this is a fatal message");
log->removeMessageConsumer(consumer);
REQUIRE(sawFatalMessage);
Note, in the C++ ABI, the actual notification mechanism happens via the omni::log::ILogMessageConsumer
interface. The lambda
wrappers in the example above are wrappers around that interface.
omni::log::ILogMessageConsumer
can be used to create custom logging “back-ends”. In fact, the Carbonite framework creates such
a custom backend to handle parsing log settings and logging appropriately formatted messages to file/stdout/stderr/debug
console/etc. Thinking “big picture”, the logging system is a router: it takes messages from multiple input channels and
multiplexes them to multiple message consumers:
Tests can use omni::log::ILogMessageConsumer
to ensure messages are logged at appropriate times. As an example, consider the
MessageConsumer
class in source/tests/test.unit/omni.log/TestILog.cpp.
GUIs can create an omni::log::ILogMessageConsumer
to capture log messages and draw them to a graphical element.
Channel Update Consumers
Developers are able to register callbacks to be informed about state changes in the logging system. The following state changes are supported:
Channel added to the log
Channel removed from the log
Log’s enabled flag updated
Log’s level updated
A channel’s enabled flag was updated
A channel’s level was updated
A channel’s description was updated
The callback mechanism takes the form of the omni::log::ILogChannelUpdateConsumer
interface. See showMessageConsumer()
in
source/tests/test.unit/omni.log/TestILogDocs.cpp for example usage.
Controlling the Default Log Instance
Omniverse applications are initialized with the OMNI_CORE_INIT
macro. This macro calls omniCreateLog()
to instantiate
the log returned by omniGetLogWithoutAcquire()
. If the application wishes for a custom implementation of omni::log::ILog
to be
used, the user can simply pass the custom instance to the OMNI_CORE_INIT
macro.