Centralized logging with severity levels, category tags, and pluggable receivers so every diagnostic message in your game flows through one pipeline you can filter, capture to disk, or strip from release builds.
Summary
The Scylla logging system routes every diagnostic message through a single static facade (Log) with eight severity levels, category tags, and any number of pluggable receivers. The bootstrap registers two default receivers during framework awake: UnityConsoleLog (always) and FileLog (when file logging is enabled in the Logger Configuration). You can create and register additional ILogReceiver implementations for custom destinations: telemetry, an in-game console, a remote log aggregator, etc. Receivers fire in priority order; failures in one receiver do not affect the others. The two lowest severity levels (Trace and Debug) compile out of release builds via [Conditional("DEBUG")], so leaving them in code costs nothing in a release build at runtime.
Two filter axes control which messages reach which receivers. Log.FilterLevel is the global severity floor: messages below the level are dropped before any receiver runs. Log.IsEnabled is the master kill-switch: when false, every log call is a no-op regardless of level. Both are read-write at runtime, which makes them useful for debug overlays that flip verbosity per session. Receivers also filter on their own terms (UnityConsoleLog can hide certain categories, FileLog writes everything that passes the global filter), so runtime filtering happens twice: once at the facade and once per receiver.
Every log call produces a LogEntry struct that the facade hands to each receiver via LogData(entry). The struct carries the timestamp, level, category, formatted prefix and labels, the original data object (boxed when value-typed), and an optional file path plus line number (populated automatically for Trace and Debug via [CallerFilePath] / [CallerLineNumber]). Receivers can inspect any field of the entry; the default receivers format the entry into a single line and write it to their respective destination. The framework also maintains an in-memory history buffer (MaxHistorySize entries, default 1000) that you can read or consume for diagnostic windows or for shipping the recent log on a crash report.
Diagnostic output is the main way to see what a running game is actually doing, and Debug.Log only goes so far: its messages do not survive across builds, they mix in with Unity’s own output, and there is no file trail without extra scaffolding. Routing through Scylla’s Log facade gives every line a level and a category from the start, so filtering by category at runtime (only LogCategory.Game warnings during gameplay debugging) is instant. The file receiver writes asynchronously with rotation, so per-session logs survive a crash without costing frame time, and custom receivers extend the system without touching the framework. Because the two lowest levels compile out, hundreds of Log.Trace calls can stay in the code and cost nothing in release.
Use the logging utilities for:
- Replacing
Debug.Logeverywhere in your code, both for the categorization and for the file output. - Routing diagnostic messages by category at runtime (only show
LogCategory.AIwarnings during AI debugging). - Per-session log files that survive crashes and ship attached to bug reports.
- Custom receivers that fan messages out to telemetry, remote loggers, or in-game debug consoles.
- Reading recent log history programmatically (for example, display the last 50 lines in a debug overlay).
- Compile-out diagnostic output for release builds via
Log.Trace/Log.Debug.
Features
- Static
Logfacade. Eight per-level methods (Trace,Debug,System,Info,Notice,Warning,Error,Fatal) plusDelimiterfor phase separators. Drop-in replacement forDebug.Logcalls anywhere in the codebase. - Eight severity levels.
Trace,Debug,System,Info,Notice,Warning,Error,Fatalordered0..7. Plus sentinel valuesMinimum = -1andMaximum = 999for receivers that opt out of filtering. LogCategorytagging. 28 enum values cover the framework’s subsystems (AI,Camera,Console,Core,Editor,Event,Game,Graphics,Input, …). Filter by category at runtime to focus on one subsystem during a debug session.- Compile-out for
TraceandDebug. The two lowest levels are decorated with[Conditional("DEBUG")], so calls disappear entirely from release builds. The string interpolation and the call site itself are removed; leaving hundreds ofLog.Tracelines costs nothing in shipping. - Pluggable
ILogReceiverimplementations. Register any number of receivers viaLog.RegisterLogReceiver. The framework shipsUnityConsoleLogandFileLog; custom receivers cover telemetry, in-game consoles, remote loggers, and crash reporters. - Isolated receiver failures. A receiver that throws does not affect the others. The framework catches and continues, so a broken telemetry sink never silences the Unity console.
- Two filter axes. Global
Log.FilterLeveldrops messages below the severity floor before any receiver runs.Log.IsEnabledis the master kill-switch; per-receiver filtering layers on top. Both are read-write at runtime for debug overlays. UnityConsoleLogdefaults. Routes throughDebug.Log/LogWarning/LogErrorwith configurable visibility flags (ShowTimestamp,ShowCodeLocation,ShowPrefix,ShowLabel,ShowCategory). Set via the bootstrap’s logger config asset.FileLogwith rotation. Asynchronous file output under{Documents}/{ProductName}/{LogSubFolder}/. Configurable max file count, max file size, free-disk-space floor, batch size, and flush interval. Two formats: human-readablePlainTextorJsonLinesfor log aggregators.- In-memory history buffer.
MaxHistorySizeentries (default 1000) of recent log lines. Read or consume for diagnostic overlays, last-50-lines displays, or crash-report attachments. LogEntrystruct for receivers. Carries timestamp, level, category, prefix, label, category padding, formatted string, original object, optional file path and line number (auto-populated forTrace/Debugvia[CallerFilePath]/[CallerLineNumber]). Receivers inspect any field.- Driven by
ScyllaLoggerConfiguration. Bootstrap-bound config asset for the master enable, filter level, and the three settings sub-objects (FileLoggingSettings,ConsoleLoggingSettings,FileLogFormattingSettings). Override at runtime through the JSON config-file tier (see Configuration).
Concepts
A few terms appear repeatedly. Severity level picks one of eight tiers, from Trace (most verbose, compiles out in release) to Fatal (lowest verbosity floor, always shown). Category is a LogCategory enum value used for runtime filtering; the Log.*(message, category) overloads take it. Receiver is an implementation of ILogReceiver that decides what to do with each LogEntry; the framework dispatches to every registered receiver. Filter level is the global cutoff: messages below this severity are dropped before any receiver runs. Log entry is the LogEntry struct passed to each receiver, carrying every piece of metadata about the log message.
| Concept | Type | Notes |
|---|---|---|
Log | static class | The facade. Eight per-level methods (Trace, Debug, System, Info, Notice, Warning, Error, Fatal) plus Delimiter. Each takes an optional LogCategory. Trace and Debug have [Conditional("DEBUG")] and compile out of release builds. |
LogLevel | enum | Eight values (Trace, Debug, System, Info, Notice, Warning, Error, Fatal) plus sentinels Minimum = -1 and Maximum = 999 used by receivers that should never filter. |
LogCategory | enum | 37 values: AI, Analytics, Animation, Asset, Audio, Camera, Character, Command, Console, Core, Data, DB, Editor, Event, File, Game, Gameplay, Graphics, Input, Lifecycle, Localization, Logger, Module, Network, None, Persistence, Physics, Platform, Renderer, Scenes, Setup, Stats, TextMode, Time, UI, Util, View. |
LogEntry | readonly struct | One log line’s metadata. Fields: Prefix, PaddedLabel, PaddedCategory, DataString, OriginalData, Level, Category, FilePath, LineNumber, Timestamp. Passed by value to every receiver’s LogData. |
ILogReceiver | interface | Implement to add a custom log destination. Members: LogPrefix, LogLabelLevel, LogData(LogEntry), ClearLog(). Register via Log.RegisterLogReceiver. |
UnityConsoleLog | sealed class | Default receiver. Routes to Unity’s Editor console via Debug.Log / LogWarning / LogError. Optional ConsoleLoggingSettings controls timestamp, code-location, prefix/label/category visibility. |
FileLog | sealed class | Default receiver. Writes asynchronously to a rotating file under {Documents}/{ProductName}/{LogSubFolder}/. Supports plain text and JSON Lines formats. Disposable; flushes on dispose. |
LogFileFormat | enum | Two values: PlainText (one human-readable line per entry, default) and JsonLines (one JSON object per entry; ideal for piping to a log aggregator or for jq filtering). |
ScyllaLoggerConfiguration | ScyllaConfiguration subclass | Bootstrap-bound configuration asset holding the master enable, the filter level, file-logging settings, console-logging settings, and file-log formatting settings. Edit in the Inspector; override via JSON config file (see Configuration). |
FileLoggingSettings | sealed class | Rotation policy: max log files, max file size, min free disk space, log subfolder, batch size, flush interval. |
ConsoleLoggingSettings | sealed class | Visibility flags for the Unity console: ShowTimestamp, ShowCodeLocation, ShowPrefix, ShowLabel, ShowCategory. |
FileLogFormattingSettings | sealed class | Visibility flags plus format for the file log: Format (PlainText / JsonLines), ShowTimestamp, ShowCodeLocation, ShowPrefix, ShowLabel, ShowCategory. |
The eight severity levels are ordered numerically (Trace = 0 through Fatal = 7); the global FilterLevel drops any message whose level is below the cutoff. The two sentinel values (Minimum = -1, Maximum = 999) are useful for receivers that want to opt out of the label-padding system: a receiver returning LogLabelLevel = LogLevel.Minimum always renders the level label; one returning Maximum never renders it.
Recipes
These recipes cover every public method on Log, every receiver’s surface, the logger configuration, and the in-memory history buffer. Each one is self-contained, so jump to the one that matches what you need. If you are new to the API, start with “Use the basic Log API” – the concepts and categories it introduces appear in every other recipe.
Use the basic Log API
Problem. You want a structured replacement for Debug.Log that carries a severity level and a category tag on every line, so you can filter the output during a debug session without wading through every message in the console. You also want verbose trace calls that evaporate completely from release builds without any #if DEBUG scaffolding in your code.
Solution. Eight severity methods cover the conventional logging hierarchy. Each takes an optional LogCategory as the second argument (defaulting to LogCategory.None). Log.Delimiter writes a visual separator line, useful between logical phases. Trace and Debug are decorated with [Conditional("DEBUG")], so calls to them compile out of release builds – the call site itself disappears, and any string interpolation in the argument disappears with it (the compiler removes the entire invocation).
using Scylla.Core.Debug;
/* Per-level methods. The first argument is the message (object, formatted via
* ToString()); the second is the optional category for runtime filtering. */
Log.Trace ("Per-frame trace info", LogCategory.Gameplay); /* compiles out in release */
Log.Debug ("Detailed diagnostic info", LogCategory.Gameplay); /* compiles out in release */
Log.System ("Framework state change", LogCategory.Lifecycle); /* framework-internal log */
Log.Info ("Player spawned", LogCategory.Game);
Log.Notice ("Save file written", LogCategory.File);
Log.Warning("Low health: 5", LogCategory.Game);
Log.Error ("Save operation failed", LogCategory.File);
Log.Fatal ("Unrecoverable framework error", LogCategory.Lifecycle);
/* Without a category: defaults to LogCategory.None. The category column is
* blank in the formatted output. */
Log.Info("Generic message");
/* String interpolation works the same as any C# string. Note: for Trace and
* Debug the compiler removes the entire call including its arguments, so the
* interpolation is also erased in release builds. */
Log.Debug($"Loaded {assetCount} assets in {ms}ms", LogCategory.Core);
/* Delimiter writes a visual separator line. Useful between logical phases
* (e.g. between "module initialization complete" and "game systems starting"). */
Log.Delimiter(LogCategory.Game);
/* Trace and Debug include caller info automatically. The file path and line
* number are captured at compile time via [CallerFilePath] and
* [CallerLineNumber]; receivers can render or skip them. */
Log.Trace("Called from this line");
/* The receiver sees FilePath = "...UserCode.cs", LineNumber = 42 (or wherever). */
Control the filter level and global enable
Problem. You want to raise the noise floor at runtime during an investigation (you suddenly care about every Trace line), or lower it for a long bake job that would otherwise flood the console. You also want a master switch that silences every log call with one line of code, useful for performance profiling sessions where logging overhead would skew the numbers.
Solution. Log.FilterLevel (default LogLevel.Trace) is the global severity cutoff. Any message whose level is below the cutoff is dropped before any receiver runs. Log.IsEnabled (default true) is the master kill-switch; when false, every log call is a no-op regardless of level. Both are read-write at runtime.
/* Read or set at runtime. Both default values come from ScyllaLoggerConfiguration
* during bootstrap (see the Configuration recipe below); changing them at runtime
* overrides the configured value for the current session. */
Log.IsEnabled = true; /* master kill-switch */
Log.FilterLevel = LogLevel.Info; /* drop Trace and Debug; let Info+ through */
/* Useful pattern: a debug overlay slider that switches the filter level. */
private void OnVerbositySliderChanged(LogLevel newLevel)
{
Log.FilterLevel = newLevel;
}
/* The Minimum / Maximum sentinels are not meant for the filter cutoff;
* they are used by ILogReceiver implementations that want to opt every
* level (or no level) into special handling. */
LogLevel never = LogLevel.Minimum; /* numeric -1 */
LogLevel always = LogLevel.Maximum; /* numeric 999 */
Wire up the default receivers
Problem. You want the Unity console plus a log file on disk wired up without writing any plumbing. You also need to understand what the default receivers expose so you can tune their output format before the first build goes to QA.
Solution. The framework registers two receivers during ScyllaBootstrap.Awake (see Bootstrap Lifecycle): UnityConsoleLog (always) and FileLog (when EnableFileLogging is true on ScyllaLoggerConfiguration). You construct them directly only when registering additional receivers alongside the defaults; the framework handles the default pair automatically.
using Scylla.Core.Config;
using Scylla.Core.Debug;
/* UnityConsoleLog: writes through Unity's Debug.Log / LogWarning / LogError.
* Pass ConsoleLoggingSettings to control what appears in the formatted output. */
var consoleSettings = new ConsoleLoggingSettings
{
ShowTimestamp = false, /* Unity Editor console shows time-of-day already */
ShowCodeLocation = false, /* Unity Editor console links to the call site */
ShowPrefix = true, /* the [Scylla] prefix */
ShowLabel = true, /* the [INFO] / [WARNING] label */
ShowCategory = true /* the [Game] / [Core] category tag */
};
var consoleLog = new UnityConsoleLog(consoleSettings);
/* The default constructor uses the same defaults as above; pass it only
* when you need the ConsoleLoggingSettings parameter. */
var defaultConsole = new UnityConsoleLog();
/* FileLog: writes to {Documents}/{ProductName}/{LogSubFolder}/.
* The first arg is an optional product-name override (null uses
* Application.productName). The second arg toggles async I/O (default true). */
var fileSettings = new FileLoggingSettings
{
MaxLogFiles = 10, /* default; older files rotate out */
MaxLogFileSizeMB = 1.0f, /* default; rotate when the active file exceeds 1 MB */
MinFreeDiskSpaceMB = 100f, /* default; pause file logging when disk space drops below */
LogSubFolder = "Logs",
BatchSize = 50, /* default; flush threshold */
FlushIntervalMs = 10 /* default; periodic flush in milliseconds */
};
var fileFormatting = new FileLogFormattingSettings
{
Format = LogFileFormat.PlainText, /* or JsonLines for log aggregators */
ShowTimestamp = true,
ShowCodeLocation = true,
ShowPrefix = true,
ShowLabel = true,
ShowCategory = true
};
var fileLog = new FileLog(
productName: null, /* use Application.productName */
useAsync: true, /* background-thread writes */
fileLoggingSettings: fileSettings,
formattingSettings: fileFormatting
);
/* Register both. The framework already does this for the configured defaults;
* additional registrations stack on top. */
Log.RegisterLogReceiver(consoleLog, priority: 0);
Log.RegisterLogReceiver(fileLog, priority: 0);
Implement a custom receiver
Problem. You need a log destination beyond the console and the file: a remote telemetry endpoint, an in-game console overlay, a per-feature analytics sink, a crash reporter that ships the last 50 lines automatically. The framework’s two default receivers cannot be reconfigured to forward messages elsewhere.
Solution. Implement ILogReceiver to add any destination you need. The interface has four members: LogPrefix (the prefix string the framework shows alongside the receiver’s name), LogLabelLevel (the severity threshold above which the receiver wants the formatted label rendered; use LogLevel.Minimum for always-labeled, LogLevel.Maximum for never-labeled), LogData(LogEntry) (the per-message hook), and ClearLog() (called when Log.Clear() runs).
using Scylla.Core.Debug;
/* Custom receiver: forwards Warning+ messages to a telemetry service.
* Implement ILogReceiver and register with Log.RegisterLogReceiver. */
public sealed class TelemetryReceiver : ILogReceiver
{
/* The prefix shown in the framework's "receiver registered" diagnostic
* log line. Conventionally bracketed. */
public string LogPrefix => "[telemetry]";
/* The minimum level at which the receiver wants the framework to render
* the standard label ([INFO], [WARNING], etc.). LogLevel.Minimum means
* "always render the label"; LogLevel.Maximum means "never render". */
public LogLevel LogLabelLevel => LogLevel.Warning;
/* The per-message hook. Inspect any field on the entry; do whatever the
* receiver needs with the data. The framework calls this from the same
* thread that invoked Log.*; main-thread-only Unity calls are safe when
* the publisher is on the main thread. */
public void LogData(LogEntry entry)
{
if (entry.Level < LogLevel.Warning)
{
return; /* only ship Warning+ to the telemetry endpoint */
}
TelemetryClient.Send(
level: entry.Level.ToString(),
category: entry.Category.ToString(),
message: entry.DataString,
file: entry.FilePath,
line: entry.LineNumber,
time: entry.Timestamp
);
}
/* Called when Log.Clear() runs. Flush or reset any state the receiver
* holds; the framework does not enforce specific behavior here. */
public void ClearLog()
{
TelemetryClient.FlushPending();
}
}
/* Register the receiver. The priority controls dispatch order; lower numeric
* values fire earlier. Use a custom priority to put a receiver before or after
* the framework's default pair (UnityConsoleLog and FileLog). */
Log.RegisterLogReceiver(new TelemetryReceiver(), priority: 100);
Register and unregister receivers
Problem. You need to add or remove receivers at specific points in the application’s lifetime: a test suite that needs a clean slate, an editor tool that registers a diagnostic receiver during a run and removes it on teardown, or a debug session where you register a verbose file receiver only while the feature under investigation is active.
Solution. Log.RegisterLogReceiver(receiver, priority) adds a receiver to the dispatch list immediately. Log.UnregisterLogReceiver(receiver) removes a specific instance; Log.UnregisterAllReceivers() clears everything (the framework re-registers its defaults on the next bootstrap).
/* Register with default priority. */
var custom = new TelemetryReceiver();
Log.RegisterLogReceiver(custom);
/* Register with explicit priority. Lower numeric values fire earlier. A
* receiver with priority 50 fires before the framework defaults; one with
* priority 200 fires after. */
Log.RegisterLogReceiver(custom, priority: 100);
/* Unregister a specific instance. The receiver is removed from the dispatch
* list immediately; the next Log.* call no longer reaches it. */
Log.UnregisterLogReceiver(custom);
/* Clear every receiver. Useful in tests that need a clean logging slate. The
* framework's default UnityConsoleLog and FileLog are also removed. */
Log.UnregisterAllReceivers();
Read and manage the log buffer and history
Problem. You want to power a debug overlay that shows the last N log lines, or you need to ship the recent log automatically when your crash reporter fires. You also want early-startup messages to survive – sometimes the most important diagnostic line is the one that arrives before any receiver is wired up.
Solution. The framework maintains two in-memory collections: a buffer of entries that arrived before any receiver was registered (so early-startup logs are not lost), and a history of recent entries you can read for diagnostic windows or crash reports. The buffer flushes to receivers as soon as the first one registers; the history is configurable via MaxHistorySize and is opt-in for retention (IsHistoryEnabled).
/* Buffer management. The framework auto-buffers entries that arrive before
* any receiver is registered. When the first receiver registers, the buffer
* flushes automatically; subsequent entries flow directly. */
int bufferedCount = Log.GetBufferedLogCount();
Log.ClearLogBuffer(); /* discard any pre-receiver entries */
Log.MaxBufferSize = 200; /* cap on the early log buffer; default 100 */
/* History management. The history is a circular buffer of the last
* MaxHistorySize entries; useful for debug overlays and crash reports. */
Log.IsHistoryEnabled = true; /* opt in; default is false */
Log.MaxHistorySize = 500; /* cap; default 1000 */
LogEntry[] recent = Log.GetLogHistory(); /* returns a snapshot copy */
int historyCount = Log.GetLogHistoryCount();
/* ConsumeLogHistory returns the same array but also clears the history.
* Use this for "ship the recent log on crash" patterns where you do not want
* the same entries sent twice. */
LogEntry[] consumed = Log.ConsumeLogHistory();
Log.ClearLogHistory(); /* clear without consuming */
Log.EnableLogHistory(); /* alias for IsHistoryEnabled = true */
/* Construct a LogEntry manually for testing receivers (rare). The framework
* does this internally on every Log.* call. */
LogEntry test = Log.CreateLogEntry(
data: "Test message",
label: "[INFO]",
category: LogCategory.Game,
logLevel: LogLevel.Info,
includeLabel: true,
filePath: "Test.cs",
lineNumber: 42
);
/* Clear flushes the buffer + history + every receiver's ClearLog(). Use in
* tests and editor tooling that need a full reset. */
Log.Clear();
Inspect what receivers see in a LogEntry
Problem. You are implementing a custom receiver and need to know exactly what fields are available on the LogEntry struct, which ones are always populated, and which ones are only present for certain log levels. You want to build a receiver that exports structured output to a log aggregator or a per-feature analytics sink.
Solution. Every receiver’s LogData method receives a LogEntry readonly struct. It carries every piece of metadata the framework has about the message: level, category, timestamp, formatted prefix and label strings, the original data object, and the caller’s file path plus line number (only when available). Inspect whatever you need and render accordingly.
/* The LogEntry struct (readonly fields). Receivers inspect any field. */
public readonly struct LogEntry
{
public readonly string Prefix; /* "[Scylla]" */
public readonly string PaddedLabel; /* "[INFO] " (padded to alignment) */
public readonly string PaddedCategory; /* "[Game] " (padded) */
public readonly string DataString; /* the formatted message text */
public readonly object OriginalData; /* the raw data argument, before ToString() */
public readonly LogLevel Level;
public readonly LogCategory Category;
public readonly string FilePath; /* populated for Trace and Debug */
public readonly int LineNumber; /* populated for Trace and Debug */
public readonly string Timestamp; /* pre-formatted timestamp string */
}
/* A receiver that exports entries as structured JSON. Uses the typed fields
* on LogEntry rather than the pre-formatted DataString, so the aggregator
* gets machine-readable output it can index and query. */
public sealed class JsonExportReceiver : ILogReceiver
{
public string LogPrefix => "[json-export]";
public LogLevel LogLabelLevel => LogLevel.Minimum;
public void LogData(LogEntry entry)
{
/* Build a structured record. Use the typed fields rather than the
* pre-formatted DataString when you need machine-readable output. */
var record = new
{
timestamp = entry.Timestamp,
level = entry.Level.ToString(),
category = entry.Category.ToString(),
message = entry.DataString,
file = string.IsNullOrEmpty(entry.FilePath) ? null : entry.FilePath,
line = entry.LineNumber > 0 ? entry.LineNumber : (int?)null
};
/* Serialize through Scylla's own JSON writer for consistency with
* other framework output. */
var json = ScyllaSerialization.ToJSON(record, SerializationSettings.CompactBinary).Value;
AppendToFile(json);
}
public void ClearLog() { /* no per-clear work for this receiver */ }
private void AppendToFile(string json) { /* ... */ }
}
Tune file rotation, console visibility, and output format
Problem. You need to know which configuration knobs control the logging system – file rotation policy, disk-space safeguards, what shows in the Unity console, and whether the file output is plain text or JSON Lines for your log aggregator pipeline. Most projects set these once, but you want to know where the values come from and how to override them per-environment.
Solution. ScyllaLoggerConfiguration is the bootstrap-bound configuration asset. It exposes the master enable flag, the filter level, the file-logging enable, three nested settings objects (FileLoggingSettings, ConsoleLoggingSettings, FileLogFormattingSettings), and three buffer/history caps. Edit the values in the Inspector; override at runtime via the JSON config file (see Configuration).
using Scylla.Core.Config;
/* The configuration asset. Bound to ScyllaBootstrap.LoggerConfiguration.
* Inherited public properties expose the values: */
public class CustomLoggerConfig : ScyllaLoggerConfiguration
{
public bool EnableLogging { /* default true */ }
public LogLevel LogLevel { /* default Trace */ }
public bool EnableFileLogging { /* default true */ }
public FileLoggingSettings FileLogging { /* nested config */ }
public ConsoleLoggingSettings ConsoleLogging { /* nested config */ }
public FileLogFormattingSettings FileFormatting { /* nested config */ }
public int MaxEarlyLogBufferSize { /* default 100 */ }
public int MaxLogBufferSize { /* default 1000 */ }
public int MaxLogHistorySize { /* default 1000 */ }
}
/* FileLoggingSettings: rotation, async, and disk-space safeguards. */
var fileLogging = new FileLoggingSettings
{
MaxLogFiles = 10, /* default; retain the last 10 rotated files */
MaxLogFileSizeMB = 1.0f, /* default; rotate when the active file passes 1 MB */
MinFreeDiskSpaceMB = 100f, /* default; pause file logging when free space drops below */
LogSubFolder = "Logs",/* default; under {Documents}/{ProductName}/ */
BatchSize = 50, /* default; queue this many entries before flushing */
FlushIntervalMs = 10 /* default; periodic flush interval in milliseconds */
};
/* ConsoleLoggingSettings: visibility flags for UnityConsoleLog. */
var consoleLogging = new ConsoleLoggingSettings
{
ShowTimestamp = false, /* default; Unity Editor console has its own */
ShowCodeLocation = false, /* default; Unity Editor console shows the call site */
ShowPrefix = true, /* default; the [Scylla] tag */
ShowLabel = true, /* default; the [INFO] / [WARNING] label */
ShowCategory = true /* default; the [Game] / [Core] category */
};
/* FileLogFormattingSettings: visibility flags plus format for FileLog. */
var fileFormatting = new FileLogFormattingSettings
{
Format = LogFileFormat.PlainText, /* default; or JsonLines */
ShowTimestamp = true,
ShowCodeLocation = true,
ShowPrefix = true,
ShowLabel = true,
ShowCategory = true
};
The settings flow through the four-tier configuration system. The Inspector-bound values are the build-time default; JSON overrides at ~/Documents/{ProductName}/Config/Scylla Logger Configuration.json apply at runtime. Use the per-build gates on ScyllaCoreConfiguration (ConfigFilesEnabledInDebugBuild, ConfigFilesEnabledInProductionBuild) to decide whether JSON overrides are applied at all.
Pick the right log category
Problem. You are adding logging to a new gameplay system and want to use the right LogCategory value so runtime filtering by category is meaningful – the AI programmer can show only LogCategory.AI warnings without seeing every save-file message, and the network programmer can focus on LogCategory.Network without filtering manually.
Solution. The LogCategory enum covers most cross-cutting subsystems. Use LogCategory.None (the default for log calls without an explicit category) when no specific tag fits. Use the project-specific categories (Game, Gameplay) for your gameplay code; the framework-internal categories (Core, Lifecycle, Setup, Module) are reserved for framework-internal log output.
using Scylla.Core.Debug;
/* The values. Pick the one that matches the message's origin. */
LogCategory[] all = {
LogCategory.AI,
LogCategory.Analytics,
LogCategory.Animation,
LogCategory.Asset, /* asset / bundle / Addressables loading */
LogCategory.Audio,
LogCategory.Camera,
LogCategory.Character,
LogCategory.Command,
LogCategory.Console,
LogCategory.Core,
LogCategory.Data,
LogCategory.DB,
LogCategory.Editor,
LogCategory.Event,
LogCategory.File,
LogCategory.Game, /* your gameplay code */
LogCategory.Gameplay, /* alternative for gameplay-adjacent systems */
LogCategory.Graphics,
LogCategory.Input,
LogCategory.Lifecycle, /* framework startup / shutdown */
LogCategory.Localization,
LogCategory.Logger, /* the logging system itself */
LogCategory.Module,
LogCategory.Network,
LogCategory.None, /* default when no category fits */
LogCategory.Persistence,/* save / load systems */
LogCategory.Physics,
LogCategory.Platform, /* store / OS platform services */
LogCategory.Renderer,
LogCategory.Scenes, /* scene loading and framework persistence */
LogCategory.Setup, /* framework setup phase */
LogCategory.Stats,
LogCategory.TextMode,
LogCategory.Time,
LogCategory.UI,
LogCategory.Util,
LogCategory.View
};
/* Use cases. */
Log.Info("Player jumped", LogCategory.Game);
Log.Warning("Pathfinding slow", LogCategory.AI);
Log.Error("Save corrupted", LogCategory.File);
Log.Info("Shader compiled", LogCategory.Renderer);
API Sketch
The public surface for logging spans the static facade, the level / category / entry types, the receiver interface, the two default receivers, and the configuration assets.
namespace Scylla.Core.Debug
{
/* Static facade. The eight per-level methods come in two overloads each:
* with and without a LogCategory. Trace and Debug have [Conditional("DEBUG")]. */
public static class Log
{
/* Compile-time labels and prefix. */
public const string LOG_PREFIX;
public const string LABEL_EMPTY, LABEL_SYSTEM, LABEL_TRACE, LABEL_DEBUG,
LABEL_INFO, LABEL_NOTICE, LABEL_WARNING, LABEL_ERROR, LABEL_FATAL;
/* Master controls. */
public static bool IsEnabled;
public static LogLevel FilterLevel;
public static int MaxBufferSize { get; set; } /* default 100 */
public static int MaxHistorySize { get; set; } /* default 1000 */
public static bool IsHistoryEnabled { get; set; }
/* Per-level methods (each has a no-category and with-category overload). */
[Conditional("DEBUG")]
public static void Trace (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
[Conditional("DEBUG")]
public static void Debug (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void System (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void Info (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void Notice (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void Warning(object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void Error (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
public static void Fatal (object data, LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
/* Visual separator. */
public static void Delimiter(LogCategory category = LogCategory.None,
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0);
/* Receiver management. */
public static void RegisterLogReceiver (ILogReceiver receiver);
public static void RegisterLogReceiver (ILogReceiver receiver, int priority);
public static void UnregisterLogReceiver(ILogReceiver receiver);
public static void UnregisterAllReceivers();
/* Buffer and history. */
public static int GetBufferedLogCount();
public static void ClearLogBuffer();
public static LogEntry[] GetLogHistory();
public static LogEntry[] ConsumeLogHistory();
public static int GetLogHistoryCount();
public static void ClearLogHistory();
public static void EnableLogHistory();
/* Reset everything. */
public static void Clear();
/* Manual entry construction (rare). */
public static LogEntry CreateLogEntry(object data, string label, LogCategory category,
LogLevel logLevel, bool? includeLabel = null, string filePath = "", int lineNumber = 0);
}
/* Eight levels plus two sentinels. */
public enum LogLevel
{
Minimum = -1,
Trace = 0, Debug = 1, System = 2, Info = 3,
Notice = 4, Warning = 5, Error = 6, Fatal = 7,
Maximum = 999
}
/* 37 categories for runtime filtering. */
public enum LogCategory
{
AI, Analytics, Camera, Character, Command, Console, Core, Data, DB, Editor,
Event, File, Game, Gameplay, Graphics, Input, Lifecycle, Module, Network, None,
Renderer, Setup, Stats, TextMode, Time, UI, Util, View, Logger, Scenes,
Audio, Physics, Animation, Persistence, Localization, Asset, Platform
}
/* One log line's metadata. */
public readonly struct LogEntry
{
public readonly string Prefix;
public readonly string PaddedLabel;
public readonly string PaddedCategory;
public readonly string DataString;
public readonly object OriginalData;
public readonly LogLevel Level;
public readonly LogCategory Category;
public readonly string FilePath;
public readonly int LineNumber;
public readonly string Timestamp;
}
/* Receiver contract. */
public interface ILogReceiver
{
string LogPrefix { get; }
LogLevel LogLabelLevel { get; }
void LogData(LogEntry logEntry);
void ClearLog();
}
/* Default receivers. */
public sealed class UnityConsoleLog : ILogReceiver
{
public UnityConsoleLog();
public UnityConsoleLog(ConsoleLoggingSettings settings);
public bool ShowTimestamp { get; set; }
public bool ShowCodeLocation { get; set; }
public bool ShowPrefix { get; set; }
public bool ShowLabel { get; set; }
public bool ShowCategory { get; set; }
}
public sealed class FileLog : ILogReceiver, IDisposable
{
public FileLog(string productName = null, bool useAsync = true,
FileLoggingSettings fileLoggingSettings = null,
FileLogFormattingSettings formattingSettings = null);
public bool ShowTimestamp { get; set; }
public bool ShowCodeLocation { get; set; }
public bool ShowPrefix { get; set; }
public bool ShowLabel { get; set; }
public bool ShowCategory { get; set; }
public LogFileFormat Format { get; set; }
public async Task FlushAsync();
public bool Flush(int maxIterations = 1000);
public void Dispose();
}
public enum LogFileFormat { PlainText, JsonLines }
}
namespace Scylla.Core.Config
{
/* Configuration asset (bound to ScyllaBootstrap.LoggerConfiguration). */
public sealed class ScyllaLoggerConfiguration : ScyllaConfiguration
{
public bool EnableLogging { get; }
public LogLevel LogLevel { get; }
public bool EnableFileLogging { get; }
public FileLoggingSettings FileLogging { get; }
public ConsoleLoggingSettings ConsoleLogging { get; }
public FileLogFormattingSettings FileFormatting { get; }
public int MaxEarlyLogBufferSize { get; } /* default 100 */
public int MaxLogBufferSize { get; } /* default 1000 */
public int MaxLogHistorySize { get; } /* default 1000 */
}
/* File rotation policy. */
public sealed class FileLoggingSettings
{
public int MaxLogFiles = 10;
public float MaxLogFileSizeMB = 1f;
public float MinFreeDiskSpaceMB = 100f;
public string LogSubFolder = "Logs";
public int BatchSize = 50;
public int FlushIntervalMs = 10;
public long MaxFileSizeBytes { get; }
public long MinFreeDiskSpaceBytes { get; }
}
/* Console visibility flags. */
public sealed class ConsoleLoggingSettings
{
public bool ShowTimestamp;
public bool ShowCodeLocation;
public bool ShowPrefix = true;
public bool ShowLabel = true;
public bool ShowCategory = true;
}
/* File log visibility flags plus format. */
public sealed class FileLogFormattingSettings
{
public LogFileFormat Format = LogFileFormat.PlainText;
public bool ShowTimestamp = true;
public bool ShowCodeLocation = true;
public bool ShowPrefix = true;
public bool ShowLabel = true;
public bool ShowCategory = true;
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Settings reference
The configuration knobs that control the logging system. Edit them in the Inspector on the asset bound to ScyllaBootstrap.LoggerConfiguration, or override at runtime via the JSON config-file system (see Configuration).
ScyllaLoggerConfiguration top-level
| Property | Type | Default | Effect |
|---|---|---|---|
EnableLogging | bool | true | Master switch. When false, every Log.* call is a no-op regardless of level. |
LogLevel | LogLevel | Trace | Global filter cutoff. Messages below this severity are dropped before any receiver runs. |
EnableFileLogging | bool | true | When true, the bootstrap registers a FileLog receiver during Awake. When false, only UnityConsoleLog is registered. |
FileLogging | FileLoggingSettings | (sub-settings) | File rotation policy (see below). |
ConsoleLogging | ConsoleLoggingSettings | (sub-settings) | Console visibility flags (see below). |
FileFormatting | FileLogFormattingSettings | (sub-settings) | File log visibility flags plus format (see below). |
MaxEarlyLogBufferSize | int | 100 | Cap on the pre-receiver buffer that holds entries between framework Awake start and the first receiver registration. |
MaxLogBufferSize | int | 1000 | Cap on the runtime log buffer used internally for batching. |
MaxLogHistorySize | int | 1000 | Cap on the in-memory history buffer (when Log.IsHistoryEnabled = true). |
FileLoggingSettings
| Property | Type | Default | Effect |
|---|---|---|---|
MaxLogFiles | int | 10 | Number of rotated archives kept. Older files are deleted in FIFO order when the count is exceeded. |
MaxLogFileSizeMB | float | 1.0 | Active log file size cap. When exceeded, the file is renamed (with a numeric suffix) and a new active file is started. |
MinFreeDiskSpaceMB | float | 100.0 | Free-space floor on the target drive. File logging is paused when free space drops below this; it resumes when space frees up. |
LogSubFolder | string | "Logs" | Subfolder name under {Documents}/{ProductName}/ for log files. |
BatchSize | int | 50 | Number of entries the file writer batches before flushing. Larger batches reduce overhead; smaller batches lose less data on crash. |
FlushIntervalMs | int | 10 | Periodic flush interval in milliseconds. Forces a flush even when the batch is not full; trades off recency vs overhead. |
ConsoleLoggingSettings
| Property | Type | Default | Effect |
|---|---|---|---|
ShowTimestamp | bool | false | Render the timestamp prefix. Default off because the Unity Editor console shows time-of-day already. |
ShowCodeLocation | bool | false | Render the file path plus line number for Trace / Debug calls. Default off because the Unity Editor console links to the call site. |
ShowPrefix | bool | true | Render the [Scylla] framework prefix. |
ShowLabel | bool | true | Render the level label ([INFO], [WARNING], etc.). |
ShowCategory | bool | true | Render the category tag ([Game], [Core], etc.). |
FileLogFormattingSettings
| Property | Type | Default | Effect |
|---|---|---|---|
Format | LogFileFormat | PlainText | Output format. PlainText for human reading; JsonLines for piping into a log aggregator or for jq filtering. |
ShowTimestamp | bool | true | Render the timestamp prefix in plain-text mode (always present in JSON mode). |
ShowCodeLocation | bool | true | Render the file path plus line number in plain-text mode (always present in JSON mode). |
ShowPrefix | bool | true | Render the [Scylla] framework prefix in plain-text mode. |
ShowLabel | bool | true | Render the level label in plain-text mode. |
ShowCategory | bool | true | Render the category tag in plain-text mode. |
Best Practices
- Use specific
LogCategoryvalues at the call site. Adding categories retroactively is a chore; picking the right category up front makes runtime filtering instant. Reach forLogCategory.Gamefor gameplay code, the framework categories (Core,Lifecycle,Setup) for framework-internal output, and the specific module categories for module messages. - Pick severity deliberately.
Tracefor per-frame detail;Debugfor diagnostic information;Systemfor framework lifecycle;Infofor normal operational messages;Noticefor important but expected events;Warningfor recoverable issues;Errorfor recoverable failures;Fatalfor unrecoverable failures. - Use
Log.TraceandLog.Debugfreely. Both compile out in release via[Conditional("DEBUG")]. Leaving hundreds of trace calls in your code costs nothing at runtime in shipped builds. - Avoid string interpolation in
TraceandDebugcalls that fire every frame. The call site compiles out in release and the argument expression is erased with it, so nothing is allocated. Still, preferif (condition) Log.Debug(...)overLog.Debug(condition ? expensiveString : "")for clarity. - Keep
LogDataexception-free. A receiver that throws is caught by the framework and logged separately, but repeated exceptions may cause the framework to disable the receiver to avoid log flooding. - Hold receiver instances and dispose disposables.
FileLogimplementsIDisposable;Dispose()flushes the queue and closes the file handle. The framework’s ownFileLogis disposed on application quit; instances you construct yourself should be disposed when no longer needed. - Use
Log.IsHistoryEnabledfor diagnostic overlays. The history buffer is opt-in (defaultfalse). Enable during development to power a debug overlay that shows the recent log; disable in production to avoid the memory cost. - Tune
MaxLogFileSizeMBandMaxLogFilesfor your project’s needs. The defaults (1 MB per file, 10 files retained) suit typical projects. Heavy-logging projects (verbose AI traces, per-frame physics) may want larger rotation thresholds; shipped projects may want smaller per-file caps to keep individual archives portable. - Set the project’s default
FilterLevelin the configuration asset. Use the JSON override (~/Documents/{ProductName}/Config/Scylla Logger Configuration.json) to switch verbosity at runtime without rebuilding. QA testers can flip the level mid-session. - Use
LogFileFormat.JsonLinesfor log aggregation. Plain text is easier to read; JSON Lines is easier to parse. Pick the right format for the consumer. The format does not affect theLogDatapayload received by other custom receivers. - Catch the receiver-registration sequence. The bootstrap registers
UnityConsoleLogfirst, thenFileLog(when enabled), then any receivers you register in registration order. A receiver that depends on file output being available should register afterEnableFileLogginghas been confirmed. - Filter at the receiver, not at the call site. Receivers can decide which entries to handle (a telemetry receiver might only ship Warnings+). Putting the filter inside the receiver keeps your call-site code uniform and avoids the trap of forgetting to log a value because of a too-aggressive call-site guard.
Pitfalls
Related
- Configuration
- Bootstrap Lifecycle
- Getting Started
- API reference:
Scylla.Core.Debug.Log - API reference:
Scylla.Core.Debug.LogLevel - API reference:
Scylla.Core.Debug.LogCategory - API reference:
Scylla.Core.Debug.LogEntry - API reference:
Scylla.Core.Debug.ILogReceiver - API reference:
Scylla.Core.Debug.UnityConsoleLog - API reference:
Scylla.Core.Debug.FileLog - API reference:
Scylla.Core.Debug.LogFileFormat - API reference:
Scylla.Core.Config.ScyllaLoggerConfiguration - API reference:
Scylla.Core.Config.FileLoggingSettings - API reference:
Scylla.Core.Config.ConsoleLoggingSettings - API reference:
Scylla.Core.Config.FileLogFormattingSettings