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}