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}