Appearance
kids.kapish.logging
unity
Structured logging for Unity with a sink-based architecture and colour-coded Unity console output.
Package Identity
| Field | Value |
|---|---|
| Name | kids.kapish.logging |
| Display name | Carrot.Logging |
| Version | 0.1.0 |
| Unity | 2022.3+ |
| License | MIT |
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-- theILogSinkimplementation that bridges toUnityEngine.DebugLogConfigurationExtensions.cs-- convenience extension methods onLogConfiguration
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:
| LogLevel | Unity method |
|---|---|
| Verbose, Debug, Information | Debug.Log |
| Warning | Debug.LogWarning |
| Error, Fatal | Debug.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
| Package | Purpose |
|---|---|
kids.kapish | Core 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 definitionUsage 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:
| Method | Level |
|---|---|
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");
}
}