Skip to content

kids.kapish.logging

unity

Structured logging for Unity with a sink-based architecture and colour-coded Unity console output.

Package Identity

FieldValue
Namekids.kapish.logging
Display nameCarrot.Logging
Version0.1.0
Unity2022.3+
LicenseMIT

Assembly Structure

The package uses a two-assembly split:

Carrot.Logging.Precompiled

Precompiled DLL at Runtime/Plugins/Carrot/Carrot.Logging.dll. Contains the entire core logging API: ILog, ILogSink, ILogModule, ICanLog, Log, LogConfiguration, LogModule, LogLevel. Built from Carrot.NetStandard21.Logging (.NET Standard 2.1, no engine references). This means the core API can be used in pure .NET contexts outside Unity.

The .asmdef sets noEngineReferences: true and has no assembly references -- it's fully standalone.

Carrot.Logging

Unity source assembly at Runtime/. Contains two files:

  • UnityConsoleSink.cs -- the ILogSink implementation that bridges to UnityEngine.Debug
  • LogConfigurationExtensions.cs -- convenience extension methods on LogConfiguration

References Carrot.Precompiled (core Carrot DLL) and Carrot.Logging.Precompiled.

Architecture

Sink Pipeline

The logging system follows a simple sink pipeline:

Log.Configure()
  → LogConfiguration (fluent builder)
    → .SendTo(sink)          // register sinks
    → .MinimumLevel(level)   // set floor
    → .CreateLogger()        // build Log instance

Log instance
  → ILogModule per level (Verbose, Debug, Info, Warn, Error, Fatal)
    → each module has Enabled flag (set by minimum level)
    → Write() dispatches to all sinks via Emit()

Messages flow from ILogModule.Write() through every registered ILogSink.Emit(). There's no filtering, buffering, or async -- it's intentionally simple and synchronous.

UnityConsoleSink

Maps log levels to the appropriate Debug.* method:

LogLevelUnity method
Verbose, Debug, InformationDebug.Log
WarningDebug.LogWarning
Error, FatalDebug.LogError / Debug.LogException

When an exception is provided at Error/Fatal level, it calls Debug.LogException (which gives Unity's full stack trace UI). If both a message and exception are provided, the message is logged first, then the exception.

Level tags use Unity rich text colour tags (e.g. <color=#44FF44>[INF]</color>) for visual scanning in the console.

ICanLog Mixin

ICanLog is a mixin interface for MonoBehaviours or services that need logging. The extension methods (LogInfo, LogDebug, etc.) add optional module prefixing ([ModuleName] message) and indentation for hierarchical output.

Design Decisions

Precompiled core -- The logging interfaces live in a precompiled DLL so they can be referenced by other precompiled Carrot libraries without circular assembly dependencies.

No structured logging -- This is intentionally a simple string-based logger. It follows Serilog's level naming convention for future API compatibility, but doesn't implement message templates or property capture. The target use case is Unity runtime/editor logging, not production telemetry.

Synchronous dispatch -- All sink emission is synchronous on the calling thread. For Unity console output this is correct (Debug.Log is main-thread-only). Custom sinks that need async behaviour should handle it internally.

Level-as-module pattern -- Rather than log.Write(LogLevel.Info, msg), the API uses log.Info.Write(msg). This gives better discoverability in IDEs and allows per-level enable/disable without allocating filter objects.

Dependencies

PackagePurpose
kids.kapishCore Carrot Unity package (provides Carrot.Precompiled assembly)

File Structure

Runtime/
├── LogConfigurationExtensions.cs         # Unity extension methods for LogConfiguration
├── UnityConsoleSink.cs                   # ILogSink → Debug.Log/LogWarning/LogError
├── Carrot.Logging.asmdef                 # Unity source assembly
└── Plugins/Carrot/
    ├── Carrot.Logging.dll                # Precompiled core API
    └── Carrot.Logging.Precompiled.asmdef # Precompiled assembly definition

Usage Guide

Structured logging for Unity with sink-based output and level filtering.

Creating a Logger

Basic Unity console logger

csharp
using Carrot.Logging;

ILog log = Log.Configure()
              .SendToUnityConsole()
              .CreateLogger();

One-liner shortcut

csharp
using Carrot.Logging;

ILog log = Log.Configure().UnityConsole();

With minimum level

csharp
using Carrot.Logging;

ILog log = Log.Configure()
              .MinimumLevel(LogLevel.Warning)
              .SendToUnityConsole()
              .CreateLogger();

// Verbose, Debug, and Info modules are now disabled
log.Info.Write("Silenced");
log.Warn.Write("This gets through");

With timestamps

csharp
using Carrot.Logging;

ILog log = Log.Configure()
              .SendToUnityConsole(includeLevel: true, includeTimestamp: true)
              .CreateLogger();

// Output: [14:32:07] [INF] Hello
log.Info.Write("Hello");

Writing Messages

Each level has its own module with Write() overloads:

csharp
log.Verbose.Write("Tick");
log.Debug.Write("Player position updated");
log.Info.Write("Level loaded");
log.Warn.Write("Texture not found, using fallback");
log.Error.Write("Failed to save progress");
log.Fatal.Write("Unrecoverable state");

Logging exceptions

csharp
try
{
    LoadAsset(path);
}
catch (Exception ex)
{
    // Exception only (uses Debug.LogException for full stack trace UI)
    log.Error.Write(ex);

    // Message + exception (logs message first, then exception)
    log.Error.Write("Failed to load asset", ex);
}

Dynamic level selection

csharp
LogLevel level = isVerbose ? LogLevel.Verbose : LogLevel.Information;
log.GetModule(level).Write("Adaptive message");

Checking if a level is enabled

csharp
if (log.Debug.Enabled)
{
    // Avoid expensive string formatting when debug is disabled
    log.Debug.Write($"Entity {entity.Id} state: {entity.DumpState()}");
}

ICanLog Mixin

Implement ICanLog on any class to get extension-method logging with module prefixes and indentation.

csharp
using Carrot.Logging;

public class EnemyAI : MonoBehaviour, ICanLog
{
    public ILog Log { get; private set; }

    void Awake()
    {
        Log = Carrot.Logging.Log.Configure().UnityConsole();
    }

    void Start()
    {
        this.LogInfo("Spawned", module: "AI");
        // Output: [INF] [AI] Spawned

        this.LogDebug("Calculating path", module: "AI", indent: 1);
        // Output: [DBG] [AI] 	Calculating path
    }
}

Available extension methods:

MethodLevel
LogDebug()Debug
LogInfo()Information
LogWarning()Warning
LogError()Error
LogAt(level)Any level

All accept optional module (string prefix) and indent (tab count) parameters.

Multiple Sinks

Register multiple sinks to send log output to different destinations:

csharp
using Carrot.Logging;

ILog log = Log.Configure()
              .SendToUnityConsole()
              .SendTo(new FileLogSink("game.log"))
              .SendTo(new AnalyticsLogSink(analyticsClient))
              .CreateLogger();

// Every Write() call dispatches to all three sinks
log.Info.Write("This goes everywhere");

Custom Sinks

Implement ILogSink to create custom log destinations:

csharp
using Carrot.Logging;

public class FileLogSink : ILogSink
{
    private readonly StreamWriter writer;

    public FileLogSink(string path)
    {
        writer = new StreamWriter(path, append: true);
    }

    public void Emit(LogLevel level, string message, Exception? exception = null)
    {
        writer.WriteLine($"[{DateTime.Now:O}] [{level}] {message}");

        if (exception != null)
        {
            writer.WriteLine(exception.ToString());
        }

        writer.Flush();
    }
}

Using a custom sink

csharp
ILog log = Log.Configure()
              .SendTo(new FileLogSink("debug.log"))
              .SendToUnityConsole()
              .CreateLogger();

Typical Setup Pattern

A common pattern is to create the logger once and inject it via ICanLog:

csharp
using Carrot.Logging;

public static class AppLog
{
    public static ILog Instance { get; private set; }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Init()
    {
        Instance = Log.Configure()
                      .MinimumLevel(Debug.isDebugBuild ? LogLevel.Debug : LogLevel.Warning)
                      .SendToUnityConsole(includeTimestamp: true)
                      .CreateLogger();
    }
}

public class PlayerController : MonoBehaviour, ICanLog
{
    public ILog Log => AppLog.Instance;

    void Start()
    {
        this.LogInfo("Player ready", module: "Player");
    }
}

Carrot