Tracing & Logging
Docs / Core / Tracing & Logging
Tracing, counters, and logging strategy for ZenECS.
Overview
ZenECS provides a lightweight logging system that can be integrated with any logging framework. The core uses a simple logger interface that you can implement to bridge to your preferred logging system.
Key Concepts:
- Pluggable Logger: Bridge to any logging framework
- Severity Levels: Info, Warn, Error
- Error Reporting: Centralized error reporting with callbacks
- Minimal Dependencies: Core remains dependency-free
How It Works
Logger Interface
ZenECS uses IEcsLogger interface for all logging:
public interface IEcsLogger
{
void Info(string message);
void Warn(string message);
void Error(string message);
}
Default Logger
By default, ZenECS uses a no-op logger (NullLogger) that discards all messages. Replace it during bootstrap to enable logging.
Error Reporting
Centralized error reporting via EcsRuntimeOptions.Report():
EcsRuntimeOptions.Report(exception, "Context");
API Surface
Logger Configuration
EcsRuntimeOptions.Log
Global logger instance:
// Set custom logger
EcsRuntimeOptions.Log = new MyLogger();
// Or during kernel creation
var kernel = new Kernel(options, new MyLogger());
IEcsLogger
Logger interface:
public interface IEcsLogger
{
void Info(string message);
void Warn(string message);
void Error(string message);
}
Error Reporting
EcsRuntimeOptions.Report()
Report non-fatal exceptions:
public static void Report(Exception ex, string context = "")
EcsRuntimeOptions.OnUnhandledError
Global error handler callback:
public static Action<Exception>? OnUnhandledError { get; set; }
Examples
Unity Logger Integration
using UnityEngine;
public class UnityLogger : IEcsLogger
{
public void Info(string message)
{
Debug.Log($"[ZenECS] {message}");
}
public void Warn(string message)
{
Debug.LogWarning($"[ZenECS] {message}");
}
public void Error(string message)
{
Debug.LogError($"[ZenECS] {message}");
}
}
// Configure during bootstrap
EcsRuntimeOptions.Log = new UnityLogger();
Serilog Integration
using Serilog;
public class SerilogLogger : IEcsLogger
{
private readonly ILogger _logger;
public SerilogLogger(ILogger logger)
{
_logger = logger;
}
public void Info(string message)
{
_logger.Information("[ZenECS] {Message}", message);
}
public void Warn(string message)
{
_logger.Warning("[ZenECS] {Message}", message);
}
public void Error(string message)
{
_logger.Error("[ZenECS] {Message}", message);
}
}
// Configure
var serilogLogger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
EcsRuntimeOptions.Log = new SerilogLogger(serilogLogger);
NLog Integration
using NLog;
public class NLogLogger : IEcsLogger
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public void Info(string message)
{
Logger.Info($"[ZenECS] {message}");
}
public void Warn(string message)
{
Logger.Warn($"[ZenECS] {message}");
}
public void Error(string message)
{
Logger.Error($"[ZenECS] {message}");
}
}
// Configure
EcsRuntimeOptions.Log = new NLogLogger();
File Logger
public class FileLogger : IEcsLogger, IDisposable
{
private readonly StreamWriter _writer;
private readonly object _lock = new();
public FileLogger(string filePath)
{
_writer = new StreamWriter(filePath, append: true)
{
AutoFlush = true
};
}
public void Info(string message)
{
Write("INFO", message);
}
public void Warn(string message)
{
Write("WARN", message);
}
public void Error(string message)
{
Write("ERROR", message);
}
private void Write(string level, string message)
{
lock (_lock)
{
_writer.WriteLine($"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}");
}
}
public void Dispose()
{
_writer?.Dispose();
}
}
// Configure
EcsRuntimeOptions.Log = new FileLogger("zenecs.log");
Error Reporting Setup
// Configure error reporting
EcsRuntimeOptions.OnUnhandledError = (ex) =>
{
// Send to error tracking service
ErrorTrackingService.CaptureException(ex);
// Log to file
File.AppendAllText("errors.log", $"{DateTime.UtcNow}: {ex}\n");
// Show user-friendly message
if (ex is InvalidOperationException)
{
UI.ShowError("An operation failed. Please try again.");
}
};
// Report errors
try
{
ProcessEntity(entity);
}
catch (Exception ex)
{
EcsRuntimeOptions.Report(ex, "ProcessEntity");
}
Conditional Logging
public class ConditionalLogger : IEcsLogger
{
private readonly IEcsLogger _baseLogger;
private readonly bool _enableInfo;
private readonly bool _enableWarn;
private readonly bool _enableError;
public ConditionalLogger(
IEcsLogger baseLogger,
bool enableInfo = true,
bool enableWarn = true,
bool enableError = true)
{
_baseLogger = baseLogger;
_enableInfo = enableInfo;
_enableWarn = enableWarn;
_enableError = enableError;
}
public void Info(string message)
{
if (_enableInfo)
_baseLogger.Info(message);
}
public void Warn(string message)
{
if (_enableWarn)
_baseLogger.Warn(message);
}
public void Error(string message)
{
if (_enableError)
_baseLogger.Error(message);
}
}
// Configure with conditional logging
#if DEBUG
EcsRuntimeOptions.Log = new ConditionalLogger(
new UnityLogger(),
enableInfo: true,
enableWarn: true,
enableError: true
);
#else
EcsRuntimeOptions.Log = new ConditionalLogger(
new UnityLogger(),
enableInfo: false,
enableWarn: true,
enableError: true
);
#endif
Structured Logging
public class StructuredLogger : IEcsLogger
{
private readonly ILogger _logger; // Your structured logger
public void Info(string message)
{
_logger.LogInformation("ZenECS: {Message}", message);
}
public void Warn(string message)
{
_logger.LogWarning("ZenECS: {Message}", message);
}
public void Error(string message)
{
_logger.LogError("ZenECS: {Message}", message);
}
}
System-Specific Logging
[FixedGroup]
public sealed class MySystem : ISystem
{
public void Run(IWorld w, float dt)
{
try
{
// System logic
foreach (var (entity, health) in w.Query<Health>())
{
// Process health
}
}
catch (Exception ex)
{
// Report with context
EcsRuntimeOptions.Report(ex, $"{nameof(MySystem)}.Run");
}
}
}
Best Practices
1. Configure Logger Early
Set logger during application bootstrap:
// ✅ Good: Configure before creating kernel
EcsRuntimeOptions.Log = new UnityLogger();
var kernel = new Kernel();
2. Use Context in Error Reports
Provide context when reporting errors:
// ✅ Good: With context
EcsRuntimeOptions.Report(ex, "MySystem.ProcessEntity");
// ❌ Bad: No context
EcsRuntimeOptions.Report(ex);
3. Handle Logger Failures
Loggers should be safe and not throw:
public class SafeLogger : IEcsLogger
{
private readonly IEcsLogger _baseLogger;
public void Info(string message)
{
try
{
_baseLogger.Info(message);
}
catch
{
// Swallow logger failures to avoid cascading errors
}
}
// Same for Warn and Error
}
4. Filter Sensitive Information
Don't log sensitive data:
public class SanitizedLogger : IEcsLogger
{
private readonly IEcsLogger _baseLogger;
public void Error(string message)
{
var sanitized = SanitizeMessage(message);
_baseLogger.Error(sanitized);
}
private string SanitizeMessage(string message)
{
// Remove sensitive patterns
return System.Text.RegularExpressions.Regex.Replace(
message,
@"(password|token|secret|key)\s*[:=]\s*\S+",
"$1: [REDACTED]",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
}
}
5. Use Appropriate Log Levels
// ✅ Good: Appropriate levels
EcsRuntimeOptions.Log.Info("System initialized");
EcsRuntimeOptions.Log.Warn("Entity count approaching limit");
EcsRuntimeOptions.Log.Error("Failed to load snapshot");
// ❌ Bad: Wrong levels
EcsRuntimeOptions.Log.Error("System initialized"); // Should be Info
EcsRuntimeOptions.Log.Info("Failed to load snapshot"); // Should be Error
Logging in Systems
System Logging Pattern
[FixedGroup]
public sealed class MySystem : ISystem
{
public void Run(IWorld w, float dt)
{
// Log system start (optional, for debugging)
// EcsRuntimeOptions.Log.Info($"[{nameof(MySystem)}] Running");
try
{
// System logic
ProcessEntities(w);
}
catch (Exception ex)
{
// Report errors with context
EcsRuntimeOptions.Report(ex, nameof(MySystem));
}
}
private void ProcessEntities(IWorld world)
{
// System implementation
}
}
Performance-Critical Logging
For performance-critical paths, use conditional compilation:
#if ZENECS_DEBUG_LOGGING
EcsRuntimeOptions.Log.Info($"Processing {count} entities");
#endif
Error Reporting Patterns
Pattern 1: Try-Catch with Reporting
public void ProcessEntity(IWorld world, Entity entity)
{
try
{
// Process entity
var health = world.ReadComponent<Health>(entity);
// ...
}
catch (Exception ex)
{
EcsRuntimeOptions.Report(ex, $"ProcessEntity(entity={entity.Id})");
}
}
Pattern 2: Global Error Handler
// Setup once during bootstrap
EcsRuntimeOptions.OnUnhandledError = (ex) =>
{
// Send to analytics
Analytics.LogException(ex);
// Log to file
LogToFile(ex);
// Notify user if critical
if (IsCritical(ex))
{
UI.ShowError("A critical error occurred. Please restart.");
}
};
Pattern 3: Contextual Error Reporting
public class EntityProcessor
{
public void Process(IWorld world, Entity entity)
{
try
{
ProcessInternal(world, entity);
}
catch (Exception ex)
{
// Include entity context
var context = $"EntityProcessor.Process(entity={entity.Id}, world={world.Name})";
EcsRuntimeOptions.Report(ex, context);
}
}
}
Debugging Tips
1. Enable Verbose Logging
#if DEBUG
EcsRuntimeOptions.Log = new VerboseLogger();
#endif
2. Log System Execution
[FixedGroup]
public sealed class DebugSystem : ISystem
{
public void Run(IWorld w, float dt)
{
#if DEBUG
EcsRuntimeOptions.Log.Info($"[DebugSystem] Frame {w.FrameCount}, Tick {w.Tick}");
#endif
}
}
3. Track Entity Operations
public class TrackingLogger : IEcsLogger
{
private readonly IEcsLogger _baseLogger;
private int _entityCreates = 0;
private int _entityDestroys = 0;
public void Info(string message)
{
if (message.Contains("Entity created"))
_entityCreates++;
else if (message.Contains("Entity destroyed"))
_entityDestroys++;
_baseLogger.Info(message);
}
public void LogStats()
{
_baseLogger.Info($"Stats: Creates={_entityCreates}, Destroys={_entityDestroys}");
}
}
FAQ
How do I disable logging?
Use the default NullLogger (already default):
// No configuration needed - NullLogger is default
// Or explicitly:
EcsRuntimeOptions.Log = new NullLogger();
Can I use multiple loggers?
Yes, create a composite logger:
public class CompositeLogger : IEcsLogger
{
private readonly IEcsLogger[] _loggers;
public CompositeLogger(params IEcsLogger[] loggers)
{
_loggers = loggers;
}
public void Info(string message)
{
foreach (var logger in _loggers)
logger.Info(message);
}
// Same for Warn and Error
}
// Use
EcsRuntimeOptions.Log = new CompositeLogger(
new UnityLogger(),
new FileLogger("zenecs.log")
);
How do I log to a file?
See the FileLogger example above, or use your preferred logging framework's file sink.
Can I filter log messages?
Yes, create a filtering logger:
public class FilteringLogger : IEcsLogger
{
private readonly IEcsLogger _baseLogger;
private readonly Func<string, bool> _filter;
public FilteringLogger(IEcsLogger baseLogger, Func<string, bool> filter)
{
_baseLogger = baseLogger;
_filter = filter;
}
public void Info(string message)
{
if (_filter(message))
_baseLogger.Info(message);
}
// Same for Warn and Error
}
// Filter out verbose messages
EcsRuntimeOptions.Log = new FilteringLogger(
new UnityLogger(),
msg => !msg.Contains("[VERBOSE]")
);
See Also
- Error Handling - Error handling strategy
- Security - Logging security considerations
- Performance - Performance impact of logging