如何在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
相关产品推荐
相关产品推荐

