log4net で正規表現を使ってログメッセージを置換する

log4net を使っていて、アプリ側ではなく log4net 側でメッセージの一部を置換できたら便利だなーと思いましてちょっと試してみました。 例えばパスワードみたいな機密情報とか、改行やタブとかね。

アプリ側の実装

ログを出力するアプリはこんな感じに適当に。

class Program
{
    private static readonly ILog Logger = LogManager.GetLogger(typeof(Program));

    static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();

        Logger.Info("start logging.");

        Logger.Info("test message."); // ここを置換する

        Logger.Info("finish logging.");
    }
}

構成ファイルはこんな感じで。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type=" log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
  </configSections>
  <log4net>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="[%date{yyyy/MM/dd}][%3thread][%-5level] %message%newline" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
      <appender-ref ref="ConsoleAppender" />
    </root>
  </log4net>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

これを普通に実行すると

[2014/10/03][ 1][INFO ] start logging.
[2014/10/03][ 1][INFO ] test message.
[2014/10/03][ 1][INFO ] finish logging.

といった感じに出力されます。今回は、2 行目のメッセージを置換することが目標です。
それもできれば、正規表現で。置換後の文字列も指定できたら嬉しいですね。

決まったテキストを置換

まずは簡単に特定のキーワードを置換するものを、PatternConverter を使って作ってみます。

public class MessageReplacePatternConverter : PatternConverter
{
    private const string pattern = "test";
    private const string replacedText = "replaced text";

    protected override void Convert(TextWriter writer, object state)
    {
        var loggingEvent = state as log4net.Core.LoggingEvent;
        if (loggingEvent == null) return;
        writer.Write(loggingEvent.RenderedMessage.Replace(pattern, replacedText));
    }
}

構成ファイルはこんな感じです。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type=" log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
  </configSections>
  <log4net>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout">
        <!-- value を変更 -->
        <conversionPattern value="[%date{yyyy/MM/dd}][%3thread][%-5level] %replaced_message%newline" />
        <!-- ↓ を追加 -->
        <converter>
          <name value="replaced_message" />
          <type value="ConsoleApplication3.MessageReplacePatternConverter" />
        </converter>
      </layout>
    </appender>
    <root>
      <level value="ALL" />
      <appender-ref ref="ConsoleAppender" />
    </root>
  </log4net>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

これを実行すると、以下のように出力されます。

[2014/10/03][ 1][INFO ] start logging.
[2014/10/03][ 1][INFO ] replaced text message.
[2014/10/03][ 1][INFO ] finish logging.

期待する出力が得られました。簡単ですね。

正規表現でテキストを置換

ただ、特定のキーワードだけ置換してもあまり使いどころがないです。
当初の目的である、正規表現を使った置換をできるようにしてみます。

ここでも PatternConverter を使いたい、、、ところなんですが出来ませんでした。

log4net では、構成ファイルに

<param name="Property Name(e.g. Pattern)" value="Property Value(e.g. ^test\b)" />

のような設定を追加してあげると、自動でカスタムクラスのプロパティに値をセットしてくれる機能があります。
ここに正規表現のパターン文字列を指定できるようにしたかったのですが、例えばさっきのクラスをこんな感じに

public class MessageReplacePatternConverter : PatternConverter
{
    public string Pattern { get; set; } // const を削除してこの行追加

    protected override void Convert(TextWriter writer, object state)
    {
        var loggingEvent = state as log4net.Core.LoggingEvent;
        if (loggingEvent == null) return;
        writer.Write(loggingEvent.RenderedMessage.Replace(pattern, replacedText));
    }
}

してあげても、

log4net:ERROR XmlHierarchyConfigurator: Cannot find Property [Pattern] to set object on [log4net.Util.ConverterInfo]

というエラーが出て、値がセットされません。

頑張って調べましたが Converter に自動で値をセットする方法が分からなかったので、ここでは log4net 標準の PatternLayout を拡張して実現することにします。

public class MessageReplaceablePatternLayout : PatternLayout
{
    public string Pattern { get; set; }
    public string NewValue { get; set; }

    public override void Format(TextWriter writer, LoggingEvent loggingEvent)
    {
        var eventData = loggingEvent.GetLoggingEventData();
        var regex = new Regex(Pattern);
        eventData.Message = regex.Replace(eventData.Message, NewValue);
        base.Format(writer, new LoggingEvent(eventData));
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type=" log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
  </configSections>
  <log4net>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <!-- ↓ の type を変更 -->
      <layout type="ConsoleApplication3.MessageReplaceablePatternLayout">
        <!-- ↓ を追加 -->
        <param name="Pattern" value="^test\b" />
        <param name="NewValue" value="replaced value" />
        <!-- ↓ の value を変更(元に戻した) -->
        <conversionPattern value="[%date{yyyy/MM/dd}][%3thread][%-5level] %message%newline" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
      <appender-ref ref="ConsoleAppender" />
    </root>
  </log4net>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

実行すると以下が出力されます。

[2014/10/03][ 1][INFO ] start logging.
[2014/10/03][ 1][INFO ] replaced value message.
[2014/10/03][ 1][INFO ] finish logging.

正規表現を使って、期待通り以下の出力が得られるようになりました。