Log4Net のラッパーをつくる

備忘録の意味も込めて。
やりたいことは、

  • Debug 系メソッドはリリースモードでは呼出しごと削除したい
  • いちいち LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); って書くのだるいから省略したい

の 2 つ。

Debug 系のメソッドをリリースモードで呼出しごと削除する

これは、ConditionalAttribute を使えば出来そうだと軽く考えていたんだけど、この属性、インターフェイスやら抽象メソッドやらには指定できないらしい・・・
まぁ、当たり前っちゃぁ当たり前なんだけど・・・


で、partial メソッドもなんかそれっぽいことに使えそう・・・と、思ったんだけど、こっちも制限が厳しすぎて実現不可能・・・
これも当たり前なんだけどね・・・


で、結局、ObsoluteAttribute と pragma と拡張メソッドを組み合わせることに。
以下実装例。

public interface ILogger
{
    void Error(object message, Exception e);
    void Error(object message);
    void ErrorFormat(string format, params object[] args);
    // Debug以外のメソッド
    // ...
    
    // Debug系のメソッドにはObsoluteAttributeを付け、
    // さらに名前も「いかにも使っちゃ駄目そう」なメソッドに。
    [Obsolute]
    void XXX_Debug(object message, Exception e);
    [Obsolute]
    void XXX_Debug(object message);
    [Obsolute]
    void XXX_DebugFormat(string format, params object[] args);
}

// Obsoluteなメソッドを使用した際の警告を一時的に無効に。
#pragma warning disable 612
public static class ILoggerExtension
{
    // 拡張メソッドはただのstaticメソッドなので、ConditionalAttributeが指定できる
    [Conditional("DEBUG")]
    public static void Debug(this ILogger self, object message, Exception e)
    {
        self.XXX_Debug(message, e);
    }
    
    [Conditional("DEBUG")]
    public static void Debug(this ILogger self, object message)
    {
        self.XXX_Debug(message);
    }
    
    [Conditional("DEBUG")]
    public static void DebugFormat(this ILogger self, string format, params object[] args)
    {
        self.XXX_DebugFormat(format, args);
    }
}
#pragma warning restore 612
// なんの面白みもない実装クラス
internal sealed class Logger : ILogger
{
    internal readonly log4net.ILog logger;
    
    // 以下ILoggerの各メソッドをLog4Netの各メソッドに転送
    // ...
}

これで、例えばこんな呼出し

log.Debug(Hoge());

は、デバッグモードでビルドした場合は Hoge メソッドが呼び出され、その結果がログに書き込まれるけど、リリースモードでビルドした場合は Hoge メソッドの呼出しすらなくなる。
なので、Debug 系のメソッドでは効率を気にせずに文字列結合やりまくったりしても全然平気!


XXX_Debug やら変なメソッドが気になるけど、これを使ったら警告出るし、そこは妥協すると言うことで。

LogManager.GetLogger に自動で自身の Type オブジェクトを渡す

生の Log4Net の場合、ロガーを取得するためには

static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

こんな感じのコードになる。
オマジナイにしては長いうえ、System.Reflection を using するのは・・・という場合、更に長く、

static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

さすがにこれをいちいち書くのはだるい・・・よね?


ということで、JIT 最適化にも負けずに呼び出し元のメソッドを取得する方法を参考に (というかそのまま)、こんな実装。

public static class LoggerPool
{
    [DynamicSecurityMethod]
    public static ILogger GetLogger()
    {
        const int callerFrameIndex = 1;
        StackFrame callerFrame = new StackFrame(callerFrameIndex);
        MethodBase callerMethod = callerFrame.GetMethod();
        return new Logger(LogManager.GetLogger(callerMethod.DeclaringType));
    }
}

namespace System.Security
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
    internal sealed class DynamicSecurityMethodAttribute : Attribute
    {
    }
}

これで、ロガーの取得は

static readonly ILogger logger = LoggerPool.GetLogger();

これだけ。

オマケ:拡張メソッドでアプリケーション独自の機能を追加する

例えば、アプリケーションによっては「ユーザー ID をログに含めたいんだ!」って要件とかもあると思う。
そんな場合でも、拡張メソッドを使えば簡単にこれが実現できる。

public static void ErrorWithUID(this ILogger self, object message)
{
    var uid = // ユーザーIDを取得
    self.ErrorFormat("UID:{0}, MESSAGE:{1}", uid, message);
}

などなど、比較的簡単に独自機能が追加できる。