.NET Core WebAPI日志扩展方法:获取调用程序集名及指定路径存日志
实现.NET Core WebAPI日志扩展方法的两个需求
嘿,这个需求其实不难拆解,咱们一步步来搞定:
一、准确获取调用日志方法的程序集名称
要拿到真正调用日志方法的业务程序集(而不是日志扩展类或框架自身的程序集),直接用Assembly.GetCallingAssembly()可能会因为JIT优化或中间层干扰不准,所以我们用StackTrace遍历栈帧来过滤:
using System.Diagnostics; using System.Reflection; private static string GetCallingAppName() { var stackTrace = new StackTrace(); // 遍历栈帧,跳过日志相关程序集,找到第一个业务代码的调用方 foreach (StackFrame frame in stackTrace.GetFrames()) { var method = frame.GetMethod(); if (method == null) continue; var declaringAssembly = method.DeclaringType.Assembly; var currentAssembly = Assembly.GetExecutingAssembly(); // 排除当前扩展类所在程序集、微软日志框架和系统程序集 if (declaringAssembly != currentAssembly && !declaringAssembly.FullName.StartsWith("Microsoft.Extensions.Logging") && !declaringAssembly.FullName.StartsWith("System.")) { return declaringAssembly.GetName().Name; } } // 兜底方案:如果找不到,返回入口程序集名称 return Assembly.GetEntryAssembly()?.GetName().Name ?? "UnknownApplication"; }
二、配置日志输出到指定路径
这里有两种方案:自定义日志提供程序(灵活可控),或者用成熟的日志框架(比如Serilog,更省心),我都给你列出来:
方案1:自定义日志提供程序(原生ILogger实现)
适合想要完全掌控日志逻辑的场景:
1. 写一个日志写入器
负责处理文件路径创建和日志写入:
public class AppSpecificFileLogger : ILogger { private readonly string _categoryName; private readonly string _appName; private readonly string _logFilePath; public AppSpecificFileLogger(string categoryName, string appName) { _categoryName = categoryName; _appName = appName; // 构建目标路径:%windir%\log\{调用程序集名称}\logfile.log var windir = Environment.GetEnvironmentVariable("windir") ?? @"C:\Windows"; var logDir = Path.Combine(windir, "log", _appName); // 确保目录存在,避免写入失败 Directory.CreateDirectory(logDir); _logFilePath = Path.Combine(logDir, "logfile.log"); } public IDisposable BeginScope<TState>(TState state) => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; // 可根据需求调整日志级别过滤 public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) return; // 拼接日志内容,包含调用程序集名称 var logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{logLevel}] [{_appName}] [{_categoryName}] {formatter(state, exception)}"; if (exception != null) { logMessage += $"\nException Details: {exception.ToString()}"; } // 异步写入,避免阻塞主线程 _ = File.AppendAllTextAsync(_logFilePath, logMessage + Environment.NewLine); } }
2. 写日志提供程序
负责创建日志实例,同时调用上面的方法获取调用程序集名称:
public class AppSpecificFileLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string categoryName) { var appName = GetCallingAppName(); return new AppSpecificFileLogger(categoryName, appName); } public void Dispose() { } // 复用之前的GetCallingAppName方法 private static string GetCallingAppName() { var stackTrace = new StackTrace(); foreach (StackFrame frame in stackTrace.GetFrames()) { var method = frame.GetMethod(); if (method == null) continue; var declaringAssembly = method.DeclaringType.Assembly; var currentAssembly = Assembly.GetExecutingAssembly(); if (declaringAssembly != currentAssembly && !declaringAssembly.FullName.StartsWith("Microsoft.Extensions.Logging") && !declaringAssembly.FullName.StartsWith("System.")) { return declaringAssembly.GetName().Name; } } return Assembly.GetEntryAssembly()?.GetName().Name ?? "UnknownApplication"; } }
3. 在Program.cs中注册
把自定义提供程序加到日志系统:
var builder = WebApplication.CreateBuilder(args); // 注册自定义日志提供程序 builder.Logging.AddProvider(new AppSpecificFileLoggerProvider()); // 其他WebAPI配置... var app = builder.Build(); // ...
方案2:用Serilog(更简洁高效)
如果不想自己造轮子,Serilog的文件Sink可以轻松实现动态路径,性能也更优:
1. 安装NuGet包
Install-Package Serilog.AspNetCore Install-Package Serilog.Sinks.File
2. 在Program.cs中配置
var builder = WebApplication.CreateBuilder(args); // 先获取调用程序集名称 var appName = GetCallingAppName(); var windir = Environment.GetEnvironmentVariable("windir") ?? @"C:\Windows"; var logPath = Path.Combine(windir, "log", appName, "logfile.log"); // 配置Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.File( path: logPath, rollingInterval: RollingInterval.Day, // 按天滚动日志,可选 outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] [{Level:u3}] [{AppName}] {Message:lj}{NewLine}{Exception}", restrictedToMinimumLevel: LogEventLevel.Information) .Enrich.WithProperty("AppName", appName) .CreateLogger(); // 替换默认日志系统 builder.Host.UseSerilog(); // 其他配置...
三、封装ILogger扩展方法(可选)
如果想给日志调用加个更友好的封装,比如直接调用logger.LogAppInfo("消息"),可以写个扩展类:
public static class LoggerExtensions { public static void LogAppInfo(this ILogger logger, string message) { var appName = GetCallingAppName(); logger.LogInformation("[{AppName}] {Message}", appName, message); } // 同样复用GetCallingAppName方法 private static string GetCallingAppName() { var stackTrace = new StackTrace(); foreach (StackFrame frame in stackTrace.GetFrames()) { var method = frame.GetMethod(); if (method == null) continue; var declaringAssembly = method.DeclaringType.Assembly; var currentAssembly = Assembly.GetExecutingAssembly(); if (declaringAssembly != currentAssembly && !declaringAssembly.FullName.StartsWith("Microsoft.Extensions.Logging") && !declaringAssembly.FullName.StartsWith("System.")) { return declaringAssembly.GetName().Name; } } return Assembly.GetEntryAssembly()?.GetName().Name ?? "UnknownApplication"; } }
关键注意事项
- 性能影响:
StackTrace会带来一定性能开销,高并发场景下建议缓存调用程序集名称,或者直接用入口程序集名称(如果不需要区分不同类库的调用)。 - 权限问题:写入
%windir%\log需要管理员权限,如果WebAPI运行在普通账户下,要么给目录加读写权限,要么换个路径(比如AppDomain.CurrentDomain.BaseDirectory\Logs)。 - 异步写入:原生
File.AppendAllTextAsync虽然是异步,但高并发下还是建议用Serilog这类自带缓冲和批量写入的框架,避免IO瓶颈。
内容的提问来源于stack exchange,提问作者fuzzybear
相关产品推荐
相关产品推荐

