RainbowGumSLF4JServiceProvider.java

package io.jstach.rainbowgum.slf4j;

import java.util.Locale;

import org.jspecify.annotations.Nullable;
import org.slf4j.ILoggerFactory;
import org.slf4j.IMarkerFactory;
import org.slf4j.helpers.BasicMarkerFactory;
import org.slf4j.spi.MDCAdapter;
import org.slf4j.spi.SLF4JServiceProvider;

import io.jstach.rainbowgum.KeyValues;
import io.jstach.rainbowgum.LogProperty;
import io.jstach.rainbowgum.RainbowGum;
import io.jstach.svc.ServiceProvider;

/**
 * SLF4J provider using RainbowGum.
 */
@ServiceProvider(SLF4JServiceProvider.class)
public class RainbowGumSLF4JServiceProvider implements SLF4JServiceProvider {

	/**
	 * Declare the version of the SLF4J API this implementation is compiled against. The
	 * value of this field is modified with each major release.
	 */
	private static final String REQUESTED_API_VERSION = "2.0";

	/*
	 * The MDC adapter initialized through SLF4J's provider lifecycle. Isolated providers
	 * initialized with initialize(RainbowGum) must not replace it. Null until SLF4J
	 * initializes, in which case nothing can have been put in MDC yet.
	 */
	private static volatile @Nullable RainbowGumMDCAdapter currentMDC;

	static KeyValues currentMDCKeyValues() {
		var mdc = currentMDC;
		return mdc == null ? KeyValues.of() : mdc.keyValues();
	}

	static void clearCurrentMDC() {
		var mdc = currentMDC;
		if (mdc != null) {
			mdc.removeThreadLocals();
		}
	}

	/**
	 * Which {@link RainbowGumMDCAdapter} implementation to use - {@code THREAD_LOCAL}
	 * (the default) is today's existing {@link ArrayMDCAdapter} behavior; {@code NOOP}
	 * swaps in a {@link NoopMDCAdapter} instead, whose every method is a
	 * no-op/empty-returning stub that never touches either of {@link ArrayMDCAdapter}'s
	 * {@link ThreadLocal} fields - for deployments that want a hard guarantee of no
	 * {@link ThreadLocal} anywhere in the logging path and are fine losing MDC entirely
	 * to get it. Kept as a type rather than an enabled/disabled toggle so a future third
	 * implementation - e.g. one backed by {@code ScopedValue} instead of
	 * {@link ThreadLocal}, once that's a viable MDC storage strategy - can be added
	 * without a breaking property-format change.
	 */
	enum MDCType {

		/**
		 * MDC works normally - {@link ArrayMDCAdapter}'s existing
		 * {@link ThreadLocal}-backed behavior, unchanged.
		 */
		THREAD_LOCAL,
		/**
		 * MDC is completely turned off - {@link NoopMDCAdapter} is used instead of
		 * {@link ArrayMDCAdapter}.
		 */
		NOOP;

		static MDCType parse(String value) {
			return LogProperty.enumValue(MDCType.class, value);
		}

	}

	/**
	 * {@code logging.mdc.type} - {@code THREAD_LOCAL} (default) or {@code NOOP}. Read
	 * once, during {@link #initialize(RainbowGum)}, since
	 * {@link #RainbowGumSLF4JServiceProvider()} (called by
	 * {@link java.util.ServiceLoader}) runs before any {@link RainbowGum} (and therefore
	 * any properties) exist yet.
	 */
	static final String LOGGING_MDC_TYPE_PROPERTY = "logging.mdc.type";

	/**
	 * {@code logging.global.threadlocalDisabled} - {@code core}'s
	 * {@code LogProperties#GLOBAL_THREADLOCAL_DISABLED_PROPERTY}, read independently here
	 * (that constant is package-private in a different module, not exposed for reuse)
	 * since it forces MDC off too, the same way it forces every appender off
	 * {@link ThreadLocal}-backed types - see that property's javadoc for why this is two
	 * independent readers of the same property key rather than one shared flag.
	 */
	static final String GLOBAL_THREADLOCAL_DISABLED_PROPERTY = "logging.global.threadlocalDisabled";

	/*
	 * TRUE/FALSE rather than .ofBoolean() - a typo'd value fails loudly through the
	 * existing Property/Result machinery instead of silently resolving to "not disabled".
	 * Mirrors core's identically-named-in-spirit LogConfig.ThreadLocalDisabled - each
	 * module parses this shared property key independently, see
	 * GLOBAL_THREADLOCAL_DISABLED_PROPERTY above.
	 */
	private enum GlobalThreadLocalDisabled {

		TRUE, FALSE;

		static GlobalThreadLocalDisabled parse(String value) {
			return LogProperty.enumValue(GlobalThreadLocalDisabled.class, value);
		}

	}

	private @Nullable ILoggerFactory loggerFactory;

	private final IMarkerFactory markerFactory;

	/*
	 * Not final: initialize(RainbowGum) may swap this from the default THREAD_LOCAL
	 * instance constructed below to a NOOP one once logging.mdc.type can actually be read
	 * - see that method. Any MDCAdapter method called between construction and
	 * initialize() running (an SLF4J-bootstrap-ordering edge case, not expected in normal
	 * use) still observes the default THREAD_LOCAL instance either way.
	 */
	private RainbowGumMDCAdapter mdcAdapter;

	/**
	 * No Arg for service laoder.
	 */
	public RainbowGumSLF4JServiceProvider() {
		mdcAdapter = new RainbowGumMDCAdapter();
		markerFactory = new BasicMarkerFactory();
	}

	@Override
	public ILoggerFactory getLoggerFactory() {
		return require(loggerFactory);
	}

	@Override
	public IMarkerFactory getMarkerFactory() {
		return markerFactory;
	}

	@Override
	public MDCAdapter getMDCAdapter() {
		return mdcAdapter;
	}

	@Override
	public String getRequestedApiVersion() {
		return REQUESTED_API_VERSION;
	}

	private static <T> T require(@Nullable T factory) {
		if (factory == null) {
			throw new IllegalStateException("slf4j was not initialized correctly");
		}
		return factory;
	}

	@Override
	public void initialize() {
		/*
		 * Make JBoss logging use us
		 */
		if (System.getProperty("org.jboss.logging.provider") == null) {
			System.setProperty("org.jboss.logging.provider", "slf4j");
		}
		RainbowGum rainbowGum = RainbowGum.of();
		initialize(rainbowGum);
		currentMDC = mdcAdapter;
		System.setProperty("SLF4J_LOGGING_LOADED", "true");
	}

	/**
	 * For testing Rainbow Gums SLF4J without initializing SLF4J.
	 * @param rainbowGum which gum to use for logger factory.
	 */
	public void initialize(RainbowGum rainbowGum) {
		var properties = rainbowGum.config().properties();
		// shared validator: a mistake in both logging.mdc.type and
		// logging.global.threadlocalDisabled at once is reported together instead of
		// only the first one found - both already have .or(default) applied, so
		// validateIfError (not validate) is correct: Missing can never occur.
		var validator = LogProperty.Validator.of(RainbowGumSLF4JServiceProvider.class);
		var typeResult = properties.forKey(LOGGING_MDC_TYPE_PROPERTY)
			.ofString()
			.map(MDCType::parse)
			.or(MDCType.THREAD_LOCAL)
			.validateIfError(validator);
		var globalDisabledResult = properties.forKey(GLOBAL_THREADLOCAL_DISABLED_PROPERTY)
			.ofString()
			.map(GlobalThreadLocalDisabled::parse)
			.or(GlobalThreadLocalDisabled.FALSE)
			.validateIfError(validator);
		validator.validate();
		var type = typeResult.value();
		var globalDisabled = globalDisabledResult.value() == GlobalThreadLocalDisabled.TRUE;
		if (type == MDCType.NOOP || globalDisabled) {
			mdcAdapter = new NoopMDCAdapter();
		}
		loggerFactory = new RainbowGumLoggerFactory(rainbowGum, mdcAdapter);
	}

}