Introduction

Networked VR applications require different types of logging, such as:

  1. Debug Logs

  2. Experiment Logs

  3. Network Traces

  4. Refers to logging expected and exceptional events that occur during a regular session. The purpose is post-hoc debugging of high-level application code.

  5. Refers to logging application-specific data, such as measurements or questionnaire responses for an experiment.

  6. Refers to captures of network traffic to investigate reproducible low-level netcode bugs.

(1) & (2) are handled by Ubiq’s Event Logging system. (3) has distinct performance implications, so is handled seperately.

Use Cases

The Event Logging System is for collecting low or medium frequency events from multiple peers. The Event Logging system can log both Ubiq and third-party events, which can then be extracted and analysed.

Events are discrete, but otherwise have very few restrictions. It is up to the user to ensure that event logging in their application doesn’t negatively affect performance.

Overview

Events are generated by EventLogger instances placed throughout the application. Events generated by these components are passed to a LogManager instance. The LogManager forwards all events in an application to a LogCollector, and the LogCollector writes them to disk (or database, or other endpoint). LogManager and LogCollector can be on the same machine or separated by the network.

overview

Event Logger

EventLogger instances are lightweight objects that the application uses to log events. Calls to an EventLogger are expected to be placed throughout the system persistently, rather than gated with pre-processor defines.

The most common types of event logger are the ContextEventLogger, which is designed to work with Components that have a NetworkContext, and the UserEventLogger, designed for logging measurements in experiments.

 
public class VoipPeerConnectionManager : MonoBehaviour, INetworkComponent
{
    private EventLogger logger;

    private void Start()
    {
        context = NetworkScene.Register(this);
        logger = new ContextEventLogger(context);
    }

    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
    {
        var msg = JsonUtility.FromJson<Message>(message.ToString());
        switch (msg.type)
        {
            case "RequestPeerConnection":
                logger.Log("CreatePeerConnectionForRequest", msg.objectid);
                break;
        }
    }
}
 

The snippet above demonstrates the creation and use of a ContextEventLogger. The VoipPeerConnectionManager declares the logger and initialises it with a ContextEventLogger once a context has been created. The Log method can then be called to log the receipt of a specific message.

EventLogger instances attach to a single LogManager. Event logger constructors find the closest LogManager automatically.

EventLogger methods can be safely called from outside the Unity main thread. They should not be called from outside CLR threads however.

EventLogger instances are designed to have zero overhead when logs are not actually written. The Log method has many overloads to avoid boxing, and serialisation only runs when logging is on. Logs are only written when there is a listening LogManager in the scene.

It is encouraged to make as many EventLogger instances as needed. Individual event loggers are simple, with few options. Use multiple EventLogger instances within a class to get fine-grained control over logging, for example different log levels.

User and Application Events

An EventLogger can tag events as Application or User events. The LogManager can forward either or both types of event. The LogCollector will have different endpoints for different tags. In the case of the default LogCollector, events will be written to different files.

The purpose is to make using the Event Logging System for experiments easier, by acknowledging that for such applications, generating log outputs are the primary purpose, and that such outputs are distinct from those used to debug and develop it.

ComponentEventLogger and ContextEventLogger have their type set to Application by default, whereas the UserEventLogger has its type set to User. The type for all EventLogger instances can be overridden at any time.

All Ubiq events are of the type Application. The distinction between Application and User is not between Ubiq code and user code, but the purpose of the log; it is expected user applications will output both types, depending on their purpose.

Log Manager

When a LogManager is placed in a scene, it will recieve events from nearby event loggers. By default the LogManager will cache events up to a limit (50 Mb worth), dropping older events as new ones are receieved. LogManager can also be set to store an infinite number of events, which may be desirable for experiments if the limit may be reached before a LogCollector is created. There are no bounds on how much memory it may consume in this mode however, up to and including triggering out of memory exceptions for the whole application.

When instructed, the LogManager will forward these events and any new ones to a LogCollector, the Component that actually writes the logs to an endpoint such as the disk, or database.

Each Peer must have at least one LogManager to emit log events; event loggers cannot communicate over the network themselves. Only one LogCollector is needed between all Peers, though multiple collectors may be present (in which case all will receive all events). At least one LogCollector must be present in the network for logs to be written; a LogManager can cache logs but it cannot write them anywhere.

Filtering of User and Application events is done at the LogManager. If a LogManager does not listen for a type of event, it is the equivalent of not having a LogManager in the scene for an EventLogger of that type.

Log Collector

LogCollector instances receive events from LogManager instances and write them to an appropriate location. LogCollector instances can also control LogManager instances remotely - for example, starting and stopping the transmission of events.

A single LogCollector receives all event types; filtering can only occur at the LogManager.

The current implementation of the LogCollector writes events with different types to different files in the Persistent Data folder of whatever platform it is running on.

Events are written as Json (Utf8 strings). The files themselves are Json compliant, with events being placed in a top-level Json Array.

Local Log Collection

LogCollector instances will automatically recieve events sent over the network, but will also receive events from LogManager instances on the same peer, even without a network connection.

LogCollector instances will find all LogManager instances at the local Peer and register them on start-up. If a LogManager is instantiated in the Root of the scene, it will find all LogManager instances in the scene. If it is instantiated in a branch of the scene, it will find all LogManager instances within that branch.

This allows logging events entirely on one machine, for example, data collection for an experiment, without using the network.

Analysis

A LogCollector outputs a stream of structured logs in standards compliant Json. These logs can be fed to a stack like the ELK, processed with third-party tools like Matlab or Excel, or processed programmatically on platforms such as Python.

See the Analysis section for examples of how to process the logs.

Edit this page on GitHub