From what I understand, Serilogs UseSerilogRequestLogging
utilizes IDiagnosticContext
and adds the properties that has ben set to it to the last log entry of the request.
Works fine, but how can I utilize IDiagnosticContext
myself to set variables in things like a background task that doesn't have a http context to collect a bucket of properties and add to a certain log entry myself?
I guess something in the lines of using their AmbientDiagnosticContextCollector
with Begin
and TryComplete
?
Psuedo code of what I'm after:
diagnosticContext.Begin();
diagnosticContext.Set("foo", 1);
diagnosticContext.Set("bar", 2);
diagnosticContext.Complete();
logger.Log("Completed in {Seconds}", seconds, diagnosticContext.Properties);
// Entry should contain properties { seconds: <elapsed seconds>, foo: 1, bar: 2 }
Yes, I want a bucket of additional properties added to a specific entry - not write shared properties to all entries within the same context.
But this is what contexts/scopes are for. Just wrap the logging line into scope in using
. For example using the build-in ILogger.BeginScope
:
var log = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddSerilog(log));
var sp = services.BuildServiceProvider();
var logger = sp.GetRequiredService<ILogger<Program>>();
var scopeInfoDict = new Dictionary<string, object>
{
{"foo", 1}
};
int seconds = 100; // do some work
scopeInfoDict.Add("bar", 42); // enrich
// ...
using (logger.BeginScope(scopeInfoDict)) // use the "context" for specific entry
{
logger.LogInformation("Completed in {Seconds}", seconds);
}
logger.LogInformation("No contextual properties");
Which results in:
{"Timestamp":"2023-07-31T13:05:39.0692467+03:00","Level":"Information","MessageTemplate":"Completed in {Seconds}","Properties":{"Seconds":100,"SourceContext":"Program","foo":1,"bar":42}}
{"Timestamp":"2023-07-31T13:05:39.0945963+03:00","Level":"Information","MessageTemplate":"No contextual properties","Properties":{"SourceContext":"Program"}}
Or using Serilog's LogContext
:
// ...
using (LogContext.PushProperty("foo", 1))
using (LogContext.PushProperty("bar", 42))
{
logger.Information("Completed in {Seconds}", seconds);
}
logger.Information("No contextual properties");
UPD:
As you have suggested in the comments you can follow the approach with resolving the DiagnosticContext
as the RequestLoggingMiddleware
does, but in background job there would be no middleware so you will need to implement something similar.
Personally I would go with custom service for that, to be a little bit more decoupled from Serilog/ASP.NET Core Serilog specific infrastructure:
services.AddScoped<Dictionary<string, object>>(); // sample code, better use some custom type, potentially over concurrent collection.
services.AddLogging(builder => builder.AddSerilog(log));
var sp = services.BuildServiceProvider();
// create scope in worker per "iteration"
using (var serviceScope = sp.CreateScope())
{
var ssp = serviceScope.ServiceProvider;
var logger = ssp.GetRequiredService<ILogger<Program>>();
ssp.GetRequiredService<Dictionary<string, object>>()
.Add("foo", 1);
int seconds = 100; // do some work
ssp.GetRequiredService<Dictionary<string, object>>().Add("bar", 42); // enrich
// log with the scope info
using (logger.BeginScope(ssp.GetRequiredService<Dictionary<string, object>>())) // use the "context" for specific entry
{
logger.LogInformation("Completed in {Seconds}", seconds);
}
logger.LogInformation("No contextual properties");
}