Search code examples
dependency-injectionserilogmasstransit

Serilog Sink with MassTransit - IBus service resolution causes an endless loop


I am trying to setup a sink for Serilog that publishes log messages through MassTransit. I run into an issue when I try to create the sink on startup. The sink creation causes an endless loop.

How should I register the dependency injection to resolve this circle?

I think it is because MassTransit.IBus uses ILogger. So when the logger is created, it asks for my sink with IBus, IBus asks for a logger, which asks for IBus, etc.

the startup code in Program.cs

//Code that sets up my mass transit. IBus is registered as part of it.
builder.Host.SetupMassTransit(); 

builder.Host.UseSerilog((ctx, services, lc) =>
{
    lc.WriteTo.Sink(new MassTransitSink(services.GetService<IBus>())); //Set a breakpoint here
});

Set a breakpoint on the lc.WriteTo.Sink(... and you'll see it is called repeatedly.

MassTransitSink

public class MassTransitSink : ILogEventSink
{
    private readonly IBus bus;

    public MassTransitSink(IBus bus)
    {
        this.bus = bus;
    }

    public async void Emit(LogEvent logEvent)
    {
        await bus.Publish<MyLog>(new MyLog("Hello World"));
    }
}

public class MyLog
{
    public MyLog(string message)
    {
        Message = message;
    }

    public string Message { get; set; } 
}

Solution

  • The issue is the cyclical reference between the ILogger and the IBus. Because our ILogger is resolved through serilog with a sink to MassTransit, it needs an IBus, which needs an ILogger, repeat. To fix it, register an ILoggerFactory that can give MassTransit an ILogger that is not dependent on an IBus.

    //Under the hood, this registers an ILoggerFactory
    builder.UseSerilog((context, services, lc) =>
    {
        lc.WriteTo.Sink(new MassTransitSink(services.GetService<IBus>()));
    });
    
    builder.ConfigureServices((context, services) =>
    {
        //Register an ILoggerFactory to replace the one serilog registers when calling builder.UseSerilog
        //This ILoggerFactory must provider MassTransit with an ILogger that is not dependent on an IBus.
    
        services.AddSingleton<ILoggerFactory, WorkAroundLoggerFactory>();
    });
    

    Why it works:
    Under the hood, serilog registers a SerilogLoggerFactory, which implements ILoggerFactory. At runtime, ILoggerFactory is resolved and used to create any ILogger. So we can register a replacement ILoggerFactory that looks for ILoggers requested for MassTransit and provides one that is not dependent on an IBus, avoiding the cyclical reference.

    Here is an example of my replacement ILoggerFactory, WorkAroundLoggerFactory. Because I still want the standard configurations from Serilog for my ILoggers, WorkAroundLoggerFactory uses the SerilogLoggerFactory. For the DI container to find SerilogLoggerFactory, I have to register the type SerilogLoggerFactory before calling builder.UseSerilog(...)

    the startup code in Program.cs

    //Code that sets up my mass transit. IBus is registered as part of it.
    builder.Host.SetupMassTransit(); 
    
    builder.ConfigureServices((context, services) =>
    {
        //If you want to use the SerilogLoggerFactory in your work around factory, you need to tell the DI container to watch for it, before it is registered as part of UseSerilog
        services.AddSingleton<SerilogLoggerFactory>();
    });
    
    builder.UseSerilog((context, services, lc) =>
    {
        lc.WriteTo.Sink(new MassTransitSink(services.GetService<IBus>()));
    });
    
    builder.ConfigureServices((context, services) =>
    {
        //Register the replacement ILoggerFactory
        services.AddSingleton<ILoggerFactory, WorkAroundLoggerFactory>();
    });
    

    WorkAroundLogger.cs

    // A logger factory that uses the SerilogLoggerFactory for anything but MassTransit logs.
    public class WorkAroundLoggerFactory : ILoggerFactory
    {
        private readonly IServiceProvider serviceProvider;
        private SerilogLoggerFactory? serilogLoggerFactory;
    
        public WorkAroundLoggerFactory(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }
        
        void ILoggerFactory.AddProvider(ILoggerProvider provider)
        {
            serilogLoggerFactory!.AddProvider(provider);
        }
    
        ILogger ILoggerFactory.CreateLogger(string categoryName)
        {
            if (categoryName.StartsWith("MassTransit"))
            {
                //Return an ILogger that does not use MassTransit for logging any MassTransit logs.
                //If you wanted to log to windows event viewer, you could use EventLogLoggerProvider().CreateLogger(categoryName);
                return new MyLogger(); 
            }
            else
            {
                if (serilogLoggerFactory is null)
                {
                    //We can't resolve the SerilogILoggerFactory at construction because it will cause a loop, because it will think it needs an IBus, which needs an ILogger, which needs an IBus, ... etc
                    serilogLoggerFactory = serviceProvider.GetService(typeof(SerilogLoggerFactory)) as SerilogLoggerFactory;
                    if (serilogLoggerFactory is null)
                    {
                        throw new NullReferenceException(nameof(SerilogLoggerFactory));
                    }
                }
                return serilogLoggerFactory.CreateLogger(categoryName);
            }
        }
    
        void IDisposable.Dispose()
        {
            if (serilogLoggerFactory != null)
            {
                serilogLoggerFactory.Dispose();
            }
        }
    }