Logging
KeenEyes provides a pluggable logging system through the KeenEyes.Logging library. The system supports multiple log providers, structured logging with properties, scoped contexts, and level-based filtering.
Basic Usage
Setting Up Logging
using KeenEyes.Logging;
using KeenEyes.Logging.Providers;
// Create a log manager
var logManager = new LogManager();
// Add providers
logManager.AddProvider(new ConsoleLogProvider());
logManager.AddProvider(new FileLogProvider("logs/app.log"));
// Set minimum log level
logManager.MinimumLevel = LogLevel.Debug;
Logging Messages
// Log at different levels
logManager.Trace("MySystem", "Entering method ProcessEntities");
logManager.Debug("MySystem", "Processing 100 entities");
logManager.Info("MySystem", "System initialized successfully");
logManager.Warning("MySystem", "Entity pool nearing capacity");
logManager.Error("MySystem", "Failed to load component data");
logManager.Fatal("MySystem", "Unrecoverable error - shutting down");
// Generic log method
logManager.Log(LogLevel.Info, "MySystem", "Custom message");
Log Levels
Log levels are ordered by severity from lowest to highest:
| Level | Code | Description |
|---|---|---|
| Trace | TRC |
Fine-grained diagnostic information |
| Debug | DBG |
Detailed information for troubleshooting |
| Info | INF |
General progress information |
| Warning | WRN |
Potential problems or unusual situations |
| Error | ERR |
Operation failures |
| Fatal | FTL |
Critical errors causing shutdown |
Messages below the configured minimum level are ignored.
Structured Logging
Include structured properties with log messages for better analysis:
// Using a dictionary for properties
logManager.Info("EntitySystem", "Entity spawned", new Dictionary<string, object?>
{
["EntityId"] = 42,
["Position"] = "(100, 200)",
["ComponentCount"] = 5
});
// Output: [12:34:56.789] INF [EntitySystem] Entity spawned {EntityId=42, Position=(100, 200), ComponentCount=5}
Log Scopes
Scopes add contextual properties to all log messages within a block:
using (logManager.BeginScope("EntityProcessing", new Dictionary<string, object?>
{
["BatchId"] = Guid.NewGuid(),
["StartTime"] = DateTime.UtcNow
}))
{
logManager.Debug("System", "Processing started");
// All messages here include BatchId and StartTime
foreach (var entity in entities)
{
using (logManager.BeginScope("Entity", new Dictionary<string, object?>
{
["EntityId"] = entity.Id
}))
{
logManager.Trace("System", "Processing entity");
// Messages here include BatchId, StartTime, and EntityId
}
}
logManager.Debug("System", "Processing complete");
}
Scopes can be nested, and child scope properties take precedence over parent scope properties for duplicate keys.
Built-in Providers
ConsoleLogProvider
Writes color-coded log messages to the console:
var console = new ConsoleLogProvider
{
MinimumLevel = LogLevel.Debug,
UseColors = true, // Enable color-coded output
TimestampFormat = "HH:mm:ss.fff", // Compact time format
IncludeProperties = true // Include structured properties
};
logManager.AddProvider(console);
Color coding:
- Trace: Gray
- Debug: Cyan
- Info: White
- Warning: Yellow
- Error: Red
- Fatal: Dark Red
FileLogProvider
Writes log messages to a file with optional rotation:
var file = new FileLogProvider("logs/app.log")
{
MinimumLevel = LogLevel.Info,
TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff",
MaxFileSizeBytes = 10 * 1024 * 1024, // 10 MB rotation
IncludeProperties = true
};
logManager.AddProvider(file);
Features:
- Automatic directory creation
- Size-based file rotation with timestamps
- Thread-safe file access
- Efficient buffered writes
DebugLogProvider
Writes to System.Diagnostics.Debug output (visible in IDE debugger):
var debug = new DebugLogProvider
{
MinimumLevel = LogLevel.Trace
};
logManager.AddProvider(debug);
NullLogProvider
A no-op provider useful for testing or disabling logging:
logManager.AddProvider(new NullLogProvider());
Creating Custom Providers
Implement ILogProvider to create custom log destinations:
public class CustomLogProvider : ILogProvider
{
public string Name => "Custom";
public LogLevel MinimumLevel { get; set; } = LogLevel.Info;
public void Log(
LogLevel level,
string category,
string message,
IReadOnlyDictionary<string, object?>? properties)
{
if (level < MinimumLevel) return;
// Write to custom destination (database, network, etc.)
SendToExternalService(level, category, message, properties);
}
public void Flush()
{
// Ensure all buffered messages are written
}
public void Dispose()
{
// Clean up resources
}
}
Requirements:
- Thread-safe
Logmethod (may be called from multiple threads) - Swallow exceptions internally (logging must not crash the application)
- Unique
Nameproperty for each provider instance
Provider Management
Adding Providers
logManager.AddProvider(new ConsoleLogProvider());
logManager.AddProvider(new FileLogProvider("logs/app.log"));
Each provider must have a unique name. Adding a provider with a duplicate name throws InvalidOperationException.
Removing Providers
bool removed = logManager.RemoveProvider("Console");
Getting Providers
var fileProvider = logManager.GetProvider("File") as FileLogProvider;
if (fileProvider != null)
{
fileProvider.MinimumLevel = LogLevel.Warning;
}
Checking Status
// Check if any providers are registered
if (logManager.IsEnabled)
{
logManager.Info("System", "Logging is active");
}
// Get provider count
int count = logManager.ProviderCount;
// Check if a specific level is enabled
if (logManager.IsLevelEnabled(LogLevel.Debug))
{
// Perform expensive string formatting only if debug is enabled
logManager.Debug("System", $"Complex data: {ExpensiveToString(data)}");
}
Performance Considerations
Early Exit Checks
Use level checks to avoid expensive operations when logging is disabled:
// Good - avoids string formatting if debug is disabled
if (logManager.IsLevelEnabled(LogLevel.Debug))
{
logManager.Debug("System", $"Entity {entity.Id} at position {position}");
}
// Also good - providers check internally, but formatting still happens
logManager.Debug("System", $"Entity {entity.Id} at position {position}");
Flushing
Call Flush() before shutdown to ensure buffered messages are written:
// Before application exit
logManager.Flush();
logManager.Dispose();
Thread Safety
LogManageris thread-safe- All built-in providers are thread-safe
- Scopes use
AsyncLocalfor proper async context flow
Integration Example
public class GameApplication : IDisposable
{
private readonly World world;
private readonly LogManager logManager;
public GameApplication()
{
// Set up logging
logManager = new LogManager();
logManager.AddProvider(new ConsoleLogProvider { MinimumLevel = LogLevel.Debug });
logManager.AddProvider(new FileLogProvider("logs/game.log")
{
MinimumLevel = LogLevel.Info,
MaxFileSizeBytes = 50 * 1024 * 1024
});
logManager.Info("Game", "Application starting");
// Create world
world = new World();
logManager.Info("Game", "World created");
}
public void Update(float deltaTime)
{
using (logManager.BeginScope("Frame", new Dictionary<string, object?>
{
["DeltaTime"] = deltaTime
}))
{
logManager.Trace("Game", "Frame update started");
world.Update(deltaTime);
logManager.Trace("Game", "Frame update completed");
}
}
public void Dispose()
{
logManager.Info("Game", "Application shutting down");
world.Dispose();
logManager.Flush();
logManager.Dispose();
}
}