Pasión por Serilog + .NET Core: Global Logger

Serilog es probablemente la biblioteca de registro más popular para .NET en este momento. Esta biblioteca nació incluso antes de la aparición de la plataforma .NET Core, en la que los desarrolladores de la plataforma propusieron su visión del subsistema de registro de aplicaciones. En 2017, Serilog crea una biblioteca para la integración en el subsistema de registro de .NET Core.





En esta serie de artículos, veremos de cerca y analizaremos los problemas de usar Serilog en .NET Core e intentaremos responder la pregunta: ¿cómo resolverlos? 





, Serilog  Serilog .NET Core. .





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





Estadísticas de popularidad de Log4net y NLog 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();
    }
}
      
      



:





Salida de consola al detectar una excepción no controlada

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



:





Salida de consola sin detectar errores no controlados

:





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 .





Estadísticas de popularidad de log4net, NLog y 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



    , .. .





En este artículo, descubrimos qué lugar ocupa el registrador global en el registro a través de Serilog en .NET Core y qué problemas puede traer.





En los siguientes artículos, se discutirán otras características de la integración de Serilog en .NET Core.








All Articles