一个Log4Net包装类应该长什么样子?

57

我一直在寻找适用于.NET (C#)的日志记录框架,阅读了StackOverflow上的一些问题/回答帖子后决定尝试使用log4net。我看到人们一遍又一遍地提到他们使用log4net的包装类,我想知道那是什么样子。

我的代码分为不同的项目(数据访问/业务/网络服务/…)。log4net的包装类会是什么样子?这个包装类需要包含在所有项目中吗?我应该将其作为一个独立的项目来构建吗?

这个包装类应该是一个单例类吗?


7
Log4Net本身就有严格的基于接口的API,所以实际上没有必要进行封装。 - Martin Buberl
2
Wrapper 也有一点小缺陷 - 当你使用 %M、%stacktrace 或 %stacktracedetails 时,这将为包装类提供方法/类型名称,因为 log4net 是从包装方法中调用的。 - Baljeetsingh Sucharia
9个回答

55
基本上,您需要创建一个接口,然后创建该接口的具体实现,该实现将直接包装Log4net的类和方法。可以通过创建更多的具体类来包装其他日志记录系统。最后,使用工厂根据配置设置或代码更改行创建包装器的实例。(注意:使用控制反转容器(例如StructureMap)可获得更灵活而复杂的效果。)
public interface ILogger
{
    void Debug(object message);
    bool IsDebugEnabled { get; }

    // continue for all methods like Error, Fatal ...
}

public class Log4NetWrapper : ILogger
{
    private readonly log4net.ILog _logger;

    public Log4NetWrapper(Type type)
    {
        _logger = log4net.LogManager.GetLogger(type);
    }

    public void Debug(object message)
    {
        _logger.Debug(message);
    }

    public bool IsDebugEnabled
    {
        get { return _logger.IsDebugEnabled; }
    }

    // complete ILogger interface implementation
}

public static class LogManager
{
    public static ILogger GetLogger(Type type)
    {
        // if configuration file says log4net...
        return new Log4NetWrapper(type);
        // if it says Joe's Logger...
        // return new JoesLoggerWrapper(type);
    }
}

以下是在类中使用此代码的示例(声明为静态只读字段):

private static readonly ILogger _logger =
    LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

你可以使用以下方法获得相同且更加高效的效果:
private static readonly ILogger _logger = 
    LogManager.GetLogger(typeof(YourTypeName));

前者的示例被认为更易于维护。

您不希望创建一个Singleton来处理所有日志记录,因为Log4Net记录调用类型的日志;每个类型使用自己的记录器要比只在日志文件中看到单个类型报告所有消息更加清晰和有用。

由于您的实现应该是相当可重复使用的(在您组织中的其他项目),所以您可以将其作为自己的程序集或理想情况下包含在您自己的个人/组织的框架/实用程序程序集中。不要在您的业务/数据/UI程序集中分别重新声明这些类,这是不可维护的。


我本来想说你不应该只有一个日志记录器,但这更像是一个日志工厂,这正是你想要的。 每个类都应该有自己的日志记录器,因为它有助于日志反射。 - Omar Kooheji
为什么不使用一个单例来查找Dictionary<type,ILogger>中已存在的(懒加载实例化的)记录器?我可能误解了一些东西,但是看起来你的实现会为每个使用它的类的实例化创建一个新的记录器。假设这个类的构造函数调用了LogManager.GetLogger()?或者你是假设所有消费类都必须管理自己的单例以获取它们各自的ILogger - mo.
ILogger 实例是静态的,因此您只能在每个类中获取一个实例,而不是每个类的实例都会获得一个新的记录器。 - cfeduke
在这种情况下,您是否仍然需要在使用此包装器的地方添加log4net.dll?这个包装器不能以某种方式嵌入它吗? - juagicre

27

假设你选择的是类似于cfeduke上面的回答,你还可以像这样在LogManager中添加一个重载:

public static ILogger GetLogger()
{
    var stack = new StackTrace();
    var frame = stack.GetFrame(1);
    return new Log4NetWrapper(frame.GetMethod().DeclaringType);
}

这样,在你的代码中现在可以直接使用:

private static readonly ILogger _logger = LogManager.GetLogger();

不要使用这两个方法中的任何一个:

private static readonly ILogger _logger =
    LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static readonly ILogger _logger = 
    LogManager.GetLogger(typeof(YourTypeName));

这实际上等同于第一种选择(即使用MethodBase.GetCurrentMethod().DeclaringType的选择),只是更简单一些。


不错的答案!我认为您的GetLogger覆写应该返回ILogger而不是ILog。有趣的是,使用StackTrace是否会有性能开销,而不是传递类型。 - Dunc
@RaceFace - 修复了 ILogger,谢谢。至于性能,我不确定,但可能会有一点影响...然而,只要你的 ILogger 实例是静态的(通常都是这样),它只会在应用程序中每种类型执行一次,我想这对整体性能来说微不足道。 - Alconja
3
我也使用这种方法。但是当我试图更好地记录通用类型时,Eric Lippert警告我栈帧并不总是可靠的:https://dev59.com/x1vUa4cB1Zd3GeqPwLnN - ErnieL
1
我喜欢这种方法,但是当涉及到检查堆栈跟踪时,我总是过于谨慎。为了保险起见,我在VS中使用代码完成宏来生成GetLogger(typeof(TypeNameHere))。 - cfeduke
你可以使用安全方法 public static ILogger GetLogger(MethodBase method) { return GetLogger(method.DeclaringType); } 来简化调用 LogManager.GetLogger(MethodBase.GetCurrentMethod()); - xmedeko

5
你计划通过编写log4net的包装器获得哪些好处?我建议先熟悉log4net类,然后再编写包装器。cfeduke在他的答案中提到了如何编写这个包装器,但除非你需要向示例中添加实际功能,否则包装器只会使记录日志的过程变慢,并为未来的维护者增加复杂性。当.Net中提供了重构工具时,这一点尤其明显,因为这些工具使进行此类更改变得非常容易。

3
人们“支付这个代价”的原因之一是为了将来更换Log4Net等内容时,成本会更低。一旦你被一个倒闭并且不会公开任何源代码的实现所困扰,你就更容易事先“支付这个代价”。Log4Net可能不属于这一类......但其他内容可能是。 - granadaCoder

1

我已成功将log4net依赖项隔离到单个项目中。如果您打算执行相同操作,请参考我的包装类示例:

using System;

namespace Framework.Logging
{
    public class Logger
    {
        private readonly log4net.ILog _log;

        public Logger()
        {
            _log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        }

        public Logger(string name)
        {
            _log = log4net.LogManager.GetLogger(name);
        }

        public Logger(Type type)
        {
            _log = log4net.LogManager.GetLogger(type);
        }

        public void Debug(object message, Exception ex = null)
        {
            if (_log.IsDebugEnabled)
            {
                if (ex == null)
                {
                    _log.Debug(message);
                }
                else
                {
                    _log.Debug(message, ex);
                }
            }
        }

        public void Info(object message, Exception ex = null)
        {
            if (_log.IsInfoEnabled)
            {
                if (ex == null)
                {
                    _log.Info(message);
                }
                else
                {
                    _log.Info(message, ex);
                }
            }
        }

        public void Warn(object message, Exception ex = null)
        {
            if (_log.IsWarnEnabled)
            {
                if (ex == null)
                {
                    _log.Warn(message);
                }
                else
                {
                    _log.Warn(message, ex);
                }
            }
        }

        public void Error(object message, Exception ex = null)
        {
            if (_log.IsErrorEnabled)
            {
                if (ex == null)
                {
                    _log.Error(message);
                }
                else
                {
                    _log.Error(message, ex);
                }
            }
        }

        public void Fatal(object message, Exception ex = null)
        {
            if (_log.IsFatalEnabled)
            {
                if (ex == null)
                {
                    _log.Fatal(message);
                }
                else
                {
                    _log.Fatal(message, ex);
                }
            }
        }
    }
}

不要忘记在接口项目的AssemblyInfo.cs中添加此内容(我花了好几个小时才找到这个)。

[assembly: log4net.Config.XmlConfigurator(Watch = true, ConfigFile = "log4net.config")]

将你的log4net配置xml文件放在名为log4net.config的文件中,将其设置为ContentCopy Always

你的意思是将这个添加到接口项目的 AssemblyInfo.cs 中,是指包含你的包装器的项目(在你的代码中的这个答案中)吗? - Snoop
1
@StevieV,是的,你说得对。通常它位于您的项目\Properties\AssemblyInfo.cs文件中。 - Jeson Martajaya

1

有一些框架,比如WPF Prism库,它们推广使用一个门面模式来管理所选的日志框架。

下面是一个使用log4net的示例:

using System;
using log4net;
using log4net.Core;
using Prism.Logging;

public class Log4NetLoggerFacade : ILoggerFacade
{
    private static readonly ILog Log4NetLog = LogManager.GetLogger(typeof (Log4NetLoggerFacade));

    public void Log(string message, Category category, Priority priority)
    {
        switch (category)
        {
            case Category.Debug:
                Log4NetLog.Logger.Log(typeof(Log4NetLoggerFacade), Level.Debug, message, null);
                break;
            case Category.Exception:
                Log4NetLog.Logger.Log(typeof(Log4NetLoggerFacade), Level.Error, message, null);
                break;
            case Category.Info:
                Log4NetLog.Logger.Log(typeof(Log4NetLoggerFacade), Level.Info, message, null);
                break;
            case Category.Warn:
                Log4NetLog.Logger.Log(typeof(Log4NetLoggerFacade), Level.Warn, message, null);
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(category), category, null);
        }
    }
}

请注意,通过指定callerStackBoundaryDeclaringType,您仍然可以获得发出日志请求的调用者的类名。您需要做的就是在转换模式中包含%C %M
<layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%date [%thread] %-5level %C.%M - %message%newline" />
</layout>

然而,正如文档所警告的那样,生成调用者类信息是很慢的,因此必须明智地使用它。


1

我的理解是,log4net的包装类应该是一个静态类,它负责从app.config/web.config或代码中初始化日志对象(例如与NUnit集成)。


0
Alconja,我喜欢你使用堆栈跟踪来返回到调用方法的想法。我在考虑进一步封装这些调用,不仅仅是获取日志记录器对象,而是实际执行日志记录。我想要的是一个静态类来处理日志记录,通过抽象出具体使用的实现方式。也就是说,
LoggingService.LogError("my error message");

这样,如果我以后决定使用另一个日志系统,我只需要更改静态类的内部即可。

因此,我使用了您的想法来使用堆栈跟踪获取调用对象:

public static class LoggingService
{
    private static ILog GetLogger()
    {    
        var stack = new StackTrace();    
        var frame = stack.GetFrame(2);    
        return log4net.LogManager.GetLogger(frame.GetMethod().DeclaringType);
    }

    public static void LogError(string message)
    {
        ILog logger = GetLogger();
        if (logger.IsErrorEnabled)
            logger.Error(message);
    }
    ...
}

有人看到这种方法有问题吗?


4
我认为你的代码示例没有理解IsErrorEnabled(以及相关属性)的意义。它们的存在是为了避免运行时构建传递给相应日志方法的字符串的成本。由于你将所有代码都包装在LogError方法中,你失去了这个好处。欲知详情,请参阅http://log4net.sourceforge.net/release/1.2.0.30316/doc/manual/faq.html#fastLogging。 - Richard Ev

0
一个 log4net 包装器的可能用途是通过反射获取调用类和方法来了解日志记录条目发生的位置。至少我经常使用这种方法。

-3

我知道这个答案有点晚,但它可能会帮助到未来的某个人。

听起来你想要一个编程API,XQuiSoft Logging可以提供给你。你不需要指定想要哪个记录器。只需要这样简单:

Log.Write(Level.Verbose, "source", "category", "your message here");

然后通过配置,你可以将消息按照来源、类别、级别或任何其他自定义过滤器定向到不同的位置(文件、电子邮件等)。

请参见this article进行介绍。


这似乎是对其中一个回答的评论,而不是回答本身。 - James A Mohler

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