Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add category prefix to log4net message?

I like to add category prefix to all the messages on the existing logging messages. However it is tedious to add this prefix to all the existing logging messages one by one. Is there a way in I can just add an attribute to the class level then all the messages in this class will be logged for certain category?

Instead of the way right now as below,

Log.Info("[Ref] Level 1 Starts ...");

I really want to something like this or a different way to define the log4net.ILog.

[LoggingCategory("Ref")]
public class MyClass 
{
   public void MyMethod()
   {
        Log.Info("Level 1 Starts ...");
   }
}
like image 764
tonyjy Avatar asked Dec 15 '10 20:12

tonyjy


2 Answers

You are asking how to do this via an Attribute. @Jonathan's suggestion looks like it would probably work ok, but you might be able to achieve a good-enough result using log4net's built in capabilities.

If you want to group classes into "categories", then you could retrieve the logger based on the category name rather than on the classname. When you set up your output format, you can use the logger formatting token to tell log4net to write the logger name in the output.

Typically one would retrieve the logger based on the classname like this:

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");
  }
}

It is perfectly acceptable to retrieve loggers based on an arbitrary name like this:

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");
  }
}

In the preceding example, classes A and B are considered to be in the same "category", so they retrieved the logger with the same name. Class C is in a different category, so it retrieved the logger with a different name.

You could configure your loggers (in the config file) with your own "category" hierarchy:

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

You can also configure the logger output format to log only part of the fully qualified logger name. Something like this:

%logger:2

To get the last 2 parts of the fully qualified name. For example, if your class's fully qualified name is:

NameSpaceA.NameSpaceB.NameSpaceC.Class

Then the above format would output this as the logger name:

NameSpaceC.Class

I am not 100% sure about the syntax because I haven't used it and I can't find a good example right now.

One drawback to this approach is that you have to define and remember what your categories are and you must decide what the appropriate category is for each class (you also have this issue if you want to decorate each class with an attribute containing its category). Also, if you have several classes in the same category, you cannot turn logging on or off or change the logging level for a subset of those classes.

Maybe it would be useful to log a single namespace from within the namespace hierarchy:

Maybe you can "categorize" your classes based on their namespace. So, you might want to log the immediate parent namespace of a class as its category.

So, for the fully qualified class name above, you might want to log "NameSpaceC" as the "category" or logger name.

I'm not sure if you can do that out of the box with log4net, but you could easily write a PatternLayoutConverter to get the logger name and strip off the class name and any "higher level" namespaces.

Here is a link to an example of a custom PatternLayoutConverter. Takes a parameter that, in my case, I wanted to use to look up a value in a dictionary. In this case the parameter could represent the offset from the END of the fully-qualified logger name (the same interpretation as the parameter to log4net's built in logger name layout object), but additional code could be added to log ONLY the single namespace at that index.

Custom log4net property PatternLayoutConverter (with index)

Again, given this fully qualified class name:

NameSpaceA.NameSpaceB.NameSpaceC.Class

You might consider the immediate parent namespace to be the "category". If you defined a custom PatternLayoutConverter, category, and it took a parameter, then your configuration might look like this:

%category

By default it would return the substring between the last and next to last '.' characters. Given a parameter, it could return any discrete namespace up the chain.

The PatternLayoutConverter might look something like this (untested):

  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);
    }
  }

Or, using the Option property to get the Nth namespace name (relative to the end):

  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);
    }

  }

The idea from @Jonathan is pretty cool, but it does add some extra coding on your part to define and maintain a new logger wrapper (but a lot of people do that and don't find it to be a particularly onerous burden). Of course, my custom PatternLayoutConverter ideas also require custom code on your part.

One other drawback is that GetCategory looks like it could be pretty expensive to call on every logging call.

like image 141
wageoghe Avatar answered Oct 23 '22 23:10

wageoghe


Interesting problem, rough attempt...

Log4NetLogger - logging adapter

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 - applicable to classes

[AttributeUsage(AttributeTargets.Class)]
public class LoggingCategoryAttribute : Attribute
{
    private readonly string _category;

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

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

LogTester - a test implementation

[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");
    }
}
like image 26
Jonathan Avatar answered Oct 23 '22 23:10

Jonathan