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.
.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 v2.10.0 https://github.com/serilog/serilog.git
Serilog.AspNetCore v4.0.0 https://github.com/serilog/serilog-aspnetcore.git
Serilog.Extensions.Logging v3.0.1 https://github.com/serilog/serilog-extensions-logging.git
Serilog.Extensions.Hosting v4.1.1 https://github.com/serilog/serilog-extensions-hosting.git
Serilog + .NET Core
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
.
— ?
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
, SerilogLog.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();
}
}
:
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();
}
}
:
:
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 .
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.