001package com.streamconverter.logging; 002 003import java.util.ArrayDeque; 004import java.util.Deque; 005import java.util.HashMap; 006import java.util.Map; 007import org.slf4j.spi.MDCAdapter; 008 009/** 010 * MDCAdapter implementation that propagates MDC context to child threads via 011 * InheritableThreadLocal. 012 * 013 * <p>The standard {@link ch.qos.logback.classic.util.LogbackMDCAdapter} uses plain {@link 014 * ThreadLocal}, which does not propagate to child threads. This implementation uses {@link 015 * InheritableThreadLocal} so that virtual threads (and platform threads) spawned from a parent 016 * automatically inherit a copy of the parent's MDC context map. 017 * 018 * <p>Use {@link MDCInitializer#initialize()} at application startup (before the first log call) to 019 * install this adapter. 020 * 021 * <p><b>Thread safety:</b> Each thread holds its own independent copy of the context map via {@link 022 * InheritableThreadLocal}. Child threads receive a shallow copy (snapshot) of the parent's map at 023 * creation time; subsequent changes in the parent are not reflected in already-running children, 024 * and vice versa. Individual per-thread maps are not synchronized, which is safe as long as each 025 * map is accessed only by its owning thread — which is the normal MDC usage pattern. 026 */ 027public class InheritableMDCAdapter implements MDCAdapter { 028 029 /** Constructs a new InheritableMDCAdapter. */ 030 public InheritableMDCAdapter() {} 031 032 private final InheritableThreadLocal<Map<String, String>> tlm = 033 new InheritableThreadLocal<>() { 034 @Override 035 protected Map<String, String> childValue(Map<String, String> parentValue) { 036 return parentValue == null ? null : new HashMap<>(parentValue); 037 } 038 }; 039 040 private final InheritableThreadLocal<Map<String, Deque<String>>> tlmDeque = 041 new InheritableThreadLocal<>() { 042 @Override 043 protected Map<String, Deque<String>> childValue(Map<String, Deque<String>> parentValue) { 044 if (parentValue == null) return null; 045 Map<String, Deque<String>> copy = new HashMap<>(); 046 for (Map.Entry<String, Deque<String>> entry : parentValue.entrySet()) { 047 copy.put(entry.getKey(), new ArrayDeque<>(entry.getValue())); 048 } 049 return copy; 050 } 051 }; 052 053 /** 054 * 現在のスレッドのMDCコンテキストにキーと値を設定する。 055 * 056 * <p>このメソッド呼び出し後に起動された子スレッドは、このエントリを自動継承する ({@link InheritableThreadLocal} 057 * による)。ただし、既に起動済みの子スレッドには反映されない。 058 * 059 * @param key MDCキー名。nullの場合は {@link IllegalArgumentException} をスロー 060 * @param val 設定する値 061 * @throws IllegalArgumentException keyがnullの場合 062 */ 063 @Override 064 public void put(String key, String val) { 065 if (key == null) throw new IllegalArgumentException("key cannot be null"); 066 Map<String, String> map = tlm.get(); 067 if (map == null) { 068 map = new HashMap<>(); 069 tlm.set(map); 070 } 071 map.put(key, val); 072 } 073 074 /** 075 * 現在のスレッドのMDCコンテキストから指定キーの値を取得する。 076 * 077 * @param key MDCキー名 078 * @return 設定されている値。キーが存在しないかkeyがnullの場合はnull 079 */ 080 @Override 081 public String get(String key) { 082 Map<String, String> map = tlm.get(); 083 return (map != null && key != null) ? map.get(key) : null; 084 } 085 086 /** 087 * 現在のスレッドのMDCコンテキストから指定キーを削除する。 088 * 089 * <p>削除後にコンテキストが空になった場合は {@link InheritableThreadLocal} 自体を解放する。 Dequeスタック({@link 090 * #pushByKey}/{@link #popByKey})には影響しない。 Dequeを含む全エントリのクリアには {@link #clear()} を使用すること。 091 * 092 * @param key 削除するMDCキー名 093 */ 094 @Override 095 public void remove(String key) { 096 Map<String, String> map = tlm.get(); 097 if (map != null) { 098 map.remove(key); 099 if (map.isEmpty()) { 100 tlm.remove(); 101 } 102 } 103 } 104 105 /** 106 * 現在のスレッドのMDCコンテキスト(マップおよびDeque)をすべてクリアする。 107 * 108 * <p>{@link InheritableThreadLocal} のエントリを解放するため、このメソッド呼び出し後に 生成される子スレッドにはコンテキストが継承されない。 109 * なお、既に起動済みの子スレッドが保持するコピーには影響しない。 110 */ 111 @Override 112 public void clear() { 113 tlm.remove(); 114 tlmDeque.remove(); 115 } 116 117 /** 118 * 現在のスレッドのMDCコンテキストのコピーを返す。 119 * 120 * <p>返されたマップはスナップショットであり、以降の変更は反映されない。 121 * 122 * @return MDCコンテキストのコピー。コンテキストが未設定の場合はnull 123 */ 124 @Override 125 public Map<String, String> getCopyOfContextMap() { 126 Map<String, String> map = tlm.get(); 127 return (map != null) ? new HashMap<>(map) : null; 128 } 129 130 /** 131 * 現在のスレッドのMDCコンテキストを指定されたマップで置き換える。 132 * 133 * <p>nullを渡すとコンテキストをクリアする。子スレッドへの継承も更新後の内容になる。 134 * 135 * @param contextMap 新しいMDCコンテキスト。nullの場合はクリア 136 */ 137 @Override 138 public void setContextMap(Map<String, String> contextMap) { 139 if (contextMap == null) { 140 tlm.remove(); 141 } else { 142 tlm.set(new HashMap<>(contextMap)); 143 } 144 } 145 146 /** 147 * 指定キーのDequeスタックに値をプッシュする。 148 * 149 * <p>keyがnullの場合は何もしない。 150 * 151 * @param key Dequeのキー名 152 * @param value プッシュする値 153 */ 154 @Override 155 public void pushByKey(String key, String value) { 156 if (key == null) return; 157 Map<String, Deque<String>> map = tlmDeque.get(); 158 if (map == null) { 159 map = new HashMap<>(); 160 tlmDeque.set(map); 161 } 162 map.computeIfAbsent(key, k -> new ArrayDeque<>()).push(value); 163 } 164 165 /** 166 * 指定キーのDequeスタックから先頭の値をポップして返す。 167 * 168 * <p>keyがnullまたはDequeが存在しない場合はnullを返す。 169 * 170 * @param key Dequeのキー名 171 * @return ポップした値。キーが存在しないかkeyがnullの場合はnull 172 */ 173 @Override 174 public String popByKey(String key) { 175 if (key == null) return null; 176 Map<String, Deque<String>> map = tlmDeque.get(); 177 if (map == null) return null; 178 Deque<String> deque = map.get(key); 179 return (deque != null) ? deque.pop() : null; 180 } 181 182 /** 183 * 指定キーのDequeのコピーを返す。 184 * 185 * <p>返されたDequeはスナップショットであり、以降の変更は反映されない。 186 * 187 * @param key Dequeのキー名 188 * @return Dequeのコピー。キーが存在しない場合はnull 189 */ 190 @Override 191 public Deque<String> getCopyOfDequeByKey(String key) { 192 Map<String, Deque<String>> map = tlmDeque.get(); 193 if (map == null) return null; 194 Deque<String> deque = map.get(key); 195 return (deque != null) ? new ArrayDeque<>(deque) : null; 196 } 197 198 /** 199 * 指定キーのDequeの全要素をクリアする。 200 * 201 * <p>keyがnullまたはDequeが存在しない場合は何もしない。 202 * 203 * @param key クリアするDequeのキー名 204 */ 205 @Override 206 public void clearDequeByKey(String key) { 207 if (key == null) return; 208 Map<String, Deque<String>> map = tlmDeque.get(); 209 if (map == null) return; 210 Deque<String> deque = map.get(key); 211 if (deque != null) { 212 deque.clear(); 213 } 214 } 215}