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 the carb::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.

Plugin Authors

When creating an Omniverse plugin, it is necessary to call:

OMNI_MODULE_GLOBALS("example.log.plugin", "Example plugin showing how to use logging.")

This macro not only sets storage needed by the plugin, it also allocates and registers a default logging channel. The name of this channel is the name of the first argument to the macro.

For Carbonite plugins, the following necessary macro performs similar actions:

const struct carb::PluginImplDesc kPluginImpl = { "carb.windowing-glfw.plugin", "Windowing (glfw).", "NVIDIA",
                                                  carb::PluginHotReload::eDisabled, "dev" };
CARB_PLUGIN_IMPL_EX(kPluginImpl, carb::windowing::IWindowing, carb::windowing::IGLContext)
CARB_PLUGIN_IMPL_DEPS(carb::input::IInput)

In short, by default, plugin authors have access to a default logging channel. The name of default channel will be the same name passed to carb::PluginImplDesc (e.g. carb.windowing-glfw.plugin).

As described above, additional channels can be added with OMNI_LOG_ADD_CHANNEL.

Both CARB_LOG_* and OMNI_LOG_* macros can be called with a plugin. However, if you wish to log to a non-default channel, one of the OMNI_LOG_* macros must be used.

Application Authors

When creating an Omniverse application, it is necessary to call:

OMNI_APP_GLOBALS("example.windowing.native.app", "Native (C++) example app using example.windowing.");

This macro not only sets storage needed by the application, it also allocates and registers a default logging channel. The name of this channel is the name of the first argument to the macro.

For Carbonite applications, the following necessary macros performs similar actions:

CARB_GLOBALS("example.dictionary")

In short, by default, application authors have access to a default logging channel for use with their executable.

As described above, additional channels can be added with OMNI_LOG_ADD_CHANNEL.

Both CARB_LOG_* and OMNI_LOG_* macros can be called with an application. However, if you wish to log to a non-default channel, one of the OMNI_LOG_* macros must be used.

It is recommended that Carbonite applications should transition to using OMNI_APP_GLOBALS or CARB_GLOBALS_EX rather than CARB_GLOBALS since the others allow application authors to set a description for their default channel.

CARB_GLOBALS_EX("test.cleanshutdown", "Tests that a Carbonite app ends cleanly even without shutdown of the framework.")

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:

  1. Forward declare your channels with OMNI_LOG_DECLARE_CHANNEL.

  2. Specific the new channels when logging.

  3. 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:

graph LR log{ILog} --> std[StandardLogger] log --> l0[ILogMessageConsumer] log --> l1[ILogMessageConsumer] c0(audio.mp3.encode) -.-> log c1(audio.mp3.decode) -.-> log c2(audio.spatial) -.-> log c3(omni.core) -.-> log classDef bold font-weight:bold,stroke-width:4px; class log bold

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.