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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 01:06:25