Reputation: 673
In a .Net Core 2.2 API, I am trying to use Serilog for logging to SQL Server to take advantage of its structured logging capabilities. Besides the standard fields (Id, Timestamp, Level, Message, MessageTemplate, LogEvent), I need to capture the user’s name and IP address with every log entry. I have seen this post but I’d like to do this in one place so the developers don’t have to add it manually with each log statement. I have the following snippet in my Startup.cs
ctor:
public Startup(IConfiguration configuration)
{
_configuration = configuration;
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.Enrich.With<SerilogContextEnricher>()
.CreateLogger();
}
The .Enrich.FromLogContext()
call makes the static LogContext available for me to add my custom properties (username and IP address) in a custom middleware like so:
public class SerilogMiddleware
{
private static readonly ILogger Log = Serilog.Log.ForContext<SerilogMiddleware>();
private readonly RequestDelegate _next;
public SerilogMiddleware(RequestDelegate next)
{
this._next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
// Get user info
var user = httpContext.User.FindFirst(ClaimTypes.Name)?.Value ?? "anonymous";
// Get client info
var client = httpContext.Connection.RemoteIpAddress.ToString() ?? "unknown";
// enrich LogContext with user info
using (LogContext.PushProperty("User", user))
using (LogContext.PushProperty("Client", client))
{
try
{
await _next(httpContext);
}
// Never caught, because LogException() returns false.
catch (Exception ex) when (LogException(httpContext, ex)) { }
}
}
private static bool LogException(HttpContext httpContext, Exception ex)
{
var logForContext = Log.ForContext("StackTrace", ex.StackTrace);
logForContext.Error(ex, ex.Message);
return false;
}
}
However, the .Enrich.FromLogContext()
call also adds ActionId, ActionName, RequestId, RequestPath and CorrelationId properties to the LogEvent field. I don’t want to bloat my log table with these 5 properties. My current solution is to enrich my logger with the below custom enricher that removes these properties from EventLog.
public class SerilogContextEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.RemovePropertyIfPresent("ActionId");
logEvent.RemovePropertyIfPresent("ActionName");
logEvent.RemovePropertyIfPresent("RequestId");
logEvent.RemovePropertyIfPresent("RequestPath");
logEvent.RemovePropertyIfPresent("CorrelationId");
}
}
This all works fine but it seems silly to initially add these properties to the logger and then remove them afterwards. Does anyone know of a way to push custom properties to all log entries universally without this game of adding and removing unwanted properties?
Upvotes: 11
Views: 21864
Reputation: 359
Serilog allows you to inject services from dependency injection (DI) into your logging pipeline. You can implement a custom ILogEventEnricher
that injects the IHttpContextAccessor
to resolve the current HTTP context:
class HttpContextLogEventEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _contextAccessor;
HttpContextLogEventEnricher(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
// HttpContext may be null if there is no active context
var userName = _contextAccessor.HttpContext?.User.Identity.Name;
if (!string.IsNullOrEmpty(userName))
{
var userNameProperty = propertyFactory.CreateProperty("UserName", userName);
logEvent.AddPropertyIfAbsent(userNameProperty);
}
}
}
Register your customer enricher with dependency injection along with the HttpContextAccessor. With the Microsoft DI container:
services.AddHttpContextAccessor();
services.AddTransient<ILogEventEnricher, HttpContextLogEventEnricher>();
Finally, in your Serilog configuration use the ReadFrom.Services()
method. Serilog will use all the ILogEventEnrichers
it finds in the service provider:
Host.CreateDefaultBuilder().UseSerilog(
(hostBuilderContext, serviceProvider, loggerConfiguration) =>
{
loggerConfiguration.ReadFrom.Services(serviceProvider)
}
Note: The ReadFrom.Services()
extension is part of the Serilog.Extensions.Hosting
NuGet package (which is included in the Serilog.AspNetCore
package).
Upvotes: 4