You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ASP.NET Core Web API中按用户将日志写入Azure Blob

在ASP.NET Core Web API中按用户分目录存储日志的实现方案

要实现每个用户的日志独立存储到{UserId}/{Year}/{Month}/{Day}/log_.log这样的路径下,核心是动态获取当前请求的用户标识,并据此生成对应日志目录。下面提供两种可行方案:

方案一:使用Serilog实现(推荐)

Serilog自带灵活的模板配置和上下文 enrichment 能力,能快速实现按用户分目录的需求。

1. 安装依赖包

通过NuGet安装必要的包:

Install-Package Serilog.AspNetCore
Install-Package Serilog.Sinks.File

2. 配置Serilog与用户上下文注入

在Program.cs中注册HttpContextAccessor(用于获取当前用户),并配置Serilog:

var builder = WebApplication.CreateBuilder(args);

// 注册HttpContextAccessor以获取当前请求上下文
builder.Services.AddHttpContextAccessor();

// 配置Serilog
builder.Host.UseSerilog((context, services, config) => config
    .Enrich.FromLogContext() // 启用日志上下文 enrichment
    .WriteTo.File(
        path: "Logs/{UserId}/{Year:yyyy}/{Month:MM}/{Day:dd}/log_.log",
        rollingInterval: RollingInterval.Day, // 按天滚动日志文件
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
        fileSizeLimitBytes: 10 * 1024 * 1024, // 单文件最大10MB
        retainedFileCountLimit: 30 // 保留最近30天的日志
    )
);

// 注册其他服务...

var app = builder.Build();

// 添加用户日志上下文注入中间件
app.UseMiddleware<UserLogEnrichmentMiddleware>();

// 其他中间件配置...

app.Run();

3. 实现用户上下文注入中间件

创建中间件,在每个请求开始时将用户ID注入到日志上下文:

public class UserLogEnrichmentMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserLogEnrichmentMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor)
    {
        _next = next;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var userId = "Anonymous";
        if (context.User.Identity.IsAuthenticated)
        {
            // 根据你的认证方式,选择合适的用户标识字段(比如ClaimTypes.NameIdentifier)
            userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.Identity.Name ?? "Anonymous";
        }

        // 将用户ID注入日志上下文
        using (LogContext.PushProperty("UserId", userId))
        using (LogContext.PushProperty("Year", DateTime.Now.Year))
        using (LogContext.PushProperty("Month", DateTime.Now.ToString("MM")))
        using (LogContext.PushProperty("Day", DateTime.Now.ToString("dd")))
        {
            await _next(context);
        }
    }
}

方案二:自定义原生ILoggerProvider

如果不想依赖第三方库,可以基于ASP.NET Core原生日志系统实现自定义日志提供器:

1. 实现自定义Logger与Provider

// 日志配置类
public class UserFileLoggerConfig
{
    public string RootLogPath { get; set; } = "Logs";
    public LogLevel MinLogLevel { get; set; } = LogLevel.Information;
}

// 自定义LoggerProvider
public class UserFileLoggerProvider : ILoggerProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly UserFileLoggerConfig _config;
    private readonly ConcurrentDictionary<string, UserFileLogger> _loggers = new();

    public UserFileLoggerProvider(IHttpContextAccessor httpContextAccessor, UserFileLoggerConfig config)
    {
        _httpContextAccessor = httpContextAccessor;
        _config = config;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return _loggers.GetOrAdd(categoryName, name => new UserFileLogger(name, _httpContextAccessor, _config));
    }

    public void Dispose() => _loggers.Clear();
}

// 自定义Logger
public class UserFileLogger : ILogger
{
    private readonly string _categoryName;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly UserFileLoggerConfig _config;

    public UserFileLogger(string categoryName, IHttpContextAccessor httpContextAccessor, UserFileLoggerConfig config)
    {
        _categoryName = categoryName;
        _httpContextAccessor = httpContextAccessor;
        _config = config;
    }

    public IDisposable BeginScope<TState>(TState state) where TState : notnull => default!;

    public bool IsEnabled(LogLevel logLevel) => logLevel >= _config.MinLogLevel;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel)) return;

        var context = _httpContextAccessor.HttpContext;
        var userId = "Anonymous";
        if (context != null && context.User.Identity.IsAuthenticated)
        {
            userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.Identity.Name ?? "Anonymous";
        }

        // 生成日志目录与文件路径
        var now = DateTime.Now;
        var logDir = Path.Combine(_config.RootLogPath, userId, now.Year.ToString(), now.Month.ToString("D2"), now.Day.ToString("D2"));
        Directory.CreateDirectory(logDir);
        var logFilePath = Path.Combine(logDir, $"log_{now:yyyyMMdd}.log");

        // 构造日志内容
        var logContent = $"[{now:yyyy-MM-dd HH:mm:ss.fff}] [{logLevel}] [{_categoryName}] {formatter(state, exception)}";
        if (exception != null) logContent += $"{Environment.NewLine}{exception}";

        // 异步写入文件(避免阻塞请求)
        _ = File.AppendAllTextAsync(logFilePath, logContent + Environment.NewLine);
    }
}

2. 注册自定义日志提供器

在Program.cs中注册服务:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<UserFileLoggerConfig>(new UserFileLoggerConfig
{
    RootLogPath = "Logs",
    MinLogLevel = LogLevel.Information
});
builder.Services.AddSingleton<ILoggerProvider, UserFileLoggerProvider>();

// 其他服务与中间件配置...

var app = builder.Build();
app.Run();

关键注意事项

  • 用户标识选择:优先使用用户唯一ID(如ClaimTypes.NameIdentifier)而非用户名,避免因用户名重复导致日志目录冲突。
  • 性能优化:文件IO操作建议使用异步方法,避免阻塞请求线程;高并发场景可考虑引入日志队列批量写入。
  • 权限控制:确保应用程序拥有目标日志目录的读写权限,生产环境中需限制日志目录的访问权限。
  • 日志清理:定期清理过期日志文件,避免磁盘空间被占满(Serilog可通过retainedFileCountLimit配置,自定义实现需自行添加清理逻辑)。

内容的提问来源于stack exchange,提问作者abhi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:40:58