Apache log4net

最も歴史のあるApache財団の.NETロギングフレームワーク(Java log4jからの移植)。豊富な設定オプションと柔軟性で知られ、複雑な要件に対応可能。多様な出力先とカスタマイズ性を提供する成熟したソリューション。

ロギング.NETC#設定ベースレガシーApache財団アペンダー

ライブラリ

Apache log4net

概要

Apache log4netは「最も歴史のあるApache財団の.NETロギングフレームワーク」として開発された、Java log4jからの移植版ロギングライブラリです。豊富な設定オプションと柔軟性で知られ、複雑な要件に対応可能な成熟したソリューション。多様な出力先とカスタマイズ性を提供し、Enterprise環境での長年の実績を持つ伝統的なフレームワークですが、現代的な.NET開発では他の選択肢が優先される傾向にあります。

詳細

Apache log4net 2025年版では新規プロジェクトでの推奨度が低下しており、構造化ログサポートの限界、メンテナンス性の問題、パフォーマンス面での劣勢により、既存システムの保守でのみ関与する状況となっています。しかし、レガシーシステムでの重要な役割は継続しており、.NET Frameworkとの深い統合、XML設定による詳細制御、豊富なアペンダー(ファイル、データベース、イベントログ等)により、従来のエンタープライズアプリケーションでは依然として使用されています。

主な特徴

  • Java log4j互換設計: Java開発者に馴染みのある設定方式とアーキテクチャ
  • 豊富な設定オプション: XML設定による詳細なログ制御とカスタマイズ
  • 多様なアペンダー: ファイル、データベース、イベントログ、SMTP等への出力
  • レベル別制御: DEBUG、INFO、WARN、ERROR、FATALの階層的ログレベル
  • カスタムフィルター: 条件付きログ出力とメッセージフィルタリング
  • .NET統合: .NET Frameworkとの深い統合とConfiguration API対応

メリット・デメリット

メリット

  • Apache財団による長期サポートと豊富な実績による安定性
  • XML設定ファイルによる外部設定管理と運用時変更対応
  • 多様なアペンダーによる柔軟な出力先設定とカスタマイズ性
  • レガシーシステムとの高い互換性と移行コストの低さ
  • 豊富なドキュメントとコミュニティサポートの蓄積
  • エンタープライズ環境での複雑なログ要件への対応実績

デメリット

  • 構造化ログサポートが限定的で現代的な監視ツールとの統合が困難
  • パフォーマンス面でSerilog、NLog、Microsoft.Extensions.Loggingに劣る
  • 新しい.NET技術(.NET Core/.NET 5+)への対応が不十分
  • 設定の複雑性とメンテナンス性の問題による開発効率低下
  • 非同期処理サポートが限定的で高負荷環境での制約
  • モダンなDevOps実践との統合が困難

参考ページ

書き方の例

インストールと基本セットアップ

<!-- NuGetパッケージ参照 -->
<PackageReference Include="log4net" Version="2.0.17" />
# NuGetパッケージマネージャー
Install-Package log4net

# .NET CLI
dotnet add package log4net
// AssemblyInfo.cs または Global.asax.cs での設定
[assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]

// または App.config / Web.config での設定属性
[assembly: log4net.Config.XmlConfigurator(Watch = true)]

基本的なログ出力

using log4net;
using log4net.Config;
using System;
using System.IO;
using System.Reflection;

public class BasicLoggingExample
{
    // ロガーの取得(クラス名で自動命名)
    private static readonly ILog logger = LogManager.GetLogger(typeof(BasicLoggingExample));

    static void Main(string[] args)
    {
        // 設定ファイルの読み込み
        var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
        XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));

        // 基本的なログレベル別出力
        logger.Debug("デバッグ情報: アプリケーション初期化開始");
        logger.Info("一般情報: アプリケーション開始");
        logger.Warn("警告: 設定ファイルにデフォルト値を使用");
        logger.Error("エラー: データベース接続失敗", new Exception("Connection timeout"));
        logger.Fatal("致命的エラー: システム停止");

        // フォーマット済みメッセージ
        string userId = "user123";
        int transactionId = 98765;
        logger.InfoFormat("取引処理完了: ユーザー={0}, 取引ID={1}, 金額={2:C}", 
                         userId, transactionId, 15000);

        // 条件付きログ出力(パフォーマンス考慮)
        if (logger.IsDebugEnabled)
        {
            string expensiveData = GetExpensiveDebugInfo();
            logger.DebugFormat("重い処理結果: {0}", expensiveData);
        }

        // 例外の詳細ログ
        try
        {
            ProcessData();
        }
        catch (Exception ex)
        {
            logger.Error("データ処理エラー", ex);
            throw;
        }

        logger.Info("アプリケーション終了");
    }

    private static string GetExpensiveDebugInfo()
    {
        // 重い処理のシミュレーション
        System.Threading.Thread.Sleep(100);
        return $"デバッグ情報: {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
    }

    private static void ProcessData()
    {
        // 業務処理のシミュレーション
        logger.Debug("データ処理開始");
        
        // 意図的にエラーを発生
        throw new InvalidOperationException("サンプルエラー");
    }
}

XML設定ファイル(log4net.config)による詳細設定

<?xml version="1.0" encoding="utf-8"?>
<log4net>
  <!-- アペンダー定義 -->
  
  <!-- コンソールアペンダー -->
  <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %message%newline" />
    </layout>
    <threshold value="INFO" />
  </appender>

  <!-- ファイルアペンダー(ローリング) -->
  <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
    <file value="logs/application.log" />
    <appendToFile value="true" />
    <rollingStyle value="Composite" />
    <datePattern value="yyyyMMdd" />
    <maxSizeRollBackups value="30" />
    <maximumFileSize value="10MB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date{yyyy-MM-dd HH:mm:ss.fff} [%thread] %-5level %logger{2} - %message%newline" />
    </layout>
  </appender>

  <!-- エラー専用ファイルアペンダー -->
  <appender name="ErrorFileAppender" type="log4net.Appender.RollingFileAppender">
    <file value="logs/error.log" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="10" />
    <maximumFileSize value="5MB" />
    <staticLogFileName value="true" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date{yyyy-MM-dd HH:mm:ss.fff} [%thread] %-5level %logger - %message%newline%exception" />
    </layout>
    <threshold value="ERROR" />
  </appender>

  <!-- データベースアペンダー -->
  <appender name="DatabaseAppender" type="log4net.Appender.AdoNetAppender">
    <bufferSize value="100" />
    <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <connectionString value="Data Source=localhost;Initial Catalog=LogDB;Integrated Security=True" />
    <commandText value="INSERT INTO Log ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" />
    
    <parameter>
      <parameterName value="@log_date" />
      <dbType value="DateTime" />
      <layout type="log4net.Layout.RawTimeStampLayout" />
    </parameter>
    <parameter>
      <parameterName value="@thread" />
      <dbType value="String" />
      <size value="255" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%thread" />
      </layout>
    </parameter>
    <parameter>
      <parameterName value="@log_level" />
      <dbType value="String" />
      <size value="50" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%level" />
      </layout>
    </parameter>
    <parameter>
      <parameterName value="@logger" />
      <dbType value="String" />
      <size value="255" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%logger" />
      </layout>
    </parameter>
    <parameter>
      <parameterName value="@message" />
      <dbType value="String" />
      <size value="4000" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%message" />
      </layout>
    </parameter>
    <parameter>
      <parameterName value="@exception" />
      <dbType value="String" />
      <size value="2000" />
      <layout type="log4net.Layout.ExceptionLayout" />
    </parameter>
    
    <threshold value="WARN" />
  </appender>

  <!-- SMTPアペンダー(重要なエラー通知) -->
  <appender name="SmtpAppender" type="log4net.Appender.SmtpAppender">
    <to value="[email protected]" />
    <from value="[email protected]" />
    <subject value="[ERROR] アプリケーションエラー" />
    <smtpHost value="smtp.example.com" />
    <port value="587" />
    <username value="[email protected]" />
    <password value="password123" />
    <enableSsl value="true" />
    <bufferSize value="10" />
    <threshold value="ERROR" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %message%newline%exception" />
    </layout>
  </appender>

  <!-- イベントログアペンダー -->
  <appender name="EventLogAppender" type="log4net.Appender.EventLogAppender">
    <applicationName value="MyApplication" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %message" />
    </layout>
    <threshold value="WARN" />
  </appender>

  <!-- ロガー設定 -->
  
  <!-- 特定名前空間の詳細ログ -->
  <logger name="MyApplication.Services">
    <level value="DEBUG" />
    <appender-ref ref="RollingFileAppender" />
  </logger>

  <!-- データアクセス層のログ -->
  <logger name="MyApplication.DataAccess">
    <level value="TRACE" />
    <appender-ref ref="RollingFileAppender" />
  </logger>

  <!-- フレームワークログの制限 -->
  <logger name="System" additivity="false">
    <level value="WARN" />
    <appender-ref ref="ConsoleAppender" />
  </logger>

  <!-- ルートロガー -->
  <root>
    <level value="INFO" />
    <appender-ref ref="ConsoleAppender" />
    <appender-ref ref="RollingFileAppender" />
    <appender-ref ref="ErrorFileAppender" />
    <appender-ref ref="DatabaseAppender" />
    <appender-ref ref="SmtpAppender" />
    <appender-ref ref="EventLogAppender" />
  </root>
</log4net>

カスタムアペンダーと高度な設定

// カスタムアペンダーの実装例
using log4net.Appender;
using log4net.Core;
using System;
using System.IO;
using Newtonsoft.Json;

public class JsonFileAppender : AppenderSkeleton
{
    public string FileName { get; set; }
    private FileStream fileStream;
    private StreamWriter writer;

    public override void ActivateOptions()
    {
        base.ActivateOptions();
        
        // ファイルストリームの初期化
        var directory = Path.GetDirectoryName(FileName);
        if (!Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }
        
        fileStream = new FileStream(FileName, FileMode.Append, FileAccess.Write, FileShare.Read);
        writer = new StreamWriter(fileStream);
    }

    protected override void Append(LoggingEvent loggingEvent)
    {
        // JSON形式でログを出力
        var logEntry = new
        {
            Timestamp = loggingEvent.TimeStamp,
            Level = loggingEvent.Level.ToString(),
            Logger = loggingEvent.LoggerName,
            Message = loggingEvent.RenderedMessage,
            Exception = loggingEvent.ExceptionObject?.ToString(),
            Thread = loggingEvent.ThreadName,
            Username = loggingEvent.UserName,
            Properties = loggingEvent.Properties
        };

        string jsonLog = JsonConvert.SerializeObject(logEntry);
        writer.WriteLine(jsonLog);
        writer.Flush();
    }

    protected override void OnClose()
    {
        writer?.Close();
        fileStream?.Close();
        base.OnClose();
    }
}

// カスタムアペンダーの使用例
public class CustomAppenderExample
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(CustomAppenderExample));

    static void Main(string[] args)
    {
        // プログラマティック設定
        ConfigureLog4Net();

        // コンテキスト情報の設定
        log4net.GlobalContext.Properties["Environment"] = "Production";
        log4net.GlobalContext.Properties["Version"] = "1.0.0";

        // スレッドコンテキスト情報
        log4net.ThreadContext.Properties["UserId"] = "user123";
        log4net.ThreadContext.Properties["SessionId"] = "session456";

        // ログ出力
        logger.Info("カスタムアペンダーテスト開始");
        logger.Warn("警告テスト: メモリ使用量が高いです");
        logger.Error("エラーテスト", new Exception("テスト例外"));

        // MDC(Mapped Diagnostic Context)の使用
        log4net.ThreadContext.Properties["TransactionId"] = "tx789";
        logger.Info("取引処理実行");
        log4net.ThreadContext.Properties.Remove("TransactionId");

        logger.Info("カスタムアペンダーテスト完了");
    }

    private static void ConfigureLog4Net()
    {
        // プログラマティック設定の例
        var hierarchy = (log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository();
        
        // パターンレイアウト
        var patternLayout = new log4net.Layout.PatternLayout
        {
            ConversionPattern = "%date{yyyy-MM-dd HH:mm:ss.fff} [%thread] %-5level %logger - %message%newline"
        };
        patternLayout.ActivateOptions();

        // カスタムJSONアペンダー
        var jsonAppender = new JsonFileAppender
        {
            Name = "JsonFileAppender",
            FileName = "logs/application.json"
        };
        jsonAppender.ActivateOptions();

        // ロガー設定
        hierarchy.Root.AddAppender(jsonAppender);
        hierarchy.Root.Level = log4net.Core.Level.Info;
        hierarchy.Configured = true;
    }
}

ASP.NET統合とWebアプリケーション対応

// Global.asax.cs
using log4net;
using log4net.Config;
using System;
using System.IO;
using System.Web;

public class Global : HttpApplication
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(Global));

    protected void Application_Start(object sender, EventArgs e)
    {
        // log4net設定の初期化
        XmlConfigurator.Configure(new FileInfo(Server.MapPath("~/log4net.config")));
        
        logger.Info("Webアプリケーション開始");
    }

    protected void Application_Error(object sender, EventArgs e)
    {
        Exception exception = Server.GetLastError();
        string url = HttpContext.Current.Request.Url.ToString();
        string userAgent = HttpContext.Current.Request.UserAgent;
        string userIP = HttpContext.Current.Request.UserHostAddress;

        logger.Error($"未処理例外発生: URL={url}, UserAgent={userAgent}, IP={userIP}", exception);
    }

    protected void Session_Start(object sender, EventArgs e)
    {
        string sessionId = Session.SessionID;
        string userIP = Request.UserHostAddress;
        
        logger.Info($"新しいセッション開始: SessionID={sessionId}, IP={userIP}");
    }

    protected void Application_End(object sender, EventArgs e)
    {
        logger.Info("Webアプリケーション終了");
        LogManager.Shutdown();
    }
}

// コントローラーまたはページでの使用例
public partial class UserManager : System.Web.UI.Page
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(UserManager));

    protected void Page_Load(object sender, EventArgs e)
    {
        // リクエスト情報のログ
        string sessionId = Session.SessionID;
        string userIP = Request.UserHostAddress;
        string userAgent = Request.UserAgent;

        // MDC設定
        log4net.ThreadContext.Properties["SessionId"] = sessionId;
        log4net.ThreadContext.Properties["UserIP"] = userIP;

        logger.Info($"ページ読み込み: {Request.Url}");

        try
        {
            LoadUserData();
        }
        catch (Exception ex)
        {
            logger.Error("ユーザーデータ読み込みエラー", ex);
            // エラーページへのリダイレクト等
        }
        finally
        {
            // MDCのクリア
            log4net.ThreadContext.Properties.Clear();
        }
    }

    private void LoadUserData()
    {
        logger.Debug("ユーザーデータ取得開始");
        
        // データ取得処理
        System.Threading.Thread.Sleep(100); // 処理のシミュレーション
        
        logger.Info("ユーザーデータ取得完了: 件数=150");
    }

    protected void btnSave_Click(object sender, EventArgs e)
    {
        string userId = Request["userId"];
        
        // 操作ログ
        log4net.ThreadContext.Properties["UserId"] = userId;
        log4net.ThreadContext.Properties["Action"] = "Save";

        try
        {
            logger.Info("ユーザー保存処理開始");
            
            // 保存処理のシミュレーション
            SaveUserData(userId);
            
            logger.Info("ユーザー保存処理完了");
        }
        catch (Exception ex)
        {
            logger.Error("ユーザー保存処理エラー", ex);
            throw;
        }
        finally
        {
            log4net.ThreadContext.Properties.Clear();
        }
    }

    private void SaveUserData(string userId)
    {
        logger.Debug($"ユーザーデータ保存: UserID={userId}");
        
        // 実際の保存処理
        if (string.IsNullOrEmpty(userId))
        {
            throw new ArgumentException("UserIDが指定されていません");
        }
        
        // データベース保存処理のシミュレーション
        System.Threading.Thread.Sleep(200);
        
        logger.Info($"ユーザーデータ保存成功: UserID={userId}");
    }
}

パフォーマンス考慮とベストプラクティス

// パフォーマンス最適化の例
public class PerformanceOptimizedLogging
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(PerformanceOptimizedLogging));

    public static void Main(string[] args)
    {
        // パフォーマンステスト
        PerformanceTest();
        
        // メモリ使用量テスト
        MemoryUsageTest();
        
        // 非同期処理模擬
        AsyncProcessingSimulation();
    }

    private static void PerformanceTest()
    {
        logger.Info("パフォーマンステスト開始");
        
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        int logCount = 10000;

        for (int i = 0; i < logCount; i++)
        {
            // 条件チェックによる最適化
            if (logger.IsDebugEnabled)
            {
                logger.Debug($"デバッグログ: 反復={i}");
            }

            // INFO以上の場合のみ文字列フォーマット実行
            if (logger.IsInfoEnabled)
            {
                logger.InfoFormat("情報ログ: 反復={0}, タイムスタンプ={1}", i, DateTime.Now);
            }

            // 重いログは条件付きで
            if (i % 1000 == 0 && logger.IsWarnEnabled)
            {
                logger.WarnFormat("進捗ログ: {0}/{1} 完了", i, logCount);
            }
        }

        stopwatch.Stop();
        double throughput = logCount / stopwatch.Elapsed.TotalSeconds;
        
        logger.InfoFormat("パフォーマンステスト完了: 時間={0}ms, スループット={1:F0} logs/sec", 
                         stopwatch.ElapsedMilliseconds, throughput);
    }

    private static void MemoryUsageTest()
    {
        logger.Info("メモリ使用量テスト開始");
        
        long startMemory = GC.GetTotalMemory(true);
        
        // 大量のログ出力
        for (int i = 0; i < 5000; i++)
        {
            logger.InfoFormat("メモリテスト: オブジェクト={0}", new { Id = i, Name = $"Object{i}" });
        }
        
        long endMemory = GC.GetTotalMemory(true);
        long memoryDiff = endMemory - startMemory;
        
        logger.InfoFormat("メモリ使用量: 開始={0:N0} bytes, 終了={1:N0} bytes, 差分={2:N0} bytes", 
                         startMemory, endMemory, memoryDiff);
    }

    private static void AsyncProcessingSimulation()
    {
        logger.Info("非同期処理シミュレーション開始");
        
        // 複数スレッドでのログ出力
        var tasks = new System.Threading.Tasks.Task[5];
        
        for (int taskId = 0; taskId < tasks.Length; taskId++)
        {
            int capturedTaskId = taskId;
            tasks[taskId] = System.Threading.Tasks.Task.Run(() =>
            {
                // スレッドローカル情報設定
                log4net.ThreadContext.Properties["TaskId"] = capturedTaskId;
                
                for (int i = 0; i < 1000; i++)
                {
                    logger.InfoFormat("タスク{0}: 処理{1}", capturedTaskId, i);
                    
                    if (i % 100 == 0)
                    {
                        logger.DebugFormat("タスク{0}: 進捗 {1}%", capturedTaskId, i / 10);
                    }
                }
                
                logger.InfoFormat("タスク{0}: 完了", capturedTaskId);
                log4net.ThreadContext.Properties.Clear();
            });
        }
        
        System.Threading.Tasks.Task.WaitAll(tasks);
        logger.Info("非同期処理シミュレーション完了");
    }
}

// 設定によるパフォーマンス最適化
/*
Web.config での最適化設定例:

<appSettings>
  <!-- log4netの内部デバッグを無効化 -->
  <add key="log4net.Internal.Debug" value="false" />
</appSettings>

<log4net>
  <!-- 非同期アペンダーの使用 -->
  <appender name="AsyncAppender" type="log4net.Appender.AsyncAppender">
    <bufferSize value="1000" />
    <appender-ref ref="RollingFileAppender" />
  </appender>
  
  <!-- バッファリング設定 -->
  <appender name="BufferedFileAppender" type="log4net.Appender.BufferingForwardingAppender">
    <bufferSize value="100" />
    <appender-ref ref="RollingFileAppender" />
  </appender>
</log4net>
*/