包装 NLog 时如何保留调用点信息

Posted

技术标签:

【中文标题】包装 NLog 时如何保留调用点信息【英文标题】:How to retain callsite information when wrapping NLog 【发布时间】:2011-11-16 18:00:03 【问题描述】:

我有一个包装 NLog 的类(称为 NLogger)。我的日志被保存到我的数据库中。 我遇到的问题是如何显示日志记录发生的位置。 我有这个

<parameter name="@Logger" layout="$callsite"/>  

但这只是显示 Core.Logging.Loggers.NLogLogger.Log 这是我的 NlogWrapper 而不是调用我的包装器的类。

这是我的包装方法

        public void Log(LogType messageType, Type context, string message, Exception exception)
        
            NLog.Logger logger = NLog.LogManager.GetLogger(context.Name);
            LogLevel logLevel = LogLevel.Info; // Default level to info

            switch (messageType)
            
                case LogType.Debug:
                    logLevel = LogLevel.Debug;
                    break;
                case LogType.Info:
                    logLevel = LogLevel.Info;
                    break;
                case LogType.Warning:
                    logLevel = LogLevel.Warn;
                    break;
                case LogType.Error:
                    logLevel = LogLevel.Error;
                    break;
                case LogType.Fatal:
                    logLevel = LogLevel.Fatal;
                    break;
                default:
                    throw new ArgumentException("Log message type is not supported");                    
            

            logger.Log(logLevel, message, exception);
        

【问题讨论】:

【参考方案1】:

现在修复呼叫站点的一种更简单的方法是使用LogManager.AddHiddenAssembly(Assembly)

例如

LogManager.AddHiddenAssembly(yourAssembly);

这将修复调用站点,无需进行手动堆栈遍历等。

【讨论】:

这是否涵盖了整个程序集,或者如果从 Func/delegate 中调用 Trace() 也会有帮助吗? 它过滤来自该程序集的堆栈帧。【参考方案2】:

有一种简单的方法可以实现这一点。只需将这些属性添加到您的日志包装方法签名中:

void Log(LogSeverity severity, string message, [CallerFilePath] string fileName = null, [CallerMemberName] string member = null, [CallerLineNumber] int? lineNumber = null);

并将这些传递给包装好的 NLog 方法。

有关 .NET 中 System.Runtime.CompilerServices 属性的更多信息,请参阅 https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.callerfilepathattribute?view=netframework-4.7.2。

【讨论】:

【参考方案3】:

要跳过几帧并深入到包装调用者上下文中,请在 App.config 或程序中设置著名的修饰符:

skipFrames=1

示例: 见this page$callsite:skipFrames=Integer 和this page 为$callsite-linenumber:skipFrames=Integer

我建议您在包装器中使用这种格式:

$callsite:fileName=true:includeSourcePath=false:skipFrames=1

此设置的输出如下:

... LicenseServer.LSCore.MainThreadFunction(LSCore.cs:220) ...

【讨论】:

不确定哪个是 NLog 上支持 skipFrames=Integer 功能的最低版本。必须检查他们的文档。 查看此链接以获取布局渲染器列表:github.com/nlog/nlog/wiki/Layout-Renderers 赢家鸡肉晚餐,这对我来说非常适合。 截至今天,这是泛型类唯一可行的解​​决方案。我试过接受的答案无济于事,但这就像一个魅力。【参考方案4】:

伙计们 经过几天的努力和搜索。最后,我只使用一个简单的类构建了 Nlog Wrapper,它可以保留 $callsite 并在创建 Nlog Wrapper 实例时获取正确的记录器名称。我将把代码放在后面加上简单的注释。如您所见,我使用 Stacktrace 来获取正确的记录器名称。使用write和writewithex注册logevnet,这样就可以保留callsite了。

  public  class NlogWrapper 
      
        private  readonly NLog.Logger _logger; //NLog logger

    /// <summary>
    /// This is the construtor, which get the correct logger name when instance created  
    /// </summary>

    public NlogWrapper()
        
        StackTrace trace = new StackTrace();

        if (trace.FrameCount > 1)
        
            _logger = LogManager.GetLogger(trace.GetFrame(1).GetMethod().ReflectedType.FullName);
        
        else //This would go back to the stated problem
        
            _logger = LogManager.GetCurrentClassLogger();
        
    
    /// <summary>
    /// These two method are used to retain the $callsite for all the Nlog method  
    /// </summary>
    /// <param name="level">LogLevel.</param>
    ///  <param name="format">Passed message.</param>
    ///  <param name="ex">Exception.</param>
    private void Write(LogLevel level, string format, params object[] args)
    
        LogEventInfo le = new LogEventInfo(level, _logger.Name, null, format, args);
        _logger.Log(typeof(NlogWrapper), le);
    
    private void WriteWithEx(LogLevel level, string format,Exception ex, params object[] args)
    
        LogEventInfo le = new LogEventInfo(level, _logger.Name, null, format, args);
        le.Exception = ex;
        _logger.Log(typeof(NlogWrapper), le);
    


    #region  Methods
    /// <summary>
    /// This method writes the Debug information to trace file
    /// </summary>
    /// <param name="message">The message.</param>
    public  void Debug(String message)
        
            if (!_logger.IsDebugEnabled) return;

        Write(LogLevel.Debug, message);
      

    public  void Debug(string message, Exception exception, params object[] args)
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Debug, message, exception);
    

    /// <summary>
    /// This method writes the Information to trace file
    /// </summary>
    /// <param name="message">The message.</param>
    public  void Info(String message)
        
            if (!_logger.IsInfoEnabled) return;
        Write(LogLevel.Info, message);
    

    public  void Info(string message, Exception exception, params object[] args) 
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Info, message, exception);
    
    /// <summary>
    /// This method writes the Warning information to trace file
    /// </summary>
    /// <param name="message">The message.</param>
    public  void Warn(String message)
        
            if (!_logger.IsWarnEnabled) return;
          Write(LogLevel.Warn, message); 
        

    public  void Warn(string message, Exception exception, params object[] args)
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Warn, message, exception);
    

    /// <summary>
    /// This method writes the Error Information to trace file
    /// </summary>
    /// <param name="error">The error.</param>
    /// <param name="exception">The exception.</param>
    //   public static void Error( string message)
    //  
    //    if (!_logger.IsErrorEnabled) return;
    //  _logger.Error(message);
    //

    public  void Error(String message) 
    
        if (!_logger.IsWarnEnabled) return;
        //_logger.Warn(message);
        Write(LogLevel.Error, message);
    
    public void Error(string message, Exception exception, params object[] args)
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Error, message, exception);
      


    /// <summary>
    /// This method writes the Fatal exception information to trace target
    /// </summary>
    /// <param name="message">The message.</param>
    public void Fatal(String message)
        
            if (!_logger.IsFatalEnabled) return;
         Write(LogLevel.Fatal, message);
    

    public void Fatal(string message, Exception exception, params object[] args)
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Fatal, message, exception);
    

    /// <summary>
    /// This method writes the trace information to trace target
    /// </summary>
    /// <param name="message">The message.</param>
    /// 
    public  void Trace(string message, Exception exception, params object[] args)  
    
        if (!_logger.IsFatalEnabled) return;
        WriteWithEx(LogLevel.Trace, message, exception);
    
    public  void Trace(String message)
        
            if (!_logger.IsTraceEnabled) return;
            Write(LogLevel.Trace, message);
    

        #endregion

    

【讨论】:

当你在你的项目中使用它时。只需这样做: NlogWrapper mylog= new NlogWrapper( ); mylog.Error("这是关于:", ex).然后包装类将处理记录器名称和调用站点信息。当您使用 $logger $callsite。您将获得正确的信息,而不是 logwrapper 类调用站点的信息【参考方案5】:

或者,您可以从 NLog 设置中避免本机解决方案,并检索文件 |方法 |包装代码中的行信息:

using System.Diagnostics;
...
static private string GetCallsite()

  StackFrame sf = new StackTrace(2/*Skip two frames - dive to the callers context*/, true/*Yes I want the file info !*/).GetFrame(0);
  return "" + sf.GetFileName() + " | " + sf.GetMethod().Name + "-" + sf.GetFileLineNumber() + " ";

然后你只需调用你的静态方法并在消息之前添加调用站点:

LogManager.GetCurrentClassLogger().Trace(GetCallsite() + "My Trace Message.");

【讨论】:

注意:如果您使用 GetCallsite() 方法,则需要跳过 2 帧,因为这将比包装器本身中的调用方法高一个堆栈级别。然后注意不要从较低级别调用它。 此代码无法很好地扩展。从性能方面来看,使用 StackFrame 的成本很高【参考方案6】:

问题是您的包装器没有正确包装。这是一个如何正确包装 NLog 的示例,直接取自source tree of NLog:

using System;
using System.Text;
using NLog;

namespace LoggerWrapper
    
  /// <summary>    
  /// Provides methods to write messages with event IDs - useful for the Event Log target.    
  /// Wraps a Logger instance.    
  /// </summary>    
  class MyLogger    
          
    private Logger _logger;        

    public MyLogger(string name)        
                
      _logger = LogManager.GetLogger(name);        
            

    public void WriteMessage(string eventID, string message)           
                
      ///            
      /// create log event from the passed message            
      ///             
      LogEventInfo logEvent = new LogEventInfo(LogLevel.Info, _logger.Name, message);


      //
      // set event-specific context parameter            
      // this context parameter can be retrieved using $event-context:EventID            
      //            
      logEvent.Context["EventID"] = eventID;            
      //             
      // Call the Log() method. It is important to pass typeof(MyLogger) as the            
      // first parameter. If you don't, $callsite and other callstack-related             
      // layout renderers will not work properly.            
      //            
      _logger.Log(typeof(MyLogger), logEvent);        
        
  

关键是将记录器包装器的类型传递给对 Log 的调用。当 NLog 试图找到调用点时,它会向上堆栈,直到第一个调用方法的声明类型不是传递给 Log 调用的类型。这将是实际调用包装器的代码。

在您的情况下,您的记录器看起来像这样:

    public void Log(LogType messageType, Type context, string message, Exception exception)
    
        NLog.Logger logger = NLog.LogManager.GetLogger(context.Name);
        LogLevel logLevel = LogLevel.Info; // Default level to info

        switch (messageType)
        
            case LogType.Debug:
                logLevel = LogLevel.Debug;
                break;
            case LogType.Info:
                logLevel = LogLevel.Info;
                break;
            case LogType.Warning:
                logLevel = LogLevel.Warn;
                break;
            case LogType.Error:
                logLevel = LogLevel.Error;
                break;
            case LogType.Fatal:
                logLevel = LogLevel.Fatal;
                break;
            default:
                throw new ArgumentException("Log message type is not supported");                    
        

        //
        // Build LogEvent here...
        //
        LogEventInfo logEvent = new LogEventInfo(logLevel, context.Name, message);
        logEvent.Exception = exception;

        //
        // Pass the type of your wrapper class here...
        //
        logger.Log(typeof(YourWrapperClass), logEvent);
    

【讨论】:

我已经根据 github.com/NLog/NLog/tree/master/examples/ExtendingLoggers 的 LoggerWrapper 示例扩展了 NLog,以便我可以填充自定义事件属性,例如 logEvent.Properties["EventID"] = eventID;,如示例中所示。我想将调用站点添加为属性之一 - 即logEvent.Properties["callsite"] = *caller_method_name*。如何以编程方式获取呼叫站点信息?【参考方案7】:

我一直在努力解决这个问题。

真正重要的是日志文件中的调用站点(FullyQualified Namespace)。

首先,我尝试从 Stacktrace 中获取正确的记录器:

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static NLog.Logger GetLogger()
    
        var stackTrace = new StackTrace(false);
        StackFrame[] frames = stackTrace.GetFrames();
        if (null == frames) throw new ArgumentException("Stack frame array is null.");
        StackFrame stackFrame;
        switch (frames.Length)
        
            case 0:
                throw new ArgumentException("Length of stack frames is 0.");
            case 1:
            case 2:
                stackFrame = frames[frames.Length - 1];
                break;
            default:
                stackFrame = stackTrace.GetFrame(2);
                break;
        

        Type declaringType = stackFrame.GetMethod()
                                       .DeclaringType;

        return declaringType == null ? LogManager.GetCurrentClassLogger() :                 LogManager.GetLogger(declaringType.FullName);
    

但遗憾的是,带有 MEF 的 Stacktrace 非常长,我无法清楚地识别 ILogger 请求者的正确调用者。

所以,我没有通过构造函数注入注入 ILogger 接口,而是创建了一个 ILogFactory 接口,它可以通过构造函数注入注入,然后调用工厂上的创建方法

    public interface ILogFactory
    
        #region Public Methods and Operators

        /// <summary>
        ///     Creates a logger with the Callsite of the given Type
        /// </summary>
        /// <example>
        ///     factory.Create(GetType());
        /// </example>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        ILogger Create(Type type);

        #endregion
    

并实现它:

    using System;
    using System.ComponentModel.Composition;

    [Export(typeof(ILogFactory))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class LogFactory : ILogFactory
    
        #region Public Methods and Operators

        public ILogger Create(Type type)
        
            var logger = new Logger().CreateLogger(type);
            return logger;
        

        #endregion
    

使用 ILogger:

    public interface ILogger
    
        #region Public Properties

        bool IsDebugEnabled  get; 

        bool IsErrorEnabled  get; 

        bool IsFatalEnabled  get; 

        bool IsInfoEnabled  get; 

        bool IsTraceEnabled  get; 

        bool IsWarnEnabled  get; 

        #endregion

        #region Public Methods and Operators

        void Debug(Exception exception);
        void Debug(string format, params object[] args);
        void Debug(Exception exception, string format, params object[] args);
        void Error(Exception exception);
        void Error(string format, params object[] args);
        void Error(Exception exception, string format, params object[] args);
        void Fatal(Exception exception);
        void Fatal(string format, params object[] args);
        void Fatal(Exception exception, string format, params object[] args);
        void Info(Exception exception);
        void Info(string format, params object[] args);
        void Info(Exception exception, string format, params object[] args);
        void Trace(Exception exception);
        void Trace(string format, params object[] args);
        void Trace(Exception exception, string format, params object[] args);
        void Warn(Exception exception);
        void Warn(string format, params object[] args);
        void Warn(Exception exception, string format, params object[] args);

        #endregion
    

和实现:

    using System;

      using NLog;
      using NLog.Config;

      /// <summary>
      ///     The logging service.
      /// </summary>
      public class Logger : NLog.Logger, ILogger
      
          #region Fields

          private string _loggerName;

          #endregion

          #region Public Methods and Operators

          /// <summary>
          ///     The get logging service.
          /// </summary>
          /// <returns>
          ///     The <see cref="ILogger" />.
          /// </returns>
          public ILogger CreateLogger(Type type)
          
              if (type == null) throw new ArgumentNullException("type");               

              _loggerName = type.FullName;

              var logger = (ILogger)LogManager.GetLogger(_loggerName, typeof(Logger));

              return logger;
          

要使用它...只需注入 ILogFactory 并在 Mefed 导入构造函数中调用 Create 方法:

      [ImportingConstructor]
      public MyConstructor(          
        ILogFactory logFactory)
       
        _logger = logFactory.Create(GetType());
        

希望对你有帮助

【讨论】:

【参考方案8】:
internal string GetCallingMethodName()

  string result = "unknown";
  StackTrace trace = new StackTrace(false);
  for (int i = 0; i < trace.FrameCount; i++)
  
    StackFrame frame = trace.GetFrame(i);
    MethodBase method = frame.GetMethod();
    Type dt = method.DeclaringType;
    if (!typeof(ILogger).IsAssignableFrom(dt) && method.DeclaringType.Namespace != "DiagnosticsLibrary")
    
      result = string.Concat(method.DeclaringType.FullName, ".", method.Name);
      break;
    
  
  return result;

来源:http://slf.codeplex.com/discussions/210075

我使用上面发布的代码来简单地提取调用方法名称并将其作为“消息”参数的一部分传递给布局。这让我可以将调用日志包装器的原始方法名称写入日志文件(而不是日志包装器的类名)。

【讨论】:

此代码无法很好地扩展。从性能方面来看,使用 StackFrame 的成本很高。

以上是关于包装 NLog 时如何保留调用点信息的主要内容,如果未能解决你的问题,请参考以下文章

NLog 基于静态变量创建日志文件

怎样用JS实现:点击退出系统,出现提示信息框,点确认,返回到相应页面,点取消,保留在当前页面。谢谢!

NLog:找不到架构信息

如何实现二次抛异常时保存第一次异常的详细信息?

WooCommerce:当客户离开然后回来时如何保留结帐信息?

asp.net mvc 如何实现自动提交或保留用户未登录前得信息