技術memo

関数型ゴースト

ログ出力ライブラリlog4netを独自クラスでラップする(その他使い方メモ)

目的

  • log4netのインターフェースが何だか使いにくい、独自の関数でログに書き込みたい
    • でも自作クラスでラップすると%location等のlog4netで出力する「書き込み場所」情報が全部独自クラスになってしまう
  • エラー処理等、同じようなことを何度も作りたくない
  • 設定ファイルの書き方がよくわからない、すぐ忘れる

等の理由から、ひととおり定型パターンのオレオレ実装を書き残しておくことにします。 基本想定バージョンは.NET3.5以降、C# 3.0以降です。

準備

最低限としては

  • app.configファイルに以下を追加
<configuration>
  <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
  <!-- 他、その他設定内容が続く -->
</configuration>
  • exeで動かすアプリならAssemblyInfo.csに以下を追加
[assembly: log4net.Config.XmlConfigurator(ConfigFile = @"Log4net.Config.xml", Watch = true)]

このあたりを済ませておけば動くはずです。詳説は既存記事に任せます。

設定

log4net.config.xmlのサンプル。 使い勝手が良いので日付ごとのファイル出力とします。他はググってどうぞ。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <log4net>
    <appender name="MyLogAppender" type="log4net.Appender.RollingFileAppender">
      <File value="C:\log\log_" />
      <datePattern value='yyyyMMdd".txt"' />
      <appendToFile value="true" />
      <rollingStyle value="date" />
      <staticLogFileName value="false" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="[%date %-5level %location]%message%newline" />
      </layout>
      <errorHandler type="MyApplication.Log.LogErrorHandler" />
      <param name="Threshold" value="INFO" />
    </appender>
    <logger name="MyLogger">
      <appender-ref ref="MyLogAppender" />
    </logger>
    <root>
      <level value="ALL" />
      <appender-ref ref="MyLogAppender" />
    </root>
  </log4net>
</configuration>

LoggerのNameを指定すると、出力内容別にファイルを分けるなどのコーディングができますが(LogManager.GetLogger("MyLogger")としてILogを取得する)、今回は省略します。よって今回のところlogger要素はなくてもかまいません。

目的によってはappenderを増やしたりしても大丈夫です。その場合はroot内のappender-refも一緒に増やしましょう。

プログラム

実装方針としては以下

  • ログ出力インターフェースは、独自のログ内容データを作って渡すことにします
  • ログ出力エラー時には適当な例外を投げるものとします
  • IMyLog(+ MyLogのIMyLog実装)、LogDataの内容は適当なサンプルです
using System;
using System.Linq;
using log4net;
using log4net.Appender;
using log4net.Core;

namespace MyApplication.Log
{
    public class LogData : ILogData
    {
        public string Message { set; get; }
        public Exception Error { set; get; }
        public string UserId { set; get; }
        public string GetMessage()
        {
            var a = "ログインユーザー:" + UserId + "メッセージ:" + Message;
            if (Error != null) return a + " エラー:" + Error.ToString();
            return a;
        }
    }

    public interface ILogData
    {
        string GetMessage();
        Exception Error { get; }
    }

    public interface IMyLog
    {
        void Debug(ILogData data);
        void Info(ILogData data);
        void Warn(ILogData data);
        void Error(ILogData data);
        void Fatal(ILogData data);
        void Log(Level level, ILogData data);
    }

    public class MyLog : IMyLog
    {
        private readonly ILog log;
        public MyLog()
        {
            log = LogManager.GetLogger(typeof(MyLog));
            foreach (var x in LogManager.GetAllRepositories()
                 .SelectMany(x => x.GetAppenders())
                 .OfType<AppenderSkeleton>()
                 .Select(x => x.ErrorHandler)
                 .OfType<LogErrorHandler>())
            {
                x.ErrorOccurred += ErrorOccurred;
            }
        }
        private void ErrorOccurred(LogErrorData e)
        {
            throw new LogException(e);
        }
        void IMyLog.Debug(ILogData data)
        {
            Write(Level.Debug, data);
        }
        void IMyLog.Info(ILogData data)
        {
            Write(Level.Info, data);
        }
        void IMyLog.Warn(ILogData data)
        {
            Write(Level.Warn, data);
        }
        void IMyLog.Error(ILogData data)
        {
            Write(Level.Error, data);
        }
        void IMyLog.Fatal(ILogData data)
        {
            Write(Level.Fatal, data);
        }
        void IMyLog.Log(Level level, ILogData data)
        {
            Write(level, data);
        }
        private void Write(Level level, ILogData data)
        {
            if (!log.Logger.IsEnabledFor(level))
            {
                return;
            }
            var text = data.GetMessage();
            log.Logger.Log(typeof(MyLog), level, text, data.Error);
        }
    }

    public class LogErrorHandler : IErrorHandler
    {
        public event Action<LogErrorData> ErrorOccurred = null;
        private void OnErrorOccurred(LogErrorData data)
        {
            if (ErrorOccurred == null)
            {
                return;
            }
            ErrorOccurred(data);
        }
        public void Error(string message)
        {
            this.OnErrorOccurred(new LogErrorData(message, null, null));
        }
        public void Error(string message, Exception e)
        {
            this.OnErrorOccurred(new LogErrorData(message, e, null));
        }
        public void Error(string message, Exception e, ErrorCode errorCode)
        {
            OnErrorOccurred(new LogErrorData(message, e, errorCode));
        }
    }

    public class LogErrorData
    {
        public string Message { get; private set; }
        public Exception Error { get; private set; }
        public ErrorCode? ErrorCode { get; private set; }
        public LogErrorData(string message, Exception exception, ErrorCode? errorCode)
        {
            this.Message = message;
            this.Error = exception;
            this.ErrorCode = errorCode;
        }
    }

    public class LogException : Exception
    {
        public new LogErrorData Data{get;private set;}
        public LogException(LogErrorData data)
            :base(data.Message, data.Error)
        {
            this.Data = data;
        }
    }
}

MyLog.Writeメソッドが注目点です

  • 対象ログレベルのログが無効ならGetMessageを呼ばない(無駄な文字列結合等を避ける)
    • 実際にはGetMessageの内容がどの程度「気になる」程の複雑さになっているかによりますが……
  • ILog.Logger.Logメソッドを使用することで、「書き込み場所情報が全部独自クラスになってしまう」問題に対応

また、GetMessage内でExceptionの内容をログ出力メッセージとして追加しなくても、log4netの出力メソッドに例外を渡すと、自動で例外の内容が出力されます。二重に出力されるとうっとうしいので注意。

使ってみる

using System;
using MyApplication.Log;

namespace MyApplication
{
    public static class Program
    {
        static void Main(string[] args)
        {
            IMyLog log = new MyLog();
            var userId = "administrator";
            log.Info(new LogData { Message = "いんふぉ", UserId = userId });
            log.Error(new LogData { Message = "えらー", UserId = userId, Error = new InvalidOperationException("不正な操作が実行されました!") });
        }
    }
}

実際のシチュエーションでは、IMyLog型変数はシングルトンにして初めに初期化するなどすればよいと思います。

参考