If you have used .NET Core, you have probably battled with the new built-in .NET Core LoggerFactory which is in Microsoft.Extensions.Logging. It has created a lot of confusion around logging with ASP.NET Core. At Stackify, we have logging integrations for log4net, NLog, Serilog, and our direct API. We have a lot of experience with building logging appenders and libraries to work with various logging frameworks. As we have been working on perfecting our .NET Core support, I felt like I should share some of our tips and findings.
It is designed as a logging API that developers can use to capture built-in ASP.NET logging as well as for their own custom logging. The logging API supports multiple output providers and is extensible to potentially be able to send your application logging anywhere.
Other logging frameworks like NLog and Serilog have even written providers for it. So you can use the ILoggerFactory and it ends up working sort of like Common.Logging does as a facade above an actual logging library. By using it in this way, it also allows you to leverage all of the power of a library like NLog to overcome any limitations the built-in Microsoft.Extensions.Logging API may have. The biggest of those is being able to actually write your logs to a file on disk!
For an ASP.NET Core, you first see the ILoggerFactory within your Startup class that looks something like this below. This is where you can add providers to the ILoggerFactory for things like Debug & Console but also more advanced things like ETW or 3rd party providers.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); //log levels set in your configuration loggerFactory.AddDebug(); //does all log levels //removed rest of code }
If you are like me, you immediately want to know what calls Configure() and where the ILoggerFactory instance actually came from and live at.
When your app starts up you use the WebHostBuilder to essentially start up Kestrel and your web server.
public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() .Build(); host.Run(); }
This process under the covers ultimately calls some code in the WebHostBuilder class that creates a new LoggerFactory() and stores the reference to it in the ServicesCollection and the WebHostBuilder itself.
private IServiceCollection BuildHostingServices() { //removed code ServiceCollection services = new ServiceCollection(); services.AddSingleton(this._hostingEnvironment); if (this._loggerFactory == null) this._loggerFactory = (ILoggerFactory) new LoggerFactory(); using (List<Action>.Enumerator enumerator = this._configureLoggingDelegates.GetEnumerator()) { while (enumerator.MoveNext()) enumerator.Current(this._loggerFactory); } services.AddSingleton(this._loggerFactory); services.AddLogging(); //removed code }
In the example code below, I am showing off 3 different ways to access the LoggerFactory from your MVC controller. Dependency injection can give you the factory or a logger either one. Also, because it is stored in the services collection that later gets set on each HttpContext, you can potentially access it that way.
public class ValuesController : Controller { private ILoggerFactory _Factory; private ILogger _Logger; //set by dependency injection public ValuesController(ILoggerFactory factory, ILogger logger) { _Factory = factory; _Logger = logger; } [HttpGet] public IEnumerable Get() { var loggerFromDI = _Factory.CreateLogger("Values"); var loggerFactory = this.HttpContext.RequestServices.GetService(); var loggerFromServices = loggerFactory.CreateLogger("Values"); _Logger.LogDebug("From direct dependency injection"); loggerFromDI.LogDebug("From dependency injection factory"); loggerFromServices.LogDebug("From sevices"); } }
OK, so this is where the new logging API quickly becomes a nightmare. Dependency injection works great for accessing it in your MVC controller. But…how do you do logging in a class library that is consumed by this MVC project?
1. You could pass your existing LoggerFactory into every object/method you call (which is a terrible idea).
2. You could create your own LoggerFactory within your class library
This is an option as long as you don’t use any providers like a File provider that can’t have multiple instances writing at the same time. If you are using a lot of different class libraries you would have a lot of LoggerFactory objects running around.
3. Create a centrally located static class or project to hold and wrap around the main LoggerFactory reference
I see this as the best solution here unless you aren’t using any providers that have concurrency issues.
My suggestion is to create a little static helper class that becomes the owner of the LoggerFactory. The class can look something like this below. You can then use this ApplicationLogging class in any code that you want to use logging from and not have to worry about recreating LoggerFactory objects over and over. After all, logging needs to be fast!
public class ApplicationLogging { private static ILoggerFactory _Factory = null; public static void ConfigureLogger(ILoggerFactory factory) { factory.AddDebug(LogLevel.None).AddStackify(); factory.AddFile("logFileFromHelper.log"); //serilog file extension } public static ILoggerFactory LoggerFactory { get { if (_Factory == null) { _Factory = new LoggerFactory(); ConfigureLogger(_Factory); } return _Factory; } set { _Factory = value; } } public static ILogger CreateLogger() => LoggerFactory.CreateLogger(); }
There is one really key thing you need to know if you want to capture the built-in ASP.NET logging messages:
ASP.NET will only write its internal logging to the LoggerFactory object that it creates at app startup.
So if you new up (new LoggerFactory()) on your own anywhere else in your code, ASP.NET is not going to write its logging to it. This even includes my recommendation around this ApplicationLogging class.
So, what you need to do is, in Startup.Configure, grab that reference of the LoggerFactory and set it to your static logging class so it becomes your primary reference.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //call ConfigureLogger in a centralized place in the code ApplicationLogging.ConfigureLogger(loggerFactory); //set it as the primary LoggerFactory to use everywhere ApplicationLogging.LoggerFactory = loggerFactory; //other code removed }
If you follow this pattern you will only have one LoggerFactory object in your application, and you can access it anywhere via your ApplicationLogging static helper class. I sure wish the .NET team would make this built-in to the logging API somehow to simplify this.
Both NLog and Serilog both have a provider that you can use to extend the functionality of the built-in logging API. They essentially redirect all of the logs being written to the new logging API to their libraries. This gives you all the power of their libraries for the output of your logs while your code is not tied to any particular library. This is similar to the benefit of using Common.Logging.
Another option is to just use NLog or Serilog and don’t even worry about the new logging API. If you want to capture the built-in ASP.NET logging, you can plugin the NLog/Serilog provider and it will map those messages over. By doing it this way, you can use a different logging library directly and you don’t have to even think about LoggerFactory even existing.
My suggestion: Just keep doing logging how you always have.
After all the time I have spent messing with the new Microsoft.Extensions.Logging API, I would suggest using NLog and not even worrying about the new LoggerFactory. NLog is extremely robust and the maintainers of it continue to improve it. Serilog is also a good option. Your third option is log4net which is not looking good in its old age.
Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.
If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]