Passion for Serilog + .NET Core: Global Logger

Serilog is probably the most popular logging library for .NET at the moment. This library was born even before the appearance of the .NET Core platform, in which the platform developers proposed their vision of the application logging subsystem. In 2017, Serilog creates a library for integration into the .NET Core logging subsystem.





In this series of articles, we will take a close look and analyze the problems of using Serilog in .NET Core and try to answer the question - how to solve them? 





, Serilog  Serilog .NET Core. .





2013 github.com Opi, 6 Serilog. .NET Framework 4.5. , .NET API . NLog log4net.





Log4net and NLog popularity statistics 2012-2014
log4net NLog 2012-2014 .

google trends.





.NET Core (27.06.2016) (, , ). .Net Core. 2017, github.com serilog-aspnetcore. .NET Standard 2.0, .. .NET Core 2.0.





.NET Core, Serilog .NET Core. Serilog , .NET Core, API , .





.NET Core 3.1. xUnit. Serilog:





Serilog + .NET Core

100% , Serilog, .





public static int Main(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()
        .CreateBootstrapLogger();

    Log.Information("Starting up!");

    try
    {
        CreateHostBuilder(args).Build().Run();

        Log.Information("Stopped cleanly");
        return 0;
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "An unhandled exception occured during bootstrapping");
        return 1;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .UseSerilog((context, services, configuration) => configuration
                .ReadFrom.Configuration(context.Configuration)
                .ReadFrom.Services(services)
                .Enrich.FromLogContext()
                .WriteTo.Console())
    .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
      
      



, Serilog :





  • Main



    :





    • . , ;









    • ;





    • ;





    • ;





    • ;





  • CreateHostBuilder



    :





    • Serilog, .





, , , . .. ( ) . 





, , , DI, .NET Core, Serilog , CreateHostBuilder



.





— ?

Program.cs :





The initial «bootstrap» logger is able to log errors during start-up. It's completely replaced by the logger configured in UseSerilog()



below, once configuration and dependency-injection have both been set up successfully.





.. , UseSerilog()



HostBuilder



. , .





Serilog - Logger   Log get set. — SilentLogger, :





public static class Log
{
    static ILogger _logger = SilentLogger.Instance;

    /// <summary>
    /// The globally-shared logger.
    /// </summary>
    /// <exception cref="ArgumentNullException">When <paramref name="value"/> is <code>null</code></exception>
    public static ILogger Logger
    {
        get => _logger;
        set => _logger = value ?? throw new ArgumentNullException(nameof(value));
    }
    ...
}
      
      



UseSerilog



.





UseSerilog — - . :





  • , . ( ), :





    • , , .. ReloadableLogger;





    • preserveStaticLogger



      ( ( ) ) == false



      ;





  • «» , . ;





  • , ( preserveStaticLogger



    ), .NET Core , . , null, , .





preserveStaticLogger==false,



. :





  • Serilog (.. null



    ). .NET Core , ;





  • Serilog — null



    . , , Microsoft.Extensions.Logging.ILogger



    ;





  • Serilog .NET Core. Serilog , .. null



    ;





  • Serilog  Serilog : null



    , Serilog Log.Logger



    . Serilog : .





« »

Serilog Serilog .NET Core :





  • .NET Core ;





  • Logger



    Serilog.Log



    .





Serilog .NET Core Serilog :





  • , .NET Core ( — preserveStaticLogger==false



    ) —





  • ( — ), .NET Core — (preserveStaticLogger==true



    )





, preserveStaticLogger



. — , , .





. preserveStaticLogger



false



. , , .





, . , , , . , , .





! .





, output - :





class TestLogger : Serilog.ILogger
{
    private readonly string _prefix;
    private readonly ITestOutputHelper _output;

    public TestLogger(string prefix, ITestOutputHelper output)
    {
    	_prefix = prefix;
    	_output = output;
    }
    public void Write(LogEvent logEvent)
    {
    	_output.WriteLine(_prefix + " " +  logEvent.MessageTemplate.Render(logEvent.Properties));
    }
}
      
      



, . , :





class ConcurrentLoggingTestRequestSender
{
    private readonly WebApplicationFactory<Startup> _webAppFactory;
    private readonly ITestOutputHelper _output;
    private readonly string _logPrefix;

    public ConcurrentLoggingTestRequestSender(WebApplicationFactory<Startup> webAppFactory, ITestOutputHelper output, string logPrefix)
    {
        _webAppFactory = webAppFactory;
        _output = output;
        _logPrefix = logPrefix;
    }

    public async Task<HttpResponseMessage> Send()
    {
        var client = _webAppFactory.WithWebHostBuilder(b => b.UseSerilog(
        	(context, config) => config
        		.WriteTo.Logger(new TestLogger(_logPrefix, _output))
        )).CreateClient();

        return await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ping"));
    }
}
      
      



Send Serilog .





, -.





1:





public class ConcurrentLoggingTest_1of2 : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly ConcurrentLoggingTestRequestSender _requestSender;
    private readonly ITestOutputHelper _output;

    public ConcurrentLoggingTest_1of2(WebApplicationFactory<Startup> waf, ITestOutputHelper output)
    {
        _output = output;
    	_requestSender = new ConcurrentLoggingTestRequestSender(waf, output, "==1==");
    }

    [Fact]
    public async Task Test()
    {
        _output.WriteLine("Test 1 of 2");
        _output.WriteLine("");
        var resp = await _requestSender.Send();

        Assert.True(resp.IsSuccessStatusCode);
    }
}
      
      



2:





public class ConcurrentLoggingTest_2of2 : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly ConcurrentLoggingTestRequestSender _requestSender;
    private readonly ITestOutputHelper _output;

    public ConcurrentLoggingTest_2of2(WebApplicationFactory<Startup> waf, ITestOutputHelper output)
    {
        _output = output;
    	_requestSender = new ConcurrentLoggingTestRequestSender(waf, output, ">>2<<");
    }

    [Fact]
    public async Task Test()
    {
        _output.WriteLine("Test 2 of 2");
        _output.WriteLine("");
        var resp = await _requestSender.Send();

        Assert.True(resp.IsSuccessStatusCode);
    }
}
      
      



Test 1 of 2

==1== Application started. Press Ctrl+C to shut down.
==1== Hosting environment: "Development"
==1== Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
==1== Request starting HTTP/1.1 GET http://localhost/ping  
==1== Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
==1== Executing ObjectResult, writing value of type '"System.String"'.
==1== Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 13.1068ms
==1== Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Request finished in 76.8507ms 200 text/plain; charset=utf-8


Test 2 of 2

>>2<< Application started. Press Ctrl+C to shut down.
>>2<< Hosting environment: "Development"
>>2<< Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
>>2<< Request starting HTTP/1.1 GET http://localhost/ping  
>>2<< Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
>>2<< Executing ObjectResult, writing value of type '"System.String"'.
>>2<< Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 15.2088ms
>>2<< Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Request finished in 78.8673ms 200 text/plain; charset=utf-8
      
      



№1
Test 1 of 2

Test 2 of 2

>>2<< Application started. Press Ctrl+C to shut down.
>>2<< Application started. Press Ctrl+C to shut down.
>>2<< Hosting environment: "Development"
>>2<< Hosting environment: "Development"
>>2<< Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
>>2<< Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
>>2<< Request starting HTTP/1.1 GET http://localhost/ping  
>>2<< Request starting HTTP/1.1 GET http://localhost/ping  
>>2<< Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
>>2<< Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
>>2<< Executing ObjectResult, writing value of type '"System.String"'.
>>2<< Executing ObjectResult, writing value of type '"System.String"'.
>>2<< Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 13.5891ms
>>2<< Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 13.5891ms
>>2<< Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
>>2<< Request finished in 78.0903ms 200 text/plain; charset=utf-8
>>2<< Request finished in 78.0958ms 200 text/plain; charset=utf-8
      
      



№2
Test 1 of 2

==1== Application started. Press Ctrl+C to shut down.
==1== Application started. Press Ctrl+C to shut down.
==1== Hosting environment: "Development"
==1== Hosting environment: "Development"
==1== Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
==1== Content root path: "C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke"
==1== Request starting HTTP/1.1 GET http://localhost/ping  
==1== Request starting HTTP/1.1 GET http://localhost/ping  
==1== Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Executing endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
==1== Route matched with "{action = \"Ping\", controller = \"Ping\"}". Executing controller action with signature "Microsoft.AspNetCore.Mvc.IActionResult Ping()" on controller "SerilogPoke.Controllers.PingController" ("SerilogPoke").
==1== Executing ObjectResult, writing value of type '"System.String"'.
==1== Executing ObjectResult, writing value of type '"System.String"'.
==1== Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 12.7648ms
==1== Executed action "SerilogPoke.Controllers.PingController.Ping (SerilogPoke)" in 12.7649ms
==1== Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Executed endpoint '"SerilogPoke.Controllers.PingController.Ping (SerilogPoke)"'
==1== Request finished in 78.428ms 200 text/plain; charset=utf-8
==1== Request finished in 78.4282ms 200 text/plain; charset=utf-8

Test 2 of 2

      
      



, , - , , Serilog.Log.Logger



. , .





Serilog .NET Core ( UseSerilog()



preserveStaticLogger = true



, . « » .





— ?

, , DI .NET Core, , CreateHostBuilder



. .





Serilog , DI .NET Core — Main :





  • !













!

. . . , — Console



Debug



. , . , , :





  • - , ;





  • Debug



    — , , IDE .





, :





  • , ( , , );





  • , :





[01:53:06 INF] Now listening on: http://localhost:5000
[01:53:06 INF] Application started. Press Ctrl+C to shut down.
[01:53:06 INF] Hosting environment: Development
[01:53:06 INF] Content root path: C:\Users\ozzye\Documents\prog\my\serilog-poke\src\SerilogPoke
      
      



:





-





- ?

, . .





, - .





public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        throw new Exception("Ololo!");

        services.AddControllers();
    }
}
      
      



:





Console output when catching unhandled exception

Main:





public static int Main(string[] args)
{
    Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()
        .CreateBootstrapLogger();

    Log.Information("Starting up!");

    try
    {
        CreateHostBuilder(args).Build().Run();

        Log.Information("Stopped cleanly");
        return 0;
    }
    //catch (Exception ex)
    //{
    //    Log.Fatal(ex, "An unhandled exception occured during bootstrapping");
    //    return 1;
    //}
    finally
    {
        Log.CloseAndFlush();
    }
}
      
      



:





Console output without catching unhandled error

:





Serilog, .





- ?

, - try-catch



Serilog,   initial



, - — configured



.





1: . .





//Arrange
var initialLogger = new TestLogger("initial: ", _output);
var configuredLogger = new TestLogger("configured: ", _output);
HttpClient client;

Log.Logger = initialLogger;

Log.Information("Starting up!");

try
{
    client = _waf.WithWebHostBuilder(
        builder => builder
        	.UseSerilog((context, config) => config
                    .WriteTo.Logger(configuredLogger))
        	.ConfigureServices(collection =>
                           {
                               throw new Exception("Ololo!");
                           })
    ).CreateClient();
}
catch (Exception e)
{
    Log.Fatal(e, "An unhandled exception occured during bootstrapping");
    throw;
}
finally
{
    Log.CloseAndFlush();
}

//Act
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ping"));

//Assert
Assert.True(resp.IsSuccessStatusCode);
      
      



:





initial:  Starting up!
configured:  An unhandled exception occured during bootstrapping
      
      



2: .NET Core. .





//Arrange
var initialLogger = new TestLogger("initial: ", _output);
var configuredLogger = new TestLogger("configured: ", _output);
HttpClient client;

Log.Logger = initialLogger;

Log.Information("Starting up!");

try
{
    client = _waf.WithWebHostBuilder(
        builder => builder
        	.UseSerilog((context, config) => 
                        	config.WriteTo.Logger(configuredLogger)
                       )
        	.ConfigureAppConfiguration((context, configurationBuilder) => 		
                            configurationBuilder.AddJsonFile("absent.json"))
                    	)
            .CreateClient();
}
catch (Exception e)
{
    Log.Fatal(e, "An unhandled exception occured during bootstrapping");

    throw;
}
finally
{
    Log.CloseAndFlush();
}

//Act
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/ping"));

//Assert
Assert.True(resp.IsSuccessStatusCode);
      
      



:





initial:  Starting up!
initial:  An unhandled exception occured during bootstrapping
      
      



:





, :  ( ) , . ( ) , .NET Core. .. , . .





. , finally



:





public static int Main(string[] args)
{
    ...
    try
    {
        CreateHostBuilder(args).Build().Run();

        Log.Information("Stopped cleanly");
        return 0;
    }
    catch (Exception ex)
    {
        ...
    }
    finally
    {
        Log.CloseAndFlush();
    }
}
      
      



:





.





Main

- — , . , , , , .





, - — , , . , , :





  • -









, , , . . , , . , , , , , .





, , Serilog - , - .NET Core Serilog.





— , Serilog, Logger



Serilog.Log



. , - .





Singleton . - .





:





  • - . Serilog .NET Core ( preserveStaticLogger



    UseSerilog()



    ) :









    • « »





  • , .NET Core Serilog





  • Serilog.Log.Logger



    DI .NET Core, :





    • « »;





    • « , »;





    • « .NET Framework»;





  • , - - ;





  • .. Logger



    , . , , ;





  • .NET Core. Boat Anchor .





Popularity statistics for log4net, NLog and Serilog 2013-2021
log4net, NLog Serilog 2013-2021 .

google trends.





Serilog . .NET Core, , .NET Framework.  , .NET, .NET 5, , .NET Core.  Serilog .





, github . , , :





  • Serilog ,  .NET Framework serilog-aspnetcore, . .. Serilog.Log. Serilog .NET Core ;





  • serilog-aspnetcore Serilog .NET Core , , . preserveStaticLogger = true



    , .. .





In this article, we figured out what place the global logger takes in logging via Serilog in .NET Core and what problems it can bring.





In the following articles, other features of Serilog integration into .NET Core will be discussed.








All Articles