How SharpEmu's Composite Logging System Routes Messages to Multiple Sinks

SharpEmu uses a CompositeLogSink class that implements ISharpEmuLogSink and forwards each log entry to an array of child sinks, swallowing individual exceptions to ensure fault-tolerant delivery across all configured destinations.

The par274/sharpemu repository implements a lightweight, extensible logging framework designed specifically for emulator debugging scenarios. Its composite logging system solves the common problem of routing identical log output to multiple destinations—such as the console and a file—without duplicating logging calls throughout the codebase. The architecture centers on a minimal interface contract and a fault-tolerant dispatcher that guarantees delivery even when individual sinks fail.

Core Components of the Logging Pipeline

The system relies on three primary abstractions: the sink interface, the composite dispatcher, and the immutable log entry structure.

The Sink Interface (ISharpEmuLogSink)

Every destination implements ISharpEmuLogSink, defining a single method that receives log entries by reference to avoid unnecessary struct copying:

public interface ISharpEmuLogSink
{
    void Write(in LogEntry entry);
}

Source: [src/SharpEmu.Logging/ISharpEmuLogSink.cs](https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Logging/ISharpEmuLogSink.cs)

The CompositeLogSink Dispatcher

The CompositeLogSink class acts as a fan-out router, maintaining a private readonly array of child sinks and iterating through them for every log entry. According to the source code in CompositeLogSink.cs, it implements both ISharpEmuLogSink and IDisposable to manage resource cleanup across its children.

How the CompositeLogSink Implements Fault Tolerance

The critical feature of SharpEmu's composite logging system is its exception isolation. When Write is called, the method wraps each child sink invocation in an empty catch block, ensuring that a failure in one sink (such as a locked file or closed stream) never terminates the loop or affects other sinks:

public sealed class CompositeLogSink : ISharpEmuLogSink, IDisposable
{
    private readonly ISharpEmuLogSink[] _sinks;
    
    public void Write(in LogEntry entry)
    {
        foreach (var sink in _sinks)
        {
            try { sink.Write(in entry); }
            catch { /* swallow so other sinks keep working */ }
        }
    }
    
    public void Dispose()
    {
        foreach (var sink in _sinks)
        {
            if (sink is IDisposable disposable)
                disposable.Dispose();
        }
    }
}

Source: [src/SharpEmu.Logging/CompositeLogSink.cs](https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Logging/CompositeLogSink.cs)

This design provides fail-safe logging where a network sink can crash without corrupting your file-based audit trail.

Configuring the Default Sink Chain

The static façade SharpEmuLog automatically constructs the sink chain at startup based on environment variables. The ResolveSinkFromEnvironment() method determines whether to create a single console sink or a composite setup:

private static ISharpEmuLogSink ResolveSinkFromEnvironment()
{
    var consoleSink = new ConsoleLogSink(
        useColors: ResolveColorEnabledFromEnvironment(),
        includeTimestamp: false);

    var logFilePath = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FILE");
    if (!string.IsNullOrWhiteSpace(logFilePath))
    {
        var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
        _fileCapturesAllLevels = true;
        return new CompositeLogSink(new MinimumLevelFilterSink(consoleSink), fileSink);
    }

    return consoleSink;
}

Source: [src/SharpEmu.Logging/SharpEmuLog.cs (lines 96-112)](https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Logging/SharpEmuLog.cs#L96-L112)

When the SHARPEMU_LOG_FILE environment variable is present, the system creates a composite sink containing both a filtered console sink and an unfiltered file sink. This allows the console to show only warnings and errors while the file captures every debug message.

Filtering and LogEntry Structure

MinimumLevelFilterSink Implementation

The MinimumLevelFilterSink is a decorator that wraps another sink and applies the global _minimumLevel threshold before forwarding. This enables different verbosity levels per destination within the composite:

private sealed class MinimumLevelFilterSink : ISharpEmuLogSink
{
    private readonly ISharpEmuLogSink _inner;
    
    public void Write(in LogEntry entry)
    {
        if (entry.Level >= _minimumLevel)
            _inner.Write(in entry);
    }
}

Source: [src/SharpEmu.Logging/SharpEmuLog.cs (lines 27-38)](https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Logging/SharpEmuLog.cs#L27-L38)

The Immutable LogEntry Record

All sinks receive the same LogEntry struct, passed by in to optimize performance:

public readonly record struct LogEntry(
    DateTimeOffset Timestamp,
    LogLevel Level,
    string Category,
    string Message,
    string SourceFileName,
    int SourceLine,
    string SourceMemberName,
    Exception? Exception = null);

Source: [src/SharpEmu.Logging/LogEntry.cs](https://github.com/par274/sharpemu/blob/main/src/SharpEmu.Logging/LogEntry.cs)

Because LogEntry is a readonly record struct, it provides value semantics with immutability guarantees while avoiding heap allocations when passed by reference.

Practical Implementation Example

To configure a custom composite logging system with both console and file outputs programmatically:

using SharpEmu.Logging;

// Create individual sinks
var console = new ConsoleLogSink(useColors: true, includeTimestamp: false);
var file = new FileLogSink("logs/emulator.log", append: true, includeTimestamp: true);

// Combine into composite sink
var composite = new CompositeLogSink(console, file);

// Configure the global logger
SharpEmuLog.Configure(minimumLevel: LogLevel.Debug, sink: composite);

// Obtain a category-specific logger
var log = SharpEmuLog.For("Emulator.Core");

// Each message routes to both destinations
log.Info("Emulator initialized");
log.Debug("Loading ROM segment");

This pattern allows you to extend the pipeline with additional sinks—such as network loggers or in-memory buffers—simply by implementing ISharpEmuLogSink and adding instances to the CompositeLogSink constructor.

Summary

  • SharpEmu uses CompositeLogSink to route single log entries to multiple destinations simultaneously.
  • The Write method implements fault tolerance by catching and swallowing exceptions from individual sinks, preventing cascade failures.
  • Environment-based configuration automatically creates composite sinks when SHARPEMU_LOG_FILE is set, combining ConsoleLogSink and FileLogSink.
  • Level filtering is applied per-sink via MinimumLevelFilterSink, allowing files to capture all levels while the console shows only important messages.
  • All sinks receive the same immutable LogEntry struct passed by in for performance efficiency.

Frequently Asked Questions

What happens if one sink throws an exception in SharpEmu's composite logging system?

The CompositeLogSink.Write method catches exceptions individually within its foreach loop and swallows them silently. This ensures that a failure in one sink—such as a FileLogSink encountering a locked file—does not prevent other sinks from receiving the log entry.

How does SharpEmu filter log levels differently for console vs file outputs?

When both sinks are active, the console sink is wrapped in a MinimumLevelFilterSink decorator that checks the global minimum level before forwarding, while the file sink is added directly to the composite without filtering. This architecture allows the file to capture all log levels while the console respects verbosity settings.

Can I add custom sinks to SharpEmu's logging system?

Yes. Any class implementing ISharpEmuLogSink can be instantiated and passed to the CompositeLogSink constructor alongside built-in sinks like ConsoleLogSink or FileLogSink. The system treats all sinks uniformly through the interface contract.

Is the LogEntry struct passed by reference or value?

The LogEntry struct is passed by reference using the in parameter modifier in the Write method signature (void Write(in LogEntry entry)). This prevents copying the struct while maintaining read-only semantics for thread safety.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →