C#使用Serilog记录日志时如何自动获取异常真实发生行号
问题原因
你当前使用[CallerLineNumber]实现的Here()扩展方法,获取的是调用日志写入方法时的代码行号,也就是catch块里写Log.Here().Error(...)的位置,根本没有读取异常对象本身携带的堆栈信息,自然无法拿到异常实际抛出的行号。
最优解决方案:全局自定义Enricher统一处理
不需要在每个catch块重复写堆栈解析代码,只要实现一个Serilog的日志事件增强器(Enricher),全局注册后自动识别携带异常的日志,解析异常堆栈提取真实抛出位置的信息,直接适配现有输出模板。
第一步:实现异常源信息增强器
using System.Diagnostics; using Serilog.Core; using Serilog.Events; public class ExceptionOriginEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { // 无异常的普通日志直接跳过,不修改原有上下文 if (logEvent.Exception == null) return; try { var stackTrace = new StackTrace(logEvent.Exception, true); StackFrame? targetFrame = null; // 遍历栈帧,找到第一个携带有效行号信息的帧,兼容异步、嵌套异常场景 for (int i = 0; i < stackTrace.FrameCount; i++) { var frame = stackTrace.GetFrame(i); var lineNumber = frame?.GetFileLineNumber() ?? 0; if (lineNumber > 0) { targetFrame = frame; break; } } if (targetFrame == null) return; // 提取异常真实抛出位置信息,覆盖原有上下文属性 var realLine = targetFrame.GetFileLineNumber(); var realFilePath = targetFrame.GetFileName(); var realMemberName = targetFrame.GetMethod()?.Name; logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("LineNumber", realLine)); if (!string.IsNullOrWhiteSpace(realFilePath)) logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("FilePath", realFilePath)); if (!string.IsNullOrWhiteSpace(realMemberName)) logEvent.AddOrUpdateProperty(propertyFactory.CreateProperty("MemberName", realMemberName)); } catch { // 堆栈解析失败时静默跳过,不阻塞正常日志输出 } } }
第二步:全局注册增强器
在Serilog初始化配置中,把这个增强器注册到管道中,注意放在Enrich.FromLogContext()之后,保证它的属性优先级高于Here()方法传入的catch块位置信息:
var outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}"; Log.Logger = new LoggerConfiguration() .MinimumLevel.Warning() .Enrich.FromLogContext() // 注册异常源增强器 .Enrich.With<ExceptionOriginEnricher>() .WriteTo.RollingFile("log/{Date}.log", outputTemplate, LogEventLevel.Warning) .WriteTo.Console(LogEventLevel.Warning, outputTemplate, theme: AnsiConsoleTheme.Literate) .CreateLogger();
注意事项
- 要正常获取异常行号,程序运行目录必须保留对应程序集的PDB符号文件,发布时不要删除PDB文件,否则
GetFileLineNumber()会返回0,这是.NET堆栈解析的通用要求。 - 上述实现会自动兼容异步代码抛异常、异常多层嵌套(InnerException)的场景,不会出现行号定位到框架内部代码的问题。
- 没有携带异常的普通日志(比如普通信息、调试日志)不会被这个增强器修改,仍然保留
Here()方法获取的日志调用位置信息,不影响普通日志的上下文准确性。 - 如果不想覆盖原有
LineNumber属性,可以把异常真实位置信息存为独立属性(比如ExceptionLineNumber、ExceptionFilePath),再对应调整输出模板即可,不需要修改业务代码。
内容的提问来源于stack exchange,提问作者CodeMan03
相关产品推荐
相关产品推荐

