001package com.streamconverter.logging;
002
003import java.lang.reflect.Field;
004import java.lang.reflect.Method;
005import java.util.Map;
006import org.slf4j.ILoggerFactory;
007import org.slf4j.LoggerFactory;
008import org.slf4j.MDC;
009import org.slf4j.spi.MDCAdapter;
010
011/**
012 * Installs {@link InheritableMDCAdapter} so that MDC context is automatically propagated to child
013 * threads (including virtual threads) without manual copying.
014 *
015 * <p>Call {@link #initialize()} once at application startup, <em>before</em> the first log
016 * statement, to replace the default Logback MDC adapter. After initialization, MDC values set in a
017 * parent thread are automatically visible in any thread it spawns.
018 *
019 * <p>If {@link InheritableMDCAdapter} is not installed (i.e., {@link #initialize()} was not
020 * called), MDC context will not propagate to child threads. Each thread will have an independent,
021 * empty MDC context.
022 *
023 * <p><strong>Important:</strong> {@code InheritableMDCAdapter} is backed by {@link
024 * java.lang.InheritableThreadLocal}. In environments that reuse threads (for example, servlet
025 * containers or {@link java.util.concurrent.ExecutorService} thread pools), MDC values inherited
026 * for one logical request can unintentionally be visible to a subsequent, unrelated request running
027 * on the same thread unless MDC is reliably cleared. This can lead to log correlation IDs, user
028 * identifiers, or other contextual data "leaking" across requests.
029 *
030 * <p>When using {@link MDCInitializer} together with thread pools, ensure that each task clears MDC
031 * in a {@code finally} block (for example, by calling {@link MDC#clear()} or removing the keys it
032 * set), or wrap submitted {@link Runnable}/{@link java.util.concurrent.Callable} instances so that
033 * MDC is captured, applied for the task execution, and then cleared afterwards.
034 *
035 * <p>Example usage:
036 *
037 * <pre>{@code
038 * public static void main(String[] args) {
039 *     MDCInitializer.initialize();
040 *     // ... rest of application startup
041 * }
042 * }</pre>
043 */
044public final class MDCInitializer {
045
046  private MDCInitializer() {}
047
048  /**
049   * Installs {@link InheritableMDCAdapter} into both the SLF4J {@link MDC} class and the Logback
050   * {@code LoggerContext} (if Logback is present on the classpath).
051   *
052   * <p>This method is idempotent: calling it multiple times has no additional effect after the
053   * first call. It is also thread-safe; concurrent calls are serialized.
054   *
055   * <p><strong>Note:</strong> The key-value map of the current thread's MDC context is migrated
056   * into the new adapter. However, deque-based state ({@code pushByKey}/{@code popByKey}) and
057   * values set in other threads are not migrated. For best results, call this method at application
058   * startup before any MDC values are set.
059   *
060   * @throws IllegalStateException if the adapter cannot be installed via reflection
061   */
062  public static synchronized void initialize() {
063    MDCAdapter currentAdapter = MDC.getMDCAdapter();
064    boolean slf4jReady = currentAdapter instanceof InheritableMDCAdapter;
065    boolean logbackReady = isLogbackAlreadyInstalled(currentAdapter);
066
067    if (slf4jReady && logbackReady) {
068      return;
069    }
070
071    // Capture existing MDC state BEFORE replacing the adapter so it can be migrated
072    Map<String, String> existingContext = MDC.getCopyOfContextMap();
073
074    // Reuse an already-installed adapter if SLF4J side is ready; otherwise create a new one
075    InheritableMDCAdapter adapter =
076        slf4jReady ? (InheritableMDCAdapter) currentAdapter : new InheritableMDCAdapter();
077
078    // Replace the MDC_ADAPTER field in SLF4J MDC via reflection
079    // (MDC.setMDCAdapter is package-private in SLF4J 2.x)
080    if (!slf4jReady) {
081      try {
082        Field mdcAdapterField = MDC.class.getDeclaredField("MDC_ADAPTER");
083        mdcAdapterField.setAccessible(true);
084        mdcAdapterField.set(null, (MDCAdapter) adapter);
085      } catch (ReflectiveOperationException | RuntimeException e) {
086        throw new IllegalStateException(
087            "Failed to install InheritableMDCAdapter into SLF4J MDC", e);
088      }
089    }
090
091    // Install into Logback LoggerContext via reflection (avoids hard compile-time dependency)
092    if (!logbackReady) {
093      try {
094        Class<?> loggerContextClass = Class.forName("ch.qos.logback.classic.LoggerContext");
095        ILoggerFactory factory = LoggerFactory.getILoggerFactory();
096        if (loggerContextClass.isInstance(factory)) {
097          Method setMDCAdapter = loggerContextClass.getMethod("setMDCAdapter", MDCAdapter.class);
098          setMDCAdapter.invoke(factory, adapter);
099        }
100      } catch (ClassNotFoundException ignored) {
101        // Logback is not on the classpath; nothing to do
102      } catch (ReflectiveOperationException | RuntimeException e) {
103        throw new IllegalStateException(
104            "Failed to install InheritableMDCAdapter into Logback LoggerContext", e);
105      }
106    }
107
108    // Migrate existing MDC state from the previous adapter into the new one
109    if (!slf4jReady && existingContext != null) {
110      adapter.setContextMap(existingContext);
111    }
112  }
113
114  /**
115   * Returns {@code true} if the Logback {@code LoggerContext} already has an {@link
116   * InheritableMDCAdapter} installed, or if Logback is not on the classpath.
117   */
118  private static boolean isLogbackAlreadyInstalled(MDCAdapter currentSlf4jAdapter) {
119    try {
120      Class<?> loggerContextClass = Class.forName("ch.qos.logback.classic.LoggerContext");
121      ILoggerFactory factory = LoggerFactory.getILoggerFactory();
122      if (!loggerContextClass.isInstance(factory)) {
123        return true; // not Logback — nothing to install
124      }
125      Method getMDCAdapter = loggerContextClass.getMethod("getMDCAdapter");
126      MDCAdapter logbackAdapter = (MDCAdapter) getMDCAdapter.invoke(factory);
127      // Both sides must use the same InheritableMDCAdapter instance
128      return logbackAdapter instanceof InheritableMDCAdapter
129          && logbackAdapter == currentSlf4jAdapter;
130    } catch (ClassNotFoundException ignored) {
131      return true; // Logback not on classpath
132    } catch (ReflectiveOperationException | RuntimeException ignored) {
133      return false;
134    }
135  }
136}