使用NLog包装类时,如何记录实际调用方的方法与行号?
解决NLog包装类导致callsite显示错误的问题
这个问题我之前在项目里也遇到过——NLog默认会捕获直接调用它的代码位置,也就是你包装类里的Log方法内部,而不是外部调用你Logger.Log的真实业务代码位置。这里有几个简单好用的修复方案,你可以根据场景选择:
方案1:调整NLog布局参数(最简单快捷)
不需要修改任何代码,只需要在你的NLog配置里给callsite和callsite-linenumber添加skipFrames参数,跳过包装类的那一层栈帧:
<column name="callsite" layout="${callsite:skipFrames=1}" /> <column name="callsite-linenumber" layout="${callsite-linenumber:skipFrames=1}" />
原理:skipFrames=1告诉NLog忽略最近的1层栈帧(也就是你的Logger.Log方法那一层),直接获取上一层的调用信息。如果你的包装类有多层嵌套,只需要调整这个数值即可(比如skipFrames=2)。
方案2:用C# Caller特性手动传递调用信息(最可靠)
如果担心后续代码调整导致栈帧层数变化,skipFrames失效,可以用C#的Caller系列特性,让编译器自动把调用方的信息传递给NLog:
首先修改你的包装类,引入System.Runtime.CompilerServices命名空间,并给Log方法添加Caller参数:
using System; using System.Runtime.CompilerServices; using NLog; namespace Common.NLogEx { public enum LoggerNames { Database, Thermal, StateMachine, App, Poll } public enum LogLevel { Trace, Debug, Info, Warn, Error, Fatal, Off, Warning } public static class Logger { // Caller参数由编译器自动填充,调用方无需手动传入 public static void Log(LoggerNames name, LogLevel level, string message, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLine = 0) { if (level == LogLevel.Warning) level = LogLevel.Warn; var nLevel = NLog.LogLevel.FromString(level.ToString()); var logger = LogManager.GetLogger(name.ToString()); // 创建自定义日志事件,携带调用方信息 var logEvent = new LogEventInfo(nLevel, logger.Name, message); logEvent.Properties["CallerMember"] = callerMember; logEvent.Properties["CallerFilePath"] = callerFilePath; logEvent.Properties["CallerLineNumber"] = callerLine; logger.Log(logEvent); } public static void Log(LoggerNames name, LogLevel level, Exception ex, string message, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLine = 0) { if (level == LogLevel.Warning) level = LogLevel.Warn; var nLevel = NLog.LogLevel.FromString(level.ToString()); var logger = LogManager.GetLogger(name.ToString()); var logEvent = new LogEventInfo(nLevel, logger.Name, message); logEvent.Properties["CallerMember"] = callerMember; logEvent.Properties["CallerFilePath"] = callerFilePath; logEvent.Properties["CallerLineNumber"] = callerLine; logEvent.Exception = ex; logger.Log(logEvent); } } }
然后修改NLog配置里的CsvLayout,输出这些自定义属性:
<column name="callsite" layout="${event-properties:CallerMember}" /> <column name="callsite-file" layout="${event-properties:CallerFilePath}" /> <!-- 可选:输出调用文件路径 --> <column name="callsite-linenumber" layout="${event-properties:CallerLineNumber}" />
优势:完全不受栈帧变化影响,编译器直接注入调用方信息,稳定性拉满。
方案3:使用NLog的WithCaller方法(NLog 4.6+适用)
如果你使用的是NLog 4.6及以上版本,可以直接用NLog官方提供的WithCaller方法跳过指定栈帧:
public static void Log(LoggerNames name, LogLevel level, string message) { if (level == LogLevel.Warning) level = LogLevel.Warn; var nLevel = NLog.LogLevel.FromString(level.ToString()); var logger = LogManager.GetLogger(name.ToString()); // 跳过当前方法栈帧,获取外部调用位置 logger.WithCaller(1).Log(nLevel, message); } public static void Log(LoggerNames name, LogLevel level, Exception ex, string message) { if (level == LogLevel.Warning) level = LogLevel.Warn; var nLevel = NLog.LogLevel.FromString(level.ToString()); var logger = LogManager.GetLogger(name.ToString()); logger.WithCaller(1).Log(nLevel, ex, message, null); }
这个方法不需要修改配置,代码改动也很小,适合已经升级到新版NLog的项目。
内容的提问来源于stack exchange,提问作者Dave
相关产品推荐
相关产品推荐

