Introduction
Networked VR applications require different types of logging, such as:
Debug Logs
Experiment Logs
Network Traces
Refers to logging expected and exceptional events that occur during a regular session. The purpose is post-hoc debugging of high-level application code.
Refers to logging application-specific data, such as measurements or questionnaire responses for an experiment.
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.
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.