001package com.streamconverter.logging;
002
003import ch.qos.logback.classic.Level;
004import ch.qos.logback.classic.Logger;
005import ch.qos.logback.classic.turbo.TurboFilter;
006import ch.qos.logback.core.spi.FilterReply;
007import com.streamconverter.context.ExecutionContext;
008import com.streamconverter.context.ExecutionContextHolder;
009import java.util.HashSet;
010import java.util.Map;
011import java.util.Set;
012import org.slf4j.MDC;
013import org.slf4j.Marker;
014
015/**
016 * ExecutionContextからMDCに値を自動設定するLogback TurboFilter
017 *
018 * <p>このフィルターは、ログ出力の都度ExecutionContextHolderから値を取得し、 MDCに自動的に設定します。これにより、アプリケーションコードは
019 * MDC同期を意識する必要がありません。
020 *
021 * <p><b>設定例(logback.xml):</b>
022 *
023 * <pre>{@code
024 * <configuration>
025 *   <turboFilter class="com.streamconverter.logging.ExecutionContextTurboFilter"/>
026 *
027 *   <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
028 *     <encoder>
029 *       <pattern>%d{HH:mm:ss.SSS} [userId:%X{userId}] - %msg%n</pattern>
030 *     </encoder>
031 *   </appender>
032 * </configuration>
033 * }</pre>
034 *
035 * <p><b>動作:</b>
036 *
037 * <ol>
038 *   <li>ログ出力の都度、このフィルターのdecide()メソッドが呼ばれる
039 *   <li>ExecutionContextHolderから現在のExecutionContextを取得
040 *   <li>共有コンテキストの全キーについて、MDCと比較して変更がある場合のみ更新
041 *   <li>削除されたキーはMDCからも削除
042 * </ol>
043 *
044 * <p><b>パフォーマンス最適化:</b>
045 *
046 * <ul>
047 *   <li>MDCの値と比較して、変更がある場合のみMDC.put()を呼ぶ
048 *   <li>前回同期したキーをThreadLocalで追跡し、削除されたキーをクリーンアップ
049 *   <li>任意の数の共有コンテキストキーに対応
050 * </ul>
051 *
052 * <p><b>スレッドセーフ性:</b> ThreadLocalを使用しているため、スレッドごとに独立したMDC値が設定されます。
053 */
054public class ExecutionContextTurboFilter extends TurboFilter {
055
056  /**
057   * 各スレッドで管理している共有コンテキストのキーセット
058   *
059   * <p>前回同期時に存在していたキーを追跡し、削除されたキーをMDCからクリーンアップするために使用します。
060   */
061  private static final ThreadLocal<Set<String>> managedKeys = ThreadLocal.withInitial(HashSet::new);
062
063  /**
064   * ログイベント処理時に呼ばれ、ExecutionContextの共有コンテキストをMDCに同期します
065   *
066   * <p>共有コンテキストの全キーについて、現在のMDC値と比較し、変更がある場合のみ更新します。 前回存在していたが今回削除されたキーは、MDCからも削除されます。
067   *
068   * @param marker マーカー
069   * @param logger ロガー
070   * @param level ログレベル
071   * @param format メッセージフォーマット
072   * @param params パラメータ
073   * @param t 例外
074   * @return FilterReply.NEUTRAL(フィルタリングは行わず、常にログを通過させる)
075   */
076  @Override
077  public FilterReply decide(
078      Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
079
080    ExecutionContext context = ExecutionContextHolder.get();
081
082    if (context != null) {
083      // 共有コンテキストの全キーと値を取得
084      Map<String, String> sharedContext = context.getAllSharedContext();
085      Set<String> currentKeys = new HashSet<>();
086
087      // 各キーについて、MDCと比較して変更がある場合のみ更新
088      for (Map.Entry<String, String> entry : sharedContext.entrySet()) {
089        String key = entry.getKey();
090        String value = entry.getValue();
091        currentKeys.add(key);
092
093        String currentValue = MDC.get(key);
094        if (!value.equals(currentValue)) {
095          MDC.put(key, value);
096        }
097      }
098
099      // 前回同期時に存在していたが、今回削除されたキーをMDCから削除
100      Set<String> previousKeys = managedKeys.get();
101      for (String key : previousKeys) {
102        if (!currentKeys.contains(key)) {
103          MDC.remove(key);
104        }
105      }
106
107      // 今回のキーセットを保存
108      managedKeys.set(currentKeys);
109
110    } else {
111      // ExecutionContextが無い場合、管理していた全てのキーをMDCから削除
112      Set<String> previousKeys = managedKeys.get();
113      for (String key : previousKeys) {
114        MDC.remove(key);
115      }
116      // ThreadLocalをクリアしてメモリリークを防止
117      managedKeys.remove();
118    }
119
120    // フィルタリングは行わず、常にログを通過させる
121    return FilterReply.NEUTRAL;
122  }
123}