如何在log4net消息中添加类别前缀?

8
我希望您能为所有现有日志消息添加类别前缀。但是,逐个为所有现有日志消息添加此前缀非常繁琐。我是否可以在类级别上添加属性,然后该类中的所有消息都将记录某个类别,以简化操作?
与当前方式不同,如下所示:
Log.Info("[Ref] Level 1 Starts ...");

我真的希望能够像这样或者用其他方式来定义log4net.ILog。

[LoggingCategory("Ref")]
public class MyClass 
{
   public void MyMethod()
   {
        Log.Info("Level 1 Starts ...");
   }
}

1
log4net使用日志记录器层次结构来实现此功能。 - Sergey Mirvoda
该类别实际上代表什么,即同一类别记录的对象之间如何相关?理论上,您可以通过将记录器和对应于每个类别的附加器之间的关联全部在配置中完成,例如,来自命名空间X、Y和Z中的对象的所有日志记录都使用表示类别A的附加器。但是,根据我的第一个问题的答案,这可能会变得繁琐。 - Pero P.
我在这里尝试实现的目标是使日志文件更容易解析,以便针对某些类进行操作。例如,如果A类和B类相关联,并且我想要从主日志文件中获取与A类和B类相关的所有行,则通常发现这种方式对我非常有帮助。 - tonyjy
2个回答

11
您正在询问如何通过属性来实现此操作。@Jonathan的建议看起来可能可以正常工作,但您可能可以使用log4net的内置功能来达到足够好的结果。
如果您想将类分组为“类别”,那么您可以基于类别名称而不是类名检索记录器。在设置输出格式时,您可以使用记录器格式化标记告诉log4net在输出中写入记录器名称。
通常,您会像这样基于类名检索记录器:
public class Typical
{
  private static readonly ILog logger = 
       LogManager.GetLogger
          (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

  public void F()
  {
    logger.Info("this message will be tagged with the classname");
  }
}

像这样基于任意名称检索记录器是完全可接受的:

public class A
{
  private static readonly ILog logger = LogManager.GetLogger("REF");

  public void F()
  {
    logger.Info("this message will be tagged with REF");
  }
}

public class B
{
  private static readonly ILog logger = LogManager.GetLogger("REF");

  public void F()
  {
    logger.Info("this message will be tagged with REF");
  }
}

public class C
{
  private static readonly ILog logger = LogManager.GetLogger("UMP");

  public void F()
  {
    logger.Info("this message will be tagged with UMP");
  }
}
在上面的例子中,类A和B被认为处于同一“类别”,因此它们使用相同名称检索了记录器。而类C处于不同的类别,因此它检索了具有不同名称的记录器。
您可以在配置文件中配置自己的“类别”层次结构来管理日志记录器:
App
App.DataAccess
App.DataAccess.Create
App.DataAccess.Read
App.DataAccess.Update
App.DataAccess.Delete
App.UI
App.UI.Login
App.UI.Query
App.UI.Options

您还可以配置日志记录输出格式,仅记录完全限定的记录器名称的一部分。例如:

%logger:2

获取完全限定名称的最后两部分。例如,如果您的类的完全限定名称为:

NameSpaceA.NameSpaceB.NameSpaceC.Class

那么按照以上格式,将输出以下日志记录器名称:

NameSpaceC.Class

我对语法不是100%确定,因为我没有使用过,并且现在找不到一个好的例子。

这种方法的一个缺点是,您必须定义和记住您的类别,还必须决定每个类的适当类别(如果您想用属性装饰每个类,则也会出现此问题)。 另外,如果您有多个属于同一类别的类,您无法打开或关闭日志记录,或者更改该类别的子集的日志记录级别。

也许在命名空间层次结构内记录单个命名空间会很有用:

也许您可以根据命名空间将类别分组。 因此,您可能希望将类的直接父命名空间记录为其类别。

因此,对于上述完全限定的类名,您可能希望将 "NameSpaceC" 记录为 "category" 或记录器名称。

我不确定你是否可以直接使用 log4net 实现这一点,但是你可以轻松地编写一个 PatternLayoutConverter 来获取 logger 名称并剥离类名以及任何“更高级别”的命名空间。

以下是一个自定义 PatternLayoutConverter 的示例链接。它需要一个参数,在我的情况下,我想要在字典中查找一个值。 在这种情况下,该参数可以表示完全限定的记录器名称末尾的偏移量(与 log4net 内置记录器名称布局对象的参数相同解释),但可以添加其他代码以仅记录该索引处的单个命名空间。

自定义 Log4net 属性 PatternLayoutConverter(带有索引)

同样,对于给出的完全限定类名:

NameSpaceA.NameSpaceB.NameSpaceC.Class

你可以把直接父命名空间称为“类别”(category)。 如果你定义了自定义的PatternLayoutConverter,category,并且它带有参数,那么你的配置可能是这样的:

%category

默认情况下,它会返回介于最后一个和倒数第二个'.'字符之间的子字符串。根据参数,它可以返回任何离散的命名空间。

PatternLayoutConverter可能如下所示(未经测试):

  class CategoryLookupPatternConverter : PatternLayoutConverter
  {
    protected override void Convert(System.IO.TextWriter writer, LoggingEvent loggingEvent)
    {
      //Assumes logger name is fully qualified classname.  Need smarter code to handle
      //arbitrary logger names.
      string [] names = loggingEvent.LoggerName.Split('.');
      string cat = names[names.Length - 1];
      writer.Write(setting);
    }
  }

或者,使用Option属性获取第N个(相对于末尾的)命名空间名称:

  class CategoryLookupPatternConverter : PatternLayoutConverter
  {
    protected override void Convert(System.IO.TextWriter writer, LoggingEvent loggingEvent)
    {
      //Assumes logger name is fully qualified classname.  Need smarter code to handle
      //arbitrary logger names.
      string [] names = loggingEvent.LoggerName.Split('.');
      string cat;
      if (Option > 0 && Option < names.Length)
      {
        cat = names[names.Length - Option];
      }
      else
      {
        string cat = names[names.Length - 1];
      }
      writer.Write(setting);
    }

  }

@Jonathan的想法非常棒,但是为了定义和维护一个新的记录器包装器,需要您花费额外的编码时间(但是很多人都这样做,并不觉得它是一个特别繁琐的负担)。当然,我的自定义PatternLayoutConverter想法也需要您进行自定义编码。

还有一个缺点是,在每个日志调用上调用GetCategory看起来可能会非常昂贵。


6
有趣的问题,粗略的尝试... Log4NetLogger - 日志适配器
public class Log4NetLogger
{
    private readonly ILog _logger;
    private readonly string _category;

    public Log4NetLogger(Type type)
    {
        _logger = LogManager.GetLogger(type);
        _category = GetCategory();
    }

    private string GetCategory()
    {
        var attributes = new StackFrame(2).GetMethod().DeclaringType.GetCustomAttributes(typeof(LoggingCategoryAttribute), false);
        if (attributes.Length == 1)
        {
            var attr = (LoggingCategoryAttribute)attributes[0];
            return attr.Category;
        }
        return string.Empty;
    }

    public void Debug(string message)
    {
        if(_logger.IsDebugEnabled) _logger.Debug(string.Format("[{0}] {1}", _category, message));
    }
}

LoggingCategoryAttribute - 适用于类

该属性可应用于类,用于指定日志类别。
[AttributeUsage(AttributeTargets.Class)]
public class LoggingCategoryAttribute : Attribute
{
    private readonly string _category;

    public LoggingCategoryAttribute(string category)
    {
        _category = category;
    }

    public string Category { get { return _category; } }
}

LogTester是一个测试实现。

[LoggingCategory("LT")]
public class LogTester
{
    private static readonly Log4NetLogger Logger = new Log4NetLogger(typeof(LogTester));

    public void Test()
    {
        Logger.Debug("This log message should have a prepended category");
    }
}

1
这看起来很酷,但在每个日志调用上调用GetCategory可能会很昂贵。 - wageoghe
1
你能把 GetCategory 移到构造函数里吗?这样就可以避免在每次日志调用时都调用 GetCategory。 - tonyjy
1
@tonyjy,@wageoghe - 很好的发现,修改了代码以在构造时调用GetCategory一次。 - Jonathan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接