LoggerDecoratorService.java

package io.jstach.rainbowgum.slf4j.spi;

import java.util.List;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.spi.LoggingEventBuilder;

import io.jstach.rainbowgum.KeyValues;
import io.jstach.rainbowgum.LogConfig;
import io.jstach.rainbowgum.LogEventLogger;
import io.jstach.rainbowgum.RainbowGum;
import io.jstach.rainbowgum.slf4j.ForwardingLogger;
import io.jstach.rainbowgum.slf4j.WrappingLogger;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;

/**
 * <strong>EXPERIMENTAL:</strong> this rainbowgum service provider allows wrapping slf4j
 * loggers created from the logger factory. <em>This allows for custom filtering using the
 * SLF4J api directly!</em>
 * <p>
 * If there are multiple registrations {@link #order()} and then {@link #name()} will be
 * used to sort where the lower order number and name will
 * {@linkplain #decorate(RainbowGum, DepthAwareLogger, int) decorate} first. See
 * {@link RainbowGumServiceProvider} for details on how to register.
 * <p>
 * <strong>IMPORTANT implementation detail</strong> is that you should not call the SLF4J
 * logger factory otherwise a stackoverflow or similar will happen.
 *
 * <h2>Writing a decorator</h2>
 *
 * Do not extend {@link org.slf4j.helpers.AbstractLogger} directly to implement the
 * returned {@link Logger}: getting caller-info depth right means depending on that
 * class's internal multi-arity dispatch shape, which is an unversioned SLF4J
 * implementation detail rather than a contract, and is easy to get subtly wrong. Extend
 * {@link AbstractFilteringLogger} instead - it owns and tests its own depth accounting so
 * a decorator author never has to reason about stack frames.
 * <p>
 * A decorator that samples DEBUG level events and surfaces any {@link org.slf4j.Marker}
 * as a key value (Rainbow Gum core has no built-in {@code Marker} support, so this is how
 * a decorator can still make use of one):
 *
 * {@snippet class = "snippets.DecoratorExample" region = "decoratorExample" }
 *
 * Once built, register it like any other {@link RainbowGumServiceProvider}. If your
 * application is modularized:
 *
 * {@snippet :
 *
 * provides io.jstach.rainbowgum.spi.RainbowGumServiceProvider with com.mycompany.DecoratorExample;
 *
 * }
 *
 * @see ForwardingLogger
 * @see RainbowGumServiceProvider
 * @see AbstractFilteringLogger
 */
public abstract class LoggerDecoratorService implements RainbowGumServiceProvider.Configurator {

	/**
	 * No arg constructor for service loader.
	 */
	public LoggerDecoratorService() {

	}

	@Override
	public final boolean configure(LogConfig config, Pass pass) {
		config.serviceRegistry().put(LoggerDecoratorService.class, name(), this);
		return true;
	}

	/**
	 * The name of the decorator and should be unique to avoid collisions.
	 * @return name.
	 */
	public abstract String name();

	/**
	 * Lowest integer value will decorate first which means the highest order found
	 * actually has the strongest influence on filtering since its returned logger is the
	 * one used.
	 * @return order.
	 */
	public int order() {
		return 0;
	}

	/**
	 * Decorate a logger. To not decorate simply return the previous logger.
	 * @param rainbowGum rainbow gum passed to the slf4j factory.
	 * @param previousLogger the previous logger in the chain.
	 * @param depth amount of times the logger has been wrapped. If the inputted logger is
	 * returned this number does not increase.
	 * @return decorated logger and if decorated ideally one that implements
	 * {@link WrappingLogger} so that caller info depth is retained.
	 */
	public abstract Logger decorate(RainbowGum rainbowGum, DepthAwareLogger previousLogger, int depth);

	/**
	 * Because wrapping can change the depth of the logger in the callstack this interface
	 * allows loggers to recreate themselves with the proper depth if they support it.
	 */
	public interface DepthAwareLogger extends Logger {

		/**
		 * Will recreate the logger with desired depth.
		 * @param depth new depth.
		 * @return logger with new depth.
		 */
		public Logger withDepth(int depth);

		/**
		 * Will recreate the logger with desired depth if possible.
		 * @param logger to check.
		 * @param depth new depth.
		 * @return logger with new depth or the same logger if not possible.
		 */
		public static Logger withDepth(Logger logger, int depth) {
			if (logger instanceof DepthAwareLogger da) {
				return da.withDepth(depth);
			}
			return logger;
		}

	}

	/**
	 * Because wrapping can change the depth of the logger in the callstack this interface
	 * allows implementations to change the depth of the event builder. It also exposes
	 * read access to whatever has been set on the builder so far - message, cause,
	 * arguments, key values - none of which plain {@link LoggingEventBuilder} exposes, so
	 * a decorator that needs to inspect (not just add to) an event has a way to do so.
	 */
	public interface DepthAwareEventBuilder extends LoggingEventBuilder {

		/**
		 * Sets the depth of an event builder.
		 * @param depth sets depth for caller info.
		 * @return this.
		 */
		public LoggingEventBuilder setDepth(int depth);

		/**
		 * Will recreate the logger with desired depth if possible.
		 * @param eventBuilder event builder to check.
		 * @param depth new depth.
		 * @return logger with new depth or the same logger if not possible.
		 */
		public static LoggingEventBuilder setDepth(LoggingEventBuilder eventBuilder, int depth) {
			if (eventBuilder instanceof DepthAwareEventBuilder da) {
				return da.setDepth(depth);
			}
			return eventBuilder;
		}

		/**
		 * Redirects the output.
		 * @param logger logger.
		 * @return this.
		 */
		LoggingEventBuilder setLogger(LogEventLogger logger);

		/**
		 * The message set on this builder so far, if any. Plain
		 * {@link LoggingEventBuilder} has no such accessor, which makes read-modify-write
		 * use cases (e.g. prefixing) impossible without this.
		 * @return message or null if not set.
		 */
		@Nullable String message();

		/**
		 * The message set on the builder so far, if possible.
		 * @param eventBuilder event builder to check.
		 * @return message, or null if not set or not a {@link DepthAwareEventBuilder}.
		 */
		public static @Nullable String message(LoggingEventBuilder eventBuilder) {
			if (eventBuilder instanceof DepthAwareEventBuilder da) {
				return da.message();
			}
			return null;
		}

		/**
		 * The cause set on this builder so far, if any. Plain {@link LoggingEventBuilder}
		 * has no such accessor.
		 * @return cause or null if not set.
		 */
		@Nullable Throwable throwable();

		/**
		 * The cause set on the builder so far, if possible.
		 * @param eventBuilder event builder to check.
		 * @return cause, or null if not set or not a {@link DepthAwareEventBuilder}.
		 */
		public static @Nullable Throwable throwable(LoggingEventBuilder eventBuilder) {
			if (eventBuilder instanceof DepthAwareEventBuilder da) {
				return da.throwable();
			}
			return null;
		}

		/**
		 * The arguments added to this builder so far, in the order added. Plain
		 * {@link LoggingEventBuilder} has no such accessor.
		 * @return unmodifiable snapshot of the arguments, possibly empty.
		 */
		List<@Nullable Object> arguments();

		/**
		 * The arguments added to the builder so far, if possible.
		 * @param eventBuilder event builder to check.
		 * @return unmodifiable snapshot of the arguments, empty if none or not a
		 * {@link DepthAwareEventBuilder}.
		 */
		public static List<@Nullable Object> arguments(LoggingEventBuilder eventBuilder) {
			if (eventBuilder instanceof DepthAwareEventBuilder da) {
				return da.arguments();
			}
			return List.of();
		}

		/**
		 * The key values added to this builder so far (including any inherited from MDC).
		 * Plain {@link LoggingEventBuilder} has no such accessor.
		 * @return key values, possibly empty.
		 */
		KeyValues keyValues();

		/**
		 * The key values added to the builder so far, if possible.
		 * @param eventBuilder event builder to check.
		 * @return key values, empty if none or not a {@link DepthAwareEventBuilder}.
		 */
		public static KeyValues keyValues(LoggingEventBuilder eventBuilder) {
			if (eventBuilder instanceof DepthAwareEventBuilder da) {
				return da.keyValues();
			}
			return KeyValues.of();
		}

	}

}