Apache Log4j2

Apache財団によるLog4jの後継となる高性能ロギングフレームワーク。ガベージフリーロギング、非同期ロガー、ラムダ式による遅延評価をサポート。ロックフリーデータ構造により低レイテンシシステムに最適化されている。

ロギングJava高性能ガベージフリー非同期ラムダ式低レイテンシ

GitHub概要

apache/logging-log4j2

Apache Log4j is a versatile, feature-rich, efficient logging API and backend for Java.

スター3,538
ウォッチ107
フォーク1,691
作成日:2013年6月12日
言語:Java
ライセンス:Apache License 2.0

トピックス

apacheapijavajvmlibrarylog4jlog4j2loggerloggingsyslog

スター履歴

apache/logging-log4j2 Star History
データ取得日時: 2025/10/22 04:10

ライブラリ

Apache Log4j2

概要

Apache Log4j2は「Apache財団によるLog4jの後継となる高性能ロギングフレームワーク」として開発された、Javaエコシステム最速の先進的ロギングソリューションです。ガベージフリーロギング、非同期ロガー、ラムダ式による遅延評価をサポートし、ロックフリーデータ構造により低レイテンシシステムに最適化。プラグインアーキテクチャによる拡張性と、API・実装の分離設計により、エンタープライズアプリケーションでの地位を確立しています。

詳細

Apache Log4j2 2025年版は最速で最も先進的なJavaロギングフレームワークとして評価され、高スループットアプリケーションでの採用が急増中です。ガベージコレクション圧力軽減機能とマルチスレッド処理能力により、エンタープライズシステムでの地位を確立。SLF4J互換性、自動設定リロード、高度なフィルタリング機能を提供し、Log4j 1.xからの大幅な進化を実現。豊富なアペンダー(ファイル、データベース、Kafka、NoSQL等)とレイアウト(JSON、XML、CSV等)により、多様な出力要件に対応可能です。

主な特徴

  • ガベージフリーロギング: メモリアロケーション最小化による超低レイテンシ
  • 非同期ロガー: ロックフリー構造による高スループット処理
  • ラムダ式サポート: パフォーマンス向上のための遅延評価
  • プラグインアーキテクチャ: カスタムコンポーネントの柔軟な追加
  • 自動設定リロード: ランタイム設定変更の無停止適用
  • 高度なフィルタリング: スクリプト、正規表現、レート制限等

メリット・デメリット

メリット

  • Java最速のロギング性能とガベージフリー動作による超低レイテンシ
  • 非同期処理とマルチスレッド最適化による高スループット実現
  • 豊富なアペンダーとレイアウトによる多様な出力先への対応
  • プラグインアーキテクチャによる高い拡張性とカスタマイズ性
  • 自動設定リロード機能による運用時の柔軟な設定変更
  • Apache財団の継続的メンテナンスによる長期サポート保証

デメリット

  • 高機能ゆえの設定複雑性と学習コストの増大
  • 小規模アプリケーションには過度な機能と依存関係
  • ガベージフリー機能の効果的な活用には専門知識が必要
  • 他のログフレームワークからの移行コストが高い
  • プラグイン開発には深いフレームワーク理解が必要
  • 設定ミスによるパフォーマンス劣化のリスク

参考ページ

書き方の例

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

<!-- Maven依存関係 -->
<dependencies>
    <!-- Log4j2 API -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.23.1</version>
    </dependency>
    
    <!-- Log4j2 実装 -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.23.1</version>
    </dependency>
    
    <!-- SLF4J統合(オプション) -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j2-impl</artifactId>
        <version>2.23.1</version>
    </dependency>
    
    <!-- 非同期ロガー用 -->
    <dependency>
        <groupId>com.lmax</groupId>
        <artifactId>disruptor</artifactId>
        <version>3.4.4</version>
    </dependency>
</dependencies>
// Gradle依存関係
dependencies {
    implementation 'org.apache.logging.log4j:log4j-api:2.23.1'
    implementation 'org.apache.logging.log4j:log4j-core:2.23.1'
    implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.23.1'
    implementation 'com.lmax:disruptor:3.4.4'
}

基本的なログ出力

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class BasicLoggingExample {
    // ロガーの取得(クラス名で自動命名)
    private static final Logger logger = LogManager.getLogger(BasicLoggingExample.class);
    
    public static void main(String[] args) {
        // 基本的なログレベル別出力
        logger.trace("最詳細レベル: メソッド入出力時のトレース");
        logger.debug("デバッグ情報: 変数値={}, 状態={}", "test", "active");
        logger.info("一般情報: アプリケーション開始");
        logger.warn("警告: メモリ使用量が80%を超過 - {}MB", 1200);
        logger.error("エラー: データベース接続失敗", new RuntimeException("Connection timeout"));
        logger.fatal("致命的エラー: システム停止");
        
        // パラメータ化メッセージ(高効率)
        String userId = "user123";
        int transactionId = 98765;
        logger.info("取引処理完了: ユーザー={}, 取引ID={}, 金額=¥{}", 
                   userId, transactionId, 15000);
        
        // ラムダ式による遅延評価(ログレベルが無効な場合は実行されない)
        logger.debug("重い処理結果: {}", () -> expensiveOperation());
        
        // マーカーを使用した分類
        org.apache.logging.log4j.Marker marker = 
            org.apache.logging.log4j.MarkerManager.getMarker("AUDIT");
        logger.info(marker, "監査ログ: ユーザー{}がアクション{}を実行", userId, "LOGIN");
    }
    
    private static String expensiveOperation() {
        // 重い処理のシミュレーション
        try {
            Thread.sleep(100);
            return "計算結果: " + System.currentTimeMillis();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "中断";
        }
    }
}

XML設定ファイル(log4j2.xml)による高度な設定

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="60">
    <Properties>
        <!-- 共通設定 -->
        <Property name="logPattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
        <Property name="logDir">logs</Property>
    </Properties>

    <Appenders>
        <!-- コンソールアペンダー -->
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="${logPattern}" />
            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
        </Console>

        <!-- ローリングファイルアペンダー -->
        <RollingFile name="FileAppender"
                     fileName="${logDir}/application.log"
                     filePattern="${logDir}/application-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${logPattern}" />
            <Policies>
                <!-- 1日または10MBごとにローテーション -->
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- エラー専用ファイル -->
        <RollingFile name="ErrorFileAppender"
                     fileName="${logDir}/error.log"
                     filePattern="${logDir}/error-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${logPattern}" />
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="10"/>
        </RollingFile>

        <!-- JSON形式ログ -->
        <RollingFile name="JsonFileAppender"
                     fileName="${logDir}/application.json"
                     filePattern="${logDir}/application-%d{yyyy-MM-dd}-%i.json.gz">
            <JsonTemplateLayout eventTemplateUri="classpath:LogstashJsonEventLayoutV1.json"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="10MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- 非同期アペンダー(高性能) -->
        <AsyncAppender name="AsyncFileAppender" bufferSize="2048">
            <AppenderRef ref="FileAppender"/>
            <AppenderRef ref="JsonFileAppender"/>
        </AsyncAppender>

        <!-- SMTP アペンダー(重要なエラー通知) -->
        <SMTP name="MailAppender"
              subject="[ERROR] アプリケーションエラー"
              to="[email protected]"
              from="[email protected]"
              smtpHost="smtp.example.com"
              smtpPort="587"
              smtpUsername="[email protected]"
              smtpPassword="${env:SMTP_PASSWORD}"
              bufferSize="10">
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <HtmlLayout title="エラーレポート"/>
        </SMTP>
    </Appenders>

    <Loggers>
        <!-- 特定パッケージの詳細ログ -->
        <Logger name="com.example.service" level="DEBUG" additivity="false">
            <AppenderRef ref="FileAppender"/>
        </Logger>

        <!-- データベース関連ログ -->
        <Logger name="com.example.dao" level="TRACE" additivity="false">
            <AppenderRef ref="FileAppender"/>
        </Logger>

        <!-- フレームワークログ(制限) -->
        <Logger name="org.springframework" level="WARN" additivity="false">
            <AppenderRef ref="ConsoleAppender"/>
        </Logger>

        <!-- 非同期ルートロガー -->
        <AsyncRoot level="INFO" includeLocation="false">
            <AppenderRef ref="ConsoleAppender"/>
            <AppenderRef ref="AsyncFileAppender"/>
            <AppenderRef ref="ErrorFileAppender"/>
            <AppenderRef ref="MailAppender"/>
        </AsyncRoot>
    </Loggers>
</Configuration>

プログラマティック設定とカスタムアペンダー

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.appender.FileAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.builder.api.*;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;

public class ProgrammaticConfigurationExample {
    
    public static void main(String[] args) {
        // プログラマティック設定の作成
        Configuration configuration = createConfiguration();
        
        // 設定の適用
        LoggerContext context = (LoggerContext) LogManager.getContext(false);
        context.setConfiguration(configuration);
        context.updateLoggers();
        
        // ロガーの使用
        Logger logger = LogManager.getLogger(ProgrammaticConfigurationExample.class);
        
        logger.info("プログラマティック設定によるログ出力");
        logger.debug("デバッグ情報: 設定適用成功");
        logger.error("エラーテスト: 意図的なエラーメッセージ");
        
        // カスタムアペンダーのテスト
        testCustomAppender(logger);
    }
    
    private static Configuration createConfiguration() {
        ConfigurationBuilder<BuiltConfiguration> builder = 
            ConfigurationBuilderFactory.newConfigurationBuilder();
        
        // プロパティ設定
        builder.setStatusLevel(Level.DEBUG)
               .setConfigurationName("ProgrammaticConfig");
        
        // パターンレイアウト設定
        LayoutComponentBuilder layoutBuilder = builder.newLayout("PatternLayout")
            .addAttribute("pattern", "%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n");
        
        // コンソールアペンダー
        AppenderComponentBuilder consoleAppender = builder.newAppender("Console", "CONSOLE")
            .addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT)
            .add(layoutBuilder);
        builder.add(consoleAppender);
        
        // ファイルアペンダー
        AppenderComponentBuilder fileAppender = builder.newAppender("File", "File")
            .addAttribute("fileName", "logs/programmatic.log")
            .addAttribute("append", true)
            .add(layoutBuilder);
        builder.add(fileAppender);
        
        // 非同期アペンダー
        AppenderComponentBuilder asyncAppender = builder.newAppender("Async", "Async")
            .addAttribute("bufferSize", 1024)
            .addComponent(builder.newAppenderRef("File"));
        builder.add(asyncAppender);
        
        // ルートロガー設定
        RootLoggerComponentBuilder rootLogger = builder.newRootLogger(Level.INFO)
            .add(builder.newAppenderRef("Console"))
            .add(builder.newAppenderRef("Async"));
        builder.add(rootLogger);
        
        // パッケージ別ロガー
        LoggerComponentBuilder packageLogger = builder.newLogger("com.example", Level.DEBUG)
            .add(builder.newAppenderRef("File"))
            .addAttribute("additivity", false);
        builder.add(packageLogger);
        
        return builder.build();
    }
    
    private static void testCustomAppender(Logger logger) {
        // MDC(Mapped Diagnostic Context)の使用
        org.apache.logging.log4j.ThreadContext.put("userId", "user123");
        org.apache.logging.log4j.ThreadContext.put("sessionId", "session456");
        org.apache.logging.log4j.ThreadContext.put("traceId", "trace789");
        
        try {
            logger.info("MDC付きログ: ユーザー処理開始");
            
            // 業務処理のシミュレーション
            processUserData();
            
            logger.info("MDC付きログ: ユーザー処理完了");
            
        } finally {
            // MDCのクリア
            org.apache.logging.log4j.ThreadContext.clearAll();
        }
    }
    
    private static void processUserData() {
        Logger logger = LogManager.getLogger("com.example.service.UserService");
        
        logger.debug("ユーザーデータ取得開始");
        
        try {
            // 処理のシミュレーション
            Thread.sleep(100);
            logger.info("ユーザーデータ取得成功: レコード数={}", 150);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error("ユーザーデータ取得中断", e);
        }
    }
}

非同期ロギングとパフォーマンス最適化

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ObjectMessage;
import org.apache.logging.log4j.message.StringFormattedMessage;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class HighPerformanceLoggingExample {
    private static final Logger logger = LogManager.getLogger(HighPerformanceLoggingExample.class);
    
    public static void main(String[] args) throws InterruptedException {
        // パフォーマンステスト
        performanceTest();
        
        // 構造化ログテスト
        structuredLoggingTest();
        
        // マルチスレッドテスト
        multiThreadTest();
        
        // ガベージフリーロギングテスト
        garbageFreeLoggingTest();
    }
    
    private static void performanceTest() {
        logger.info("パフォーマンステスト開始");
        
        long startTime = System.nanoTime();
        int logCount = 100000;
        
        for (int i = 0; i < logCount; i++) {
            // 高速ログ出力(パラメータ化メッセージ)
            logger.info("パフォーマンステスト: 番号={}, タイムスタンプ={}", 
                       i, System.currentTimeMillis());
            
            if (i % 10000 == 0) {
                long currentTime = System.nanoTime();
                double elapsedMs = (currentTime - startTime) / 1_000_000.0;
                logger.debug("進捗: {}/{}, 経過時間={}ms, スループット={} logs/sec", 
                           i, logCount, elapsedMs, (i * 1000.0 / elapsedMs));
            }
        }
        
        long endTime = System.nanoTime();
        double totalMs = (endTime - startTime) / 1_000_000.0;
        double throughput = logCount * 1000.0 / totalMs;
        
        logger.info("パフォーマンステスト完了: 総時間={}ms, スループット={} logs/sec", 
                   totalMs, throughput);
    }
    
    private static void structuredLoggingTest() {
        logger.info("構造化ログテスト開始");
        
        // Map型オブジェクトによる構造化ログ
        Map<String, Object> logData = new HashMap<>();
        logData.put("userId", "user123");
        logData.put("action", "purchase");
        logData.put("productId", "prod456");
        logData.put("amount", 15000);
        logData.put("timestamp", System.currentTimeMillis());
        
        logger.info(new ObjectMessage(logData));
        
        // カスタムオブジェクトによる構造化ログ
        UserTransaction transaction = new UserTransaction("user789", "sale", 25000);
        logger.info("取引ログ: {}", transaction);
        
        // JSON風の構造化ログ
        logger.info("API呼び出し: {\"endpoint\":\"/api/users\", \"method\":\"GET\", \"responseTime\":{}}", 150);
    }
    
    private static void multiThreadTest() throws InterruptedException {
        logger.info("マルチスレッドテスト開始");
        
        ExecutorService executor = Executors.newFixedThreadPool(10);
        int tasksPerThread = 1000;
        
        for (int threadId = 0; threadId < 10; threadId++) {
            final int finalThreadId = threadId;
            executor.submit(() -> {
                for (int i = 0; i < tasksPerThread; i++) {
                    logger.info("スレッド{}: タスク番号={}, 実行時刻={}", 
                               finalThreadId, i, System.currentTimeMillis());
                    
                    if (i % 100 == 0) {
                        logger.debug("スレッド{}: 進捗={}%", finalThreadId, 
                                   (i * 100 / tasksPerThread));
                    }
                }
                logger.info("スレッド{}: 全タスク完了", finalThreadId);
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(30, TimeUnit.SECONDS);
        
        logger.info("マルチスレッドテスト完了");
    }
    
    private static void garbageFreeLoggingTest() {
        logger.info("ガベージフリーロギングテスト開始");
        
        // プリミティブ型のログ(アロケーション最小)
        long startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        
        for (int i = 0; i < 10000; i++) {
            // ガベージフリー対応の書き方
            logger.info("GCフリーテスト: 反復回数={}", i);
        }
        
        // GC実行
        System.gc();
        Thread.yield();
        
        long endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        logger.info("メモリ使用量: 開始={}KB, 終了={}KB, 差分={}KB", 
                   startMemory/1024, endMemory/1024, (endMemory-startMemory)/1024);
        
        logger.info("ガベージフリーロギングテスト完了");
    }
    
    // ユーザー取引データクラス
    static class UserTransaction {
        private final String userId;
        private final String action;
        private final int amount;
        private final long timestamp;
        
        public UserTransaction(String userId, String action, int amount) {
            this.userId = userId;
            this.action = action;
            this.amount = amount;
            this.timestamp = System.currentTimeMillis();
        }
        
        @Override
        public String toString() {
            return String.format("{\"userId\":\"%s\", \"action\":\"%s\", \"amount\":%d, \"timestamp\":%d}", 
                               userId, action, amount, timestamp);
        }
    }
}

Spring Boot統合とプロダクション設定

// Spring Boot設定クラス
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.UUID;

@SpringBootApplication
public class Log4j2SpringBootApplication {
    private static final Logger logger = LogManager.getLogger(Log4j2SpringBootApplication.class);
    
    public static void main(String[] args) {
        logger.info("Spring Boot アプリケーション開始");
        SpringApplication.run(Log4j2SpringBootApplication.class, args);
        logger.info("Spring Boot アプリケーション開始完了");
    }
}

@RestController
@RequestMapping("/api")
class ApiController {
    private static final Logger logger = LogManager.getLogger(ApiController.class);
    
    @GetMapping("/users/{userId}")
    public ResponseEntity<UserResponse> getUser(@PathVariable String userId, 
                                               HttpServletRequest request) {
        // リクエストIDの生成と設定
        String requestId = UUID.randomUUID().toString();
        org.apache.logging.log4j.ThreadContext.put("requestId", requestId);
        org.apache.logging.log4j.ThreadContext.put("userId", userId);
        
        try {
            logger.info("ユーザー取得リクエスト: URL={}, IP={}, UserAgent={}", 
                       request.getRequestURL(), 
                       request.getRemoteAddr(),
                       request.getHeader("User-Agent"));
            
            // ビジネスロジック実行
            UserResponse user = userService.findUser(userId);
            
            logger.info("ユーザー取得成功: ユーザー名={}", user.getName());
            return ResponseEntity.ok(user);
            
        } catch (UserNotFoundException e) {
            logger.warn("ユーザーが見つかりません: userId={}", userId, e);
            return ResponseEntity.notFound().build();
            
        } catch (Exception e) {
            logger.error("ユーザー取得エラー: userId={}", userId, e);
            return ResponseEntity.status(500).build();
            
        } finally {
            // ThreadContext のクリア
            org.apache.logging.log4j.ThreadContext.clearAll();
        }
    }
    
    @PostMapping("/users")
    public ResponseEntity<UserResponse> createUser(@RequestBody CreateUserRequest request,
                                                  HttpServletRequest httpRequest) {
        String requestId = UUID.randomUUID().toString();
        org.apache.logging.log4j.ThreadContext.put("requestId", requestId);
        
        try {
            logger.info("ユーザー作成リクエスト: 名前={}, メール={}", 
                       request.getName(), request.getEmail());
            
            // バリデーション
            if (request.getName() == null || request.getName().trim().isEmpty()) {
                logger.warn("ユーザー作成失敗: 名前が空");
                return ResponseEntity.badRequest().build();
            }
            
            // ユーザー作成
            UserResponse user = userService.createUser(request);
            
            logger.info("ユーザー作成成功: ID={}, 名前={}", user.getId(), user.getName());
            return ResponseEntity.status(201).body(user);
            
        } catch (EmailDuplicateException e) {
            logger.warn("メールアドレス重複: メール={}", request.getEmail(), e);
            return ResponseEntity.status(409).build();
            
        } catch (Exception e) {
            logger.error("ユーザー作成エラー", e);
            return ResponseEntity.status(500).build();
            
        } finally {
            org.apache.logging.log4j.ThreadContext.clearAll();
        }
    }
}

// ログ設定用プロファイル(application-production.yml)
/*
logging:
  config: classpath:log4j2-production.xml
  level:
    com.example: INFO
    org.springframework: WARN
    org.hibernate: WARN
    
# システムプロパティ設定
log4j2:
  asyncLoggerWaitStrategy: Block
  asyncLoggerRingBufferSize: 2048
  garbageFreeThresholdBytes: 1024
  enableThreadLocals: true
*/
<!-- log4j2-production.xml(本番環境用設定) -->
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="300">
    <Properties>
        <Property name="appName">MySpringBootApp</Property>
        <Property name="logPattern">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%X{requestId}] %logger{36} - %msg%n</Property>
        <Property name="logDir">/var/log/${appName}</Property>
    </Properties>

    <Appenders>
        <!-- アプリケーションログ -->
        <RollingFile name="AppFileAppender"
                     fileName="${logDir}/application.log"
                     filePattern="${logDir}/application-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${logPattern}" />
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="90"/>
        </RollingFile>

        <!-- エラーログ -->
        <RollingFile name="ErrorFileAppender"
                     fileName="${logDir}/error.log"
                     filePattern="${logDir}/error-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="${logPattern}" />
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="50MB"/>
            </Policies>
            <DefaultRolloverStrategy max="30"/>
        </RollingFile>

        <!-- アクセスログ -->
        <RollingFile name="AccessFileAppender"
                     fileName="${logDir}/access.log"
                     filePattern="${logDir}/access-%d{yyyy-MM-dd}-%i.log.gz">
            <JsonTemplateLayout eventTemplateUri="classpath:AccessLogLayout.json"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="90"/>
        </RollingFile>

        <!-- 非同期ラッパー -->
        <AsyncAppender name="AsyncAppender" bufferSize="4096" 
                      shutdownTimeout="3000" includeLocation="false">
            <AppenderRef ref="AppFileAppender"/>
        </AsyncAppender>
    </Appenders>

    <Loggers>
        <!-- アプリケーションロガー -->
        <Logger name="com.example" level="INFO" additivity="false">
            <AppenderRef ref="AsyncAppender"/>
            <AppenderRef ref="ErrorFileAppender"/>
        </Logger>

        <!-- アクセスログロガー -->
        <Logger name="ACCESS_LOG" level="INFO" additivity="false">
            <AppenderRef ref="AccessFileAppender"/>
        </Logger>

        <!-- フレームワークログ制限 -->
        <Logger name="org.springframework" level="WARN" additivity="false">
            <AppenderRef ref="AsyncAppender"/>
        </Logger>

        <!-- 非同期ルートロガー -->
        <AsyncRoot level="INFO" includeLocation="false">
            <AppenderRef ref="AsyncAppender"/>
            <AppenderRef ref="ErrorFileAppender"/>
        </AsyncRoot>
    </Loggers>
</Configuration>