Based on the details in this blog post from Nicholas Blumhardt (I know, it's a few years old, but I can't seem to find anything more current), I'd expect to be able to use the following (minimal) configuration and have scope automatically added to my log event, however, I'm not getting anything from the default console provider. Ideally, this would just be a key=value
on the log line, but a json blob {"key":"value"}
could work too.
ASP.NET Core version: 3.1
Serilog version: 2.10.0
Serilog.AspNetCore version: 3.4.0
Program.cs
public class Program
{
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting web host");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
WeatherForecastController.cs
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] _summaries = { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var scopeProps = new Dictionary<string, string> { ["dude"] = "123" };
using (_logger.BeginScope(scopeProps))
{
_logger.LogInformation("Hello world!");
}
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = _summaries[rng.Next(_summaries.Length)]
})
.ToArray();
}
}
Logs (note the "Hello World" line that doesn't have "dude" or "123" anywhere in it)
[15:47:09 INF] Starting web host
[15:47:11 INF] Now listening on: https://localhost:5001
[15:47:11 INF] Now listening on: http://localhost:5000
[15:47:11 INF] Application started. Press Ctrl+C to shut down.
[15:47:11 INF] Hosting environment: Development
[15:47:11 INF] Content root path: C:\Users\a806228\source\repos\WebApplication10\WebApplication10
[15:47:13 INF] Request starting HTTP/2 GET https://localhost:5001/weatherforecast
[15:47:13 INF] Executing endpoint 'WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)'
[15:47:13 INF] Route matched with {action = "Get", controller = "WeatherForecast"}. Executing controller action with signature System.Collections.Generic.IEnumerable`1[WebApplication10.WeatherForecast] Get() on controller WebApplication10.Controllers.WeatherForecastController (WebApplication10).
[15:47:13 INF] Hello world!
[15:47:13 INF] Executing ObjectResult, writing value of type 'WebApplication10.WeatherForecast[]'.
[15:47:13 INF] Executed action WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10) in 197.2872ms
[15:47:13 INF] Executed endpoint 'WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)'
[15:47:13 INF] Request finished in 533.989ms 200 application/json; charset=utf-8
I've also tried adding the RenderedCompactJsonFormatter
, but that doesn't produce what I'm expecting either (based on the aforementioned blog post).
RenderedCompactJsonFormatter logs (note that scope properties are nested within the "Scope" property, not at the top level of the json object).
{"@t":"2021-02-24T23:56:08.5102552Z","@m":"Content root path: \"C:\\Users\\a806228\\source\\repos\\WebApplication10\\WebApplication10\"","@i":"b5d60022","contentRoot":"C:\\Users\\a806228\\source\\repos\\WebApplication10\\WebApplication10","SourceContext":"Microsoft.Hosting.Lifetime"}
{"@t":"2021-02-24T23:56:10.2879619Z","@m":"Request starting HTTP/2 GET https://localhost:5001/weatherforecast ","@i":"ca22a1cb","Protocol":"HTTP/2","Method":"GET","ContentType":null,"ContentLength":null,"Scheme":"https","Host":"localhost:5001","PathBase":"","Path":"/weatherforecast","QueryString":"","HostingRequestStartingLog":"Request starting HTTP/2 GET https://localhost:5001/weatherforecast ","EventId":{"Id":1},"SourceContext":"Microsoft.AspNetCore.Hosting.Diagnostics","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.3924937Z","@m":"Executing endpoint '\"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)\"'","@i":"500cc934","EndpointName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","EventId":{"Name":"ExecutingEndpoint"},"SourceContext":"Microsoft.AspNetCore.Routing.EndpointMiddleware","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.5410912Z","@m":"Route matched with \"{action = \\\"Get\\\", controller = \\\"WeatherForecast\\\"}\". Executing controller action with signature \"System.Collections.Generic.IEnumerable`1[WebApplication10.WeatherForecast] Get()\" on controller \"WebApplication10.Controllers.WeatherForecastController\" (\"WebApplication10\").","@i":"122b2fdf","RouteData":"{action = \"Get\", controller = \"WeatherForecast\"}","MethodInfo":"System.Collections.Generic.IEnumerable`1[WebApplication10.WeatherForecast] Get()","Controller":"WebApplication10.Controllers.WeatherForecastController","AssemblyName":"WebApplication10","EventId":{"Id":3,"Name":"ControllerActionExecuting"},"SourceContext":"Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker","ActionId":"e2ddfe67-f10f-4720-b5f3-d7219eac02de","ActionName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.5548872Z","@m":"Hello world!","@i":"cc6ac8ad","SourceContext":"WebApplication10.Controllers.WeatherForecastController","ActionId":"e2ddfe67-f10f-4720-b5f3-d7219eac02de","ActionName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":"","Scope":[{"dude":"123"}]}
{"@t":"2021-02-24T23:56:10.6437907Z","@m":"Executing ObjectResult, writing value of type '\"WebApplication10.WeatherForecast[]\"'.","@i":"8a1b66c8","Type":"WebApplication10.WeatherForecast[]","EventId":{"Id":1,"Name":"ObjectResultExecuting"},"SourceContext":"Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor","ActionId":"e2ddfe67-f10f-4720-b5f3-d7219eac02de","ActionName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.7730179Z","@m":"Executed action \"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)\" in 181.1047ms","@i":"afa2e885","ActionName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","ElapsedMilliseconds":181.1047,"EventId":{"Id":2,"Name":"ActionExecuted"},"SourceContext":"Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker","ActionId":"e2ddfe67-f10f-4720-b5f3-d7219eac02de","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.8005134Z","@m":"Executed endpoint '\"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)\"'","@i":"99874f2b","EndpointName":"WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)","EventId":{"Id":1,"Name":"ExecutedEndpoint"},"SourceContext":"Microsoft.AspNetCore.Routing.EndpointMiddleware","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
{"@t":"2021-02-24T23:56:10.8202960Z","@m":"Request finished in 538.2013ms 200 application/json; charset=utf-8","@i":"791a596a","ElapsedMilliseconds":538.2013,"StatusCode":200,"ContentType":"application/json; charset=utf-8","HostingRequestFinishedLog":"Request finished in 538.2013ms 200 application/json; charset=utf-8","EventId":{"Id":2},"SourceContext":"Microsoft.AspNetCore.Hosting.Diagnostics","RequestId":"0HM6P3V0MULJ3:00000001","RequestPath":"/weatherforecast","SpanId":"|705afff2-40607f543d36f77d.","TraceId":"705afff2-40607f543d36f77d","ParentId":""}
Also, I know I can specifically use the LogContext.PushProperty
methods from within Serilog, but as a library maintainer, I'd really like to avoid that and have my callers to use Microsoft's ILogger interface (and scope) to interface with logging in ASP.NET Core because it gives me more flexibility in the long run.
Edit: I'm specifically concerned with Microsoft's ILogger interface, NOT ANYTHING RELATED to Serilog specifically. I don't actually care about "context" because I want to standardize around "scope" -- the .NET concept (not the Serilog concept). I don't care about all the other "properties" here because that's a "Serilog" concept, not a "Microsoft" concept.
Adding "Properties" to the "outputTemplate" is not what I'm asking for. Doing so via the following code:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog(
(context, configuration) =>
{
configuration.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} Properties={Properties:j}{NewLine}{Exception}");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Creates a log line like this (note that scope is still nested within properties as a specific property).
[17:39:39 INF] Hello world! Properties={"SourceContext": "WebApplication10.Controllers.WeatherForecastController", "ActionId": "09a79aaf-7b21-46d2-b142-70d17fa41213", "ActionName": "WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)", "RequestId": "0HM6P5OR3IC2K:00000001", "RequestPath": "/weatherforecast", "SpanId": "|1c7f1f94-446bb6d3ebf05f96.", "TraceId": "1c7f1f94-446bb6d3ebf05f96", "ParentId": "", "Scope": [{"dude": "123"}]}
What I want is for the logging scope to be rendered in a way that matches how context is rendered.
Here's the Serilog way to do it.
// Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog(
(context, configuration) =>
{
// add properties
configuration
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
// WeatherForecastController.cs
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// use Serilog's LogContext instead of _logger.BeginScope()
using (LogContext.PushProperty("dude", 123))
{
_logger.LogInformation("Hello world!");
}
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = _summaries[rng.Next(_summaries.Length)]
})
.ToArray();
}
It should produce the exact same log line as when I add to scope directly (however, scope is nested within properties instead of being a top level properties element like the context property I'm adding)
[17:51:35 INF] Hello world! {"SourceContext": "WebApplication10.Controllers.WeatherForecastController", "ActionId": "e0997db9-b301-495d-9cfc-7f2d2113f6f2", "ActionName": "WebApplication10.Controllers.WeatherForecastController.Get (WebApplication10)", "RequestId": "0HM6P5VGQ0U9J:00000001", "RequestPath": "/weatherforecast", "SpanId": "|1134d47e-49ed237c257f2857.", "TraceId": "1134d47e-49ed237c257f2857", "ParentId": "", "dude": 123}