CallerInstructionLogic.java

/*
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v2.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.model.processor;

import ch.qos.logback.core.status.InfoStatus;
import ch.qos.logback.core.status.Status;
import ch.qos.logback.core.status.WarnStatus;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static ch.qos.logback.core.CoreConstants.CODES_URL;

/**
 * Detects contradictory caller-data extraction instructions across appenders.
 *
 * <p>During configuration analysis, each appender is associated with an
 * {@link Instruction} describing whether it wants caller data extracted and
 * how. This class checks a map of appender name to instruction for
 * combinations that cannot work together at runtime and returns the
 * corresponding {@link Status} messages.</p>
 *
 * <p>Compatibility rules:</p>
 * <ul>
 *   <li>{@link Instruction#DIRECT_WANT} may appear alone.</li>
 *   <li>{@link Instruction#PREPROCESS_WANT} may coexist with
 *       {@link Instruction#DIRECT_WANT}.</li>
 *   <li>{@link Instruction#DO_NOT_WANT} must not coexist with
 *       {@link Instruction#PREPROCESS_WANT}.</li>
 *   <li>{@link Instruction#DO_NOT_WANT} must not coexist with
 *       {@link Instruction#DIRECT_WANT}.</li>
 *   <li>{@link Instruction#PREPROCESS_WANT} alone is not a valid
 *       configuration.</li>
 * </ul>
 *
 * @since 1.6.2
 * @see CallerContradictionAnalyser
 * @see CallerContradictionWarnAnalyser
 */
public class CallerInstructionLogic {

    static final String CALLER_CONTRADICTION_ANCHOR = "#callerContradiction";
    static final String CALLER_CONTRADICTION_URL = CODES_URL + CALLER_CONTRADICTION_ANCHOR;
    static final String WARNING_MSG_TEMPLATE = "appenders named %s instruct against caller extraction info while other appenders named %s instruct in favor of caller extraction";
    static final String LONE_PREPROCESS_WANT_MSG_TEMPLATE = "appenders named %s instruct preprocessing of caller extraction info but no appender instructs in favor of caller extraction";
    static final String NO_CONTRADICTIONS_MSG = "No contradictions in caller extraction instruction were detected";

    /**
     * How an appender relates to caller-data extraction.
     */
    enum Instruction {
        /**
         * Caller data should be extracted during preprocessing (for example by
         * an {@code AsyncAppender}, {@code SocketAppender} or
         * {@code SMTPAppender} with {@code includeCallerData} set to
         * {@code true}) so that nested appenders, the remote peer or the
         * SMTP layout can use it.
         */
        PREPROCESS_WANT,

        /**
         * Caller data should not be extracted (for example an
         * {@code AsyncAppender}, {@code SocketAppender} or
         * {@code SMTPAppender} with {@code includeCallerData} false or
         * absent, the default).
         */
        DO_NOT_WANT,

        /**
         * The appender itself requires caller data, typically because its
         * layout pattern uses a caller-data converter such as {@code %C},
         * {@code %M}, {@code %L}, {@code %F}, {@code %l}, or
         * {@code %caller}.
         */
        DIRECT_WANT,
    }

    /**
     * Checks the given appender instructions for contradictions.
     *
     * <p>The map maps appender names to the caller-inclusion instruction
     * gathered during analysis of the configuration model. Contradictions
     * are reported as {@link WarnStatus} entries; if none are found, a
     * single {@link InfoStatus} is returned. When one or more contradiction
     * warnings are produced, an additional warning pointing to
     * {@link #CALLER_CONTRADICTION_URL} is appended.</p>
     *
     * @param appenderNameToInstructionMap map of appender name to its
     *        {@link Instruction}; must not be {@code null}
     * @return a non-empty list of status objects describing the outcome of
     *         the contradiction check
     */
    public List<Status> contradiction(Map<String, Instruction> appenderNameToInstructionMap) {
        List<String> preprocessWantList   = new ArrayList<>();
        List<String> doNotWantList = new ArrayList<>();
        List<String> directWantList   = new ArrayList<>();

        for (Map.Entry<String, Instruction> e : appenderNameToInstructionMap.entrySet()) {
            switch (e.getValue()) {
                case PREPROCESS_WANT:
                    preprocessWantList.add(e.getKey());
                    break;
                case DO_NOT_WANT:
                    doNotWantList.add(e.getKey());
                    break;
                case DIRECT_WANT:
                    directWantList.add(e.getKey());
                    break;
            }
        }

        List<Status> result = new ArrayList<>();

        // DIRECT_WANT elements can exist alone
        // one or more PREPROCESS_WANT elements can coexist one or more DIRECT_WANT elements
        // DO_NOT_WANT cannot be allowed to coexist with PREPROCESS_WANT;
        // DO_NOT_WANT cannot be allowed to coexist with DIRECT_WANT;
        // PREPROCESS_WANT alone is not allowed.
        // DO_NOT_WANT and PREPROCESS_WANT are contradictory

        if (!doNotWantList.isEmpty() && !preprocessWantList.isEmpty()) {
            String msg = String.format(
                    WARNING_MSG_TEMPLATE,
                    String.join(", ", doNotWantList),
                    String.join(", ", preprocessWantList));

            result.add(new WarnStatus(msg, this));
        }

        if (!doNotWantList.isEmpty() && !directWantList.isEmpty()) {
            String msg = String.format(
                    WARNING_MSG_TEMPLATE,
                    String.join(", ", doNotWantList),
                    String.join(", ", directWantList));

            result.add(new WarnStatus(msg, this));
        }

        // PREPROCESS_WANT alone (without DIRECT_WANT) is not allowed
        if (!preprocessWantList.isEmpty() && directWantList.isEmpty() && doNotWantList.isEmpty()) {
            String msg = String.format(
                    LONE_PREPROCESS_WANT_MSG_TEMPLATE,
                    String.join(", ", preprocessWantList));
            result.add(new WarnStatus(msg, this));
        }

        if (result.isEmpty()) {
            result.add(new InfoStatus(NO_CONTRADICTIONS_MSG, this));
        } else {
            result.add(new WarnStatus("See "+CALLER_CONTRADICTION_URL+" for details", this));
        }

        return result;
    }



}