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}