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; }
}
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();
}
}
}