将用户名添加到 Serilog

39

我在 program.cs 中有这个 Serilog 配置。

public class Program
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
            .Build();

        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .MinimumLevel.Override("System", LogEventLevel.Warning)
                .WriteTo.MSSqlServer(Configuration.GetConnectionString("DefaultConnection"), "dbo.Log")
                .Enrich.WithThreadId()
                .Enrich.WithProperty("Version", "1.0.0")
                .CreateLogger();
            try
            {
                BuildWebHost(args).Run();
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
            }
            finally
            {
                Log.CloseAndFlush();
            }

        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseSerilog()
                .Build();
    }

现在我想将HttpContext.Current.User.Identity.Name添加到所有日志消息中。

我尝试按照文档https://github.com/serilog/serilog/wiki/Configuration-Basics#enrichers创建新的Enrich类。

class UsernameEnricher : ILogEventEnricher
    {
        public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory, HttpContext httpContext)
        {
            logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
                    "Username", httpContext.User.Identity.Name));
        }
    }

但是,ILogEventEnricher 存在与 HttpContext 不兼容的冲突。

我还尝试安装 Nuget 包 Serilog.Web.Classic,其中包含用户名 Enricher,但是 .Net Framework 和 .Net Core 的目标框架之间存在冲突,因此我无法使用此插件。

有什么想法吗?

5个回答

56

你可以创建一个中间件来将需要的属性放入LogContext。

public class LogUserNameMiddleware
{
    private readonly RequestDelegate next;

    public LogUserNameMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public Task Invoke(HttpContext context)
    {
        LogContext.PushProperty("UserName", context.User.Identity.Name);

        return next(context);
    }
}

还需要将以下内容添加到您的日志配置中:

.Enrich.FromLogContext()

在 Startup 中添加中间件 LogUserNameMiddleware,并注意该中间件应该在UserAuthentication之后添加,以便初始化context.User.Identity
例如:
    app.UseAuthentication();     

    app.UseMiddleware<LogUserNameMiddleware>();

3
它起作用了,你是我的英雄!:)) (还要在startup.cs中加载中间件 app.UseMiddleware<LogUserNameMiddleware>(); - Muflix
2
我尝试过这个,但似乎不起作用 - 中间件总是在身份验证处理程序之前调用,因此它没有用户。我尝试在调用app.UseAuthentication()之后分配它,但结果相同。有什么想法吗? - Cocowalla
1
@JianYA的回答已发布,希望能有所帮助! - Cocowalla
2
app.UseMiddleware<LogUserNameMiddleware>() 可能最好放在 app.UseMvc() 之前,确保任何身份验证中间件都在它之前。 - Josh Brown
2
这种方法存在许多问题。 - Mike
显示剩余3条评论

28

如果您正在使用Serilog.AspNetCore,那么添加身份验证/用户属性非常容易。

    app.UseSerilogRequestLogging(options =>
    {
         options.EnrichDiagnosticContext = PushSeriLogProperties;
    });



    public void PushSeriLogProperties(IDiagnosticContext diagnosticContext, HttpContext httpContext)
    {
            diagnosticContext.Set("SomePropertyName", httpContext.User...);
    }

这个非常简短的代码片段确实帮了我很大的忙,甚至可能比其他答案更好。 - Stephan
6
这将仅向请求日志条目中添加“SomePropertyName”。我该如何将此属性添加到我写入Serilog的所有日志条目中?PushSeriLogProperties方法未被其他日志调用,并且不携带此属性。 - Himal Patel
这个问题是关于在Serilog中添加用户属性的。用户属性与请求相关联。如果您想添加与httpcontext无关的其他属性,请查看https://github.com/serilog/serilog/wiki/Enrichment。 - flux

15

@Alex Riabov提出的方法存在一些问题:

  1. 需要使用Dispose方法释放被推送的属性。
  2. 中间件中的Invoke方法是异步的,因此不能只使用return next(),而需要使用await next()
  3. UseSerilogRequestLogging()中间件记录了请求信息。如果在到达该中间件之前将属性弹出,则该属性会变为空。

为了解决这些问题,我建议进行以下修改。

在中间件中:

public async Task Invoke(HttpContext context)
{
    using (LogContext.PushProperty("UserName", context.User.Identity.Name ?? "anonymous"))
    {
        await next(context);
    }
}

Startup.cs 文件中:

appl.UseRouting()
    .UseAuthentication()
    .UseAuthorization()
    .UseMiddleware<SerilogUserNameMiddleware>()
    .UseSerilogRequestLogging()
    .UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorPages();
        endpoints.MapHealthChecks("/health");
    });

你可以返回一个Task而不必等待它;你只需不将该方法标记为“async”。 - Massimiliano Kraus
1
@MassimilianoKraus 虽然这是正确的,但在处理对象时必须非常小心:如果您返回任务而不等待,并且它依赖于您(调用者)创建并需要处理的可处理资源,则处置将不可避免地在某些情况下在实际使用资源之前运行,从而导致异常。 - julealgon

12

使用操作筛选器是使用中间件的另一种选择。

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Serilog.Context;

namespace Acme.Widgets.Infrastructure
{
    public class LogEnrichmentFilter : IActionFilter
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        public LogEnrichmentFilter(IHttpContextAccessor httpContextAccessor)
        {
            this.httpContextAccessor = httpContextAccessor;
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var httpUser = this.httpContextAccessor.HttpContext.User;

            if (httpUser.Identity.IsAuthenticated)
            {
                var appUser = new AppIdentity(httpUser);
                LogContext.PushProperty("Username", appUser.Username);
            }
            else
            {
                LogContext.PushProperty("Username", "-");
            }
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // Do nothing
        }
    }
}

在你的Startup.ConfigureServices方法中,你需要:

  1. 确保将IHttpContextAccessor添加到IoC容器中
  2. LogEnrichmentFilter添加到IoC容器,并限定请求范围
  3. LogEnrichmentFilter注册为全局操作过滤器

Startup.cs:

services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<LogEnrichmentFilter>();

services.AddMvc(o =>
{
    o.Filters.Add<LogEnrichmentFilter>();
});

然后,对于在MVC操作调用管道中运行的代码,您应该在日志上下文中拥有当前用户名。如果您使用资源过滤器而不是操作过滤器,则我想用户名将附加到更多日志条目中,因为它们在管道中稍早运行(我刚刚发现这些!)


你好!感谢您的回答。当用户登录时,这是如何工作的?目前似乎没有人能够检测到用户首次登录,只有之后的请求。 - JianYA
@JianYA 只有在授权之后,它才会将用户名添加到日志上下文中。如果我想在实际的登录过程中记录某些内容,我会在负责的控制器/处理程序/服务中单独处理。 - Cocowalla
我明白了。谢谢! - JianYA
1
你上面的代码中的'AppIdentity'方法是什么? - spankymac
1
@spankymac 我这里没有包含它,但它对我所展示的内容并不重要 - AppIdentity 只是扩展了 ClaimsIdentity,以提供一些方便的属性来访问声明值。 - Cocowalla
显示剩余2条评论

11

你只需两步就能完成它

1- 创建一个可以访问服务的增强器。

using Microsoft.AspNetCore.Http;
using Serilog.Core;
using Serilog.Events;
using System.Security.Claims;

namespace CoolProject.Logging.Enricher;
public class UserEnricher : ILogEventEnricher
{
private readonly IHttpContextAccessor _httpContextAccessor;

public UserEnricher() : this(new HttpContextAccessor())
{
}

//Dependency injection can be used to retrieve any service required to get a user or any data.
//Here, I easily get data from HTTPContext
public UserEnricher(IHttpContextAccessor httpContextAccessor)
{
    _httpContextAccessor = httpContextAccessor;
}

public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
    logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
            "UserId", _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous"));
}
}

使用 With 来包含您的 UserEnricher。

loggerConfiguration.Enrich.FromLogContext()
            .MinimumLevel.Is(level)
            .Enrich.With<UserEnricher>()

只需要两个步骤就可以添加用户增强器,但我也会添加我的驱动程序代码。不要忘记注入IHttpContextAccessor!

 public static IHostBuilder UseLogging(this IHostBuilder webHostBuilder, string applicationName = null)
    => webHostBuilder.UseSerilog((context ,loggerConfiguration) =>
    {
        var logOptions = context.Configuration.GetSection("logging");
        var serilogOptions = logOptions.GetSection("serilog").Get<SerilogOptions>();
        if (!Enum.TryParse<LogEventLevel>(serilogOptions.Level, true, out var level))
        {
            level = LogEventLevel.Error;
        }

        loggerConfiguration.Enrich.FromLogContext()
            .MinimumLevel.Is(level)
            .Enrich.With<UserEnricher>()
            .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
            .Enrich.WithProperty("ApplicationName", applicationName);
        loggerConfiguration.WriteTo.Console(outputTemplate: "{Timestamp:HH:mm:ss} [{Level}]  {Environment} {ApplicationName} {UserId} {Message:lj}{NewLine}{Exception}");

    });

3
这是最干净、最恰当和最新的答案。 - user2839499

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接